Attach: session controls, HITL approval, dry-run#36
Conversation
AttachController gains: an onNavigate HITL approver (can deny a policy-clean page), dry-run mode (simulate navigation/reads without touching the browser), pause/resume, and richer status (paused/dryRun/navigations/lastUrl). New browser_attach_control tool drives pause/resume/dry-run at runtime. Pack + env expose dryRun and deadlineMs for flexibility.
Reviewer's GuideAdds human-in-the-loop navigation gating, dry-run mode, pause/resume and richer status tracking to the attach controller, exposes a runtime Sequence diagram for AttachController.goto with HITL approval and dry-runsequenceDiagram
actor Agent
participant AttachController
participant UrlPolicyEngine
participant NavigationApprover
participant AttachBackend
Agent->>AttachController: goto(url)
AttachController->>UrlPolicyEngine: isNavigationAllowed(url, urlPolicy)
UrlPolicyEngine-->>AttachController: verdict
alt [policy blocked]
AttachController-->>Agent: AttachError POLICY_BLOCKED
else [policy allowed]
alt [onNavigate configured]
AttachController->>NavigationApprover: onNavigate(url)
NavigationApprover-->>AttachController: decision
alt [decision denied]
AttachController-->>Agent: AttachError POLICY_BLOCKED
else [decision approved]
alt [dryRun enabled]
AttachController-->>Agent: return url
else [dryRun disabled]
AttachController->>AttachBackend: gotoTab(tab, url)
AttachBackend-->>AttachController: landedUrl
AttachController-->>Agent: return landedUrl
end
end
else [no onNavigate]
alt [dryRun enabled]
AttachController-->>Agent: return url
else [dryRun disabled]
AttachController->>AttachBackend: gotoTab(tab, url)
AttachBackend-->>AttachController: landedUrl
AttachController-->>Agent: return landedUrl
end
end
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughAttach mode now supports runtime pause/resume, dry-run navigation and reads, navigation approval hooks, session telemetry, configurable deadlines, and a new control tool for changing these states. ChangesAttach runtime controls
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant AttachController
participant NavigationApprover
participant BrowserBackend
Caller->>AttachController: goto(url)
AttachController->>NavigationApprover: approve(url)
NavigationApprover-->>AttachController: approval decision
AttachController->>BrowserBackend: navigate(url) when not dry-run
AttachController-->>Caller: target URL
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="registry/curated/system/browser-automation/src/attach/AttachController.ts" line_range="56-58" />
<code_context>
+ * to block the navigation with a `POLICY_BLOCKED` result. Async so a caller can
+ * prompt a real operator.
+ */
+export type NavigationApprover = (
+ url: string,
+) => boolean | { approved: boolean; reason?: string } | Promise<boolean | { approved: boolean; reason?: string }>;
+
/** Controller configuration. */
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against `null` decisions from `NavigationApprover` to avoid runtime errors.
In `goto`, `NavigationApprover` results are handled as either boolean or object, but `typeof null === 'object'`. If an approver erroneously returns `null`, the branch `typeof decision === 'object' && decision.reason` will dereference `reason` on `null` and throw. Please add a null-safe check (e.g. `decision && typeof decision === 'object'`) to avoid this runtime error while keeping the existing API.
</issue_to_address>
### Comment 2
<location path="registry/curated/system/browser-automation/src/attach/tools.ts" line_range="138-147" />
<code_context>
+export class AttachControlTool {
</code_context>
<issue_to_address>
**suggestion:** Align the `execute` argument typing with `inputSchema` for extensibility.
`inputSchema` treats `action` as a generic string enum, while `execute` uses a hard-coded union (`'pause' | 'resume' | 'dry_run_on' | 'dry_run_off' | 'status'`). This requires manual updates whenever new actions are added. Define a shared `type` (or derive the union from a constant) so `inputSchema` and `execute` stay in sync automatically.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| export type NavigationApprover = ( | ||
| url: string, | ||
| ) => boolean | { approved: boolean; reason?: string } | Promise<boolean | { approved: boolean; reason?: string }>; |
There was a problem hiding this comment.
issue (bug_risk): Guard against null decisions from NavigationApprover to avoid runtime errors.
In goto, NavigationApprover results are handled as either boolean or object, but typeof null === 'object'. If an approver erroneously returns null, the branch typeof decision === 'object' && decision.reason will dereference reason on null and throw. Please add a null-safe check (e.g. decision && typeof decision === 'object') to avoid this runtime error while keeping the existing API.
| export class AttachControlTool { | ||
| readonly id = 'browser_attach_control'; | ||
| readonly name = 'browser_attach_control'; | ||
| readonly displayName = 'Attach: session control'; | ||
| readonly description = | ||
| 'Control the attach session at runtime: pause (refuse further ops), resume, or toggle dry-run (simulate navigation/reads without touching the browser). Returns the updated status.'; | ||
| readonly category = 'browser'; | ||
| readonly version = '0.1.0'; | ||
| readonly hasSideEffects = true; | ||
| readonly inputSchema = { |
There was a problem hiding this comment.
suggestion: Align the execute argument typing with inputSchema for extensibility.
inputSchema treats action as a generic string enum, while execute uses a hard-coded union ('pause' | 'resume' | 'dry_run_on' | 'dry_run_off' | 'status'). This requires manual updates whenever new actions are added. Define a shared type (or derive the union from a constant) so inputSchema and execute stay in sync automatically.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a805e08c49
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| // HITL gate: after the policy passes, an approver may still block the page. | ||
| if (this.opts.onNavigate) { | ||
| const decision = await this.opts.onNavigate(url); |
There was a problem hiding this comment.
Put async approval under the operation guard
With an async onNavigate implementation, such as one waiting for an operator, this await happens while the controller still reports ready and before the existing deadline-wrapped navigation path starts. In hosts that can dispatch another attach tool call during that prompt, the second call can pass requireReady() and navigate/read the same agent tab before the first approved navigation resumes; if the prompt stalls, deadlineMs also no longer bounds the goto() call. Move the approval wait under a busy state/mutex and deadline, restoring ready on denial.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@registry/curated/system/browser-automation/src/attach/AttachController.ts`:
- Around line 196-204: Update the navigation approval flow in the
AttachController method containing onNavigate to await the decision through
withDeadline using the applicable deadlineMs, preventing an unresolved operator
prompt from blocking indefinitely. After the approval resolves and before
requireLease or any dry-run/backend navigation side effect, call requireReady()
again so pauses issued during the wait are enforced; preserve the existing
denial handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: b348fd25-39dc-4e0d-a572-27d040d6022b
📒 Files selected for processing (6)
registry/curated/system/browser-automation/src/attach/AttachController.tsregistry/curated/system/browser-automation/src/attach/__tests__/AttachController.test.tsregistry/curated/system/browser-automation/src/attach/__tests__/pack-registration.test.tsregistry/curated/system/browser-automation/src/attach/__tests__/tools.test.tsregistry/curated/system/browser-automation/src/attach/tools.tsregistry/curated/system/browser-automation/src/index.ts
| if (this.opts.onNavigate) { | ||
| const decision = await this.opts.onNavigate(url); | ||
| const approved = typeof decision === 'boolean' ? decision : decision.approved; | ||
| if (!approved) { | ||
| const reason = typeof decision === 'object' && decision.reason ? decision.reason : 'operator denied'; | ||
| throw new AttachError('POLICY_BLOCKED', `navigation to "${url}" denied by approver: ${reason}`); | ||
| } | ||
| } | ||
| requireLease(this.opts.leaseFile, this.lease!.nonce); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Bound approval waits and re-check pause state.
Line 197 awaits onNavigate without deadlineMs, so an operator prompt that never resolves leaves goto() stuck indefinitely. A pause issued during that await is also bypassed when approval later resolves. Wrap the decision in withDeadline(...) and call requireReady() again before the dry-run/backend side effect.
Proposed fix
if (this.opts.onNavigate) {
- const decision = await this.opts.onNavigate(url);
+ const decision = await withDeadline(
+ Promise.resolve(this.opts.onNavigate(url)),
+ this.deadline,
+ 'onNavigate',
+ );
const approved = typeof decision === 'boolean' ? decision : decision.approved;
if (!approved) {
const reason = typeof decision === 'object' && decision.reason ? decision.reason : 'operator denied';
throw new AttachError('POLICY_BLOCKED', `navigation to "${url}" denied by approver: ${reason}`);
}
}
+this.requireReady();
requireLease(this.opts.leaseFile, this.lease!.nonce);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (this.opts.onNavigate) { | |
| const decision = await this.opts.onNavigate(url); | |
| const approved = typeof decision === 'boolean' ? decision : decision.approved; | |
| if (!approved) { | |
| const reason = typeof decision === 'object' && decision.reason ? decision.reason : 'operator denied'; | |
| throw new AttachError('POLICY_BLOCKED', `navigation to "${url}" denied by approver: ${reason}`); | |
| } | |
| } | |
| requireLease(this.opts.leaseFile, this.lease!.nonce); | |
| if (this.opts.onNavigate) { | |
| const decision = await withDeadline( | |
| Promise.resolve(this.opts.onNavigate(url)), | |
| this.deadline, | |
| 'onNavigate', | |
| ); | |
| const approved = typeof decision === 'boolean' ? decision : decision.approved; | |
| if (!approved) { | |
| const reason = typeof decision === 'object' && decision.reason ? decision.reason : 'operator denied'; | |
| throw new AttachError('POLICY_BLOCKED', `navigation to "${url}" denied by approver: ${reason}`); | |
| } | |
| } | |
| this.requireReady(); | |
| requireLease(this.opts.leaseFile, this.lease!.nonce); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@registry/curated/system/browser-automation/src/attach/AttachController.ts`
around lines 196 - 204, Update the navigation approval flow in the
AttachController method containing onNavigate to await the decision through
withDeadline using the applicable deadlineMs, preventing an unresolved operator
prompt from blocking indefinitely. After the approval resolves and before
requireLease or any dry-run/backend navigation side effect, call requireReady()
again so pauses issued during the wait are enforced; preserve the existing
denial handling.
Robustness + control features for the attach lane:
onNavigate): an approver can deny a policy-clean navigation — per-page operator gating.browser_attach_controltool: pause/resume/dry-run at runtime.dryRunanddeadlineMs.All unit-tested.
Summary by Sourcery
Add human-in-the-loop and dry-run controls to browser attach sessions, with richer status tracking and runtime tooling.
New Features:
Enhancements:
Tests:
Summary by CodeRabbit