diff --git a/CHANGELOG.md b/CHANGELOG.md index a4081f9..7b694e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Changelog ## Unreleased -- Changed `browser_handoff` into an asynchronous start operation. It now returns `completed` immediately after the durable handoff and global browser blackout are active, allowing the initiating agent to continue independent non-browser work while the user completes the step; browser observation and mutation remain blocked until the handoff clears. -- Added a disabled-by-default popup setting named YOLO mode. When enabled, recognizable consequential controls execute in the original `browser_act` call instead of entering Commit review; enabling it discards pending staged actions while task ownership, origin policy, revision checks, handoff blackout, credential isolation, and all other Standard boundaries remain enforced. +- Made `browser_handoff` a nonblocking coordination marker. It persists the active handoff and completion condition, focuses the declared tab, and returns immediately without pausing browser observations or mutations; explicit **Pause agents** remains available when the page must stay unobservable. +- Enabled YOLO mode for new and legacy default state so recognizable consequential controls execute in the original `browser_act` call. Turning YOLO mode off restores staged Commit review. Task ownership, origin policy, revision checks, credential isolation, and all other Standard boundaries remain enforced. - Replaced the Chrome Bridge v1 runtime with the AgentTab 2.0 release candidate: a Rust production host over OS-native local IPC, nine task-scoped Standard methods, explicit resumable capabilities, a developer-only tenth method, TypeScript and Python SDKs, MCP and OMP adapters, a transactional installer, and a minimal extension. Consequential controls now use a two-party Commit flow: `browser_act` stages an exact effect, the popup approves the durable review record without executing it, and the requesting task must consume its private one-use token through `browser_commit`. - Added explicit `browser_finish` lifecycle finalization across Core RPC, the extension, TypeScript and Python SDKs, CLI, MCP, OMP, and Pi. Automatic cleanup tracks tab provenance, closes task-created tabs, retains adopted tabs, ungroups retained tabs, and releases ownership; popup policy can require confirmation or retain all tabs, while active handoff, Commit review, and in-flight work defer cleanup without destroying resumability. - Fixed OMP adapter compatibility with providers that reject top-level union tool schemas. `browser_open` and `browser_snapshot` now expose provider-compatible object schemas while retaining strict runtime validation for their mode-specific parameters. - Added a focus-safe GPT-Control browser driver backed by AgentTab task ownership and durable resume capabilities. It creates background task tabs, fills and sends prompts through Standard actions and Commit review, uploads through a bounded file-input selector, captures the latest assistant message without activating Chrome, persists session state owner-only, and closes the underlying task explicitly. -- Added a disabled-by-default, host-managed 1Password credential broker. `browser_credentials` derives the current origin from task-owned host state, accepts at most three matching Login items and attempts, and uses short-lived one-use tokens to fill username, password, or one-time-code refs without exposing values through Core RPC, MCP, OMP, Pi, responses, or audit output. More than three matches, provider failures, exhausted attempts, and unsupported verification return `needs_user`. The extension's private fill path blocks capture before injecting selected values and never submits the form. +- Enabled the host-managed 1Password credential broker by default, with an explicit `one_password.enabled: false` policy opt-out and platform-specific `op` command resolution. `browser_credentials` derives the current origin from task-owned host state, accepts at most three matching Login items and attempts, and uses short-lived one-use tokens to fill username, password, or one-time-code refs without exposing values through Core RPC, MCP, OMP, Pi, responses, or audit output. More than three matches, provider failures, exhausted attempts, and unsupported verification return `needs_user`. +- Hardened persisted-state migration so legacy task records cannot brick extension startup. Builds that tracked `createdTabIds` as a cleanup ledger left entries for tabs that had already left the task; the state parser now sanitizes those to the current subset invariant instead of rejecting the whole persisted state, and the handoff completion schema documents that `kind: "url"` matches the tab URL exactly. - Fixed credential-broker activation against upgraded local state and GUI-launched browser hosts. Legacy `staged_commits` columns are migrated before dependent indexes are created, and policy can pin an absolute 1Password CLI executable when the browser process does not inherit the user's shell `PATH`. - Added task-owned background window creation to Standard `browser_open` through `placement: "new_window"`. It is operation-specific rather than a general window-control grant: only an otherwise empty task can request it, the extension creates an unfocused normal window, `background: false` is rejected, ownership is derived from persisted task state, and a failed visible group grant removes the new tab. Focus, state changes, and closure of unrelated windows remain unavailable. - Added explicit restricted-origin routing for task tabs. `browser_open` and `browser_tabs` now report `automation_route: "full" | "tab_only"`; Chrome system pages, extension pages, DevTools, and the Chrome Web Store retain task-owned explicit navigation, reload, close, and bounded waits, while page inspection, interaction, and raw CDP fail before execution with the stable `browser_restricted_origin` / `not_started` result and non-retry recovery. History movement remains available without managed origin constraints and fails closed when constraints are configured because Chrome does not expose its destination for pre-navigation authorization. diff --git a/README.md b/README.md index 41cc5dc..df5669d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ > Give an agent a tab, not the keys to your browser. -AgentTab lets an agent work in your existing signed-in Chrome profile without giving it unrestricted control of the profile. Each connection receives a task-owned browser workspace. The agent can create tabs, inspect and act in those tabs, wait for page state, and ask for help. A disabled-by-default 1Password broker can fill a matching login or one-time code without exposing its value to the agent; passkeys, security keys, CAPTCHA, payment secrets, account recovery, and unsupported verification remain **Your Turn**. Recognizable consequential actions are staged for **Commit** instead of being performed immediately. +AgentTab lets an agent work in your existing signed-in Chrome profile without giving it unrestricted control of the profile. Each connection receives a task-owned browser workspace. The agent can create tabs, inspect and act in those tabs, wait for page state, and ask for help. The built-in 1Password broker is available by default and can fill a matching login or one-time code without exposing its value to the agent; passkeys, security keys, CAPTCHA, payment secrets, account recovery, and unsupported verification remain **Your Turn**. Recognizable consequential actions execute directly by default, while the popup can enable a staged **Commit** review when desired. ## Release status @@ -24,8 +24,8 @@ The command has no path, token, or shell-specific argument and is suitable for P 1. An agent calls `browser_open` with `mode: "create"`. AgentTab creates a background tab for that task and returns its task, tab, window, page-revision, and automation-route identifiers. `placement: "new_window"` may create the task's first tab in a separate unfocused normal window. 2. On a normal web origin, the agent calls `browser_snapshot`, works from revisioned accessibility references, then calls `browser_act` with the expected page revision. It cannot act on unrelated tabs. -3. If managed policy enables 1Password and an ordinary sign-in page has at most three origin-matching Login items, the agent can request a short-lived opaque token and ask the host to fill named field refs. Credential values travel only from `op` to the host and extension, never through Core RPC or the adapter. Every other human-only input uses `browser_handoff`, which focuses that tab, pauses automation, and blocks browser observation until the declared completion condition or **I'm done**. -4. If AgentTab recognizes a send, publish, purchase, delete, upload, authorization, or permission-grant control, `browser_act` can return `commit_required`. The extension shows the staged effect in its popup. A human must approve it there before the agent can call `browser_commit` with the one-use staged token. The disabled-by-default YOLO mode setting skips this Commit review and executes recognized consequential controls in the original `browser_act` call. +3. On an ordinary sign-in page with at most three origin-matching Login items, the agent can request a short-lived opaque token and ask the host to fill named field refs through the local `op` command. Credential values travel only from `op` to the host and extension, never through Core RPC or the adapter. Owner-only policy can disable or constrain this broker. Every other human-only input uses `browser_handoff`, which focuses that tab and records a durable completion condition while browser automation remains available. +4. Recognized send, publish, purchase, delete, upload, authorization, and permission-grant controls execute in the original `browser_act` call by default. Turn off YOLO mode in the popup to require Commit review instead. In review mode, AgentTab stages the control, shows its effect in the popup, requires human approval, and then accepts the one-use token through `browser_commit`. 5. The task can list only its own tabs with `browser_tabs`. A separate client gets a separate task unless it proves its durable resume capability. 6. When browser work is complete, the agent calls `browser_finish`. Automatic cleanup closes tabs created by the task, preserves tabs adopted from the user's existing browser state, ungroups retained tabs, and releases task ownership. The popup setting can instead require confirmation or retain every tab. @@ -36,8 +36,8 @@ Commit is a two-party, best-effort semantic barrier, not proof that a page has n ## Trust contract - **Task ownership is an execution and coordination boundary, not profile isolation.** AgentTab can use the signed-in session in the browser profile, but Standard mode does not expose raw cookies, storage, passwords, arbitrary JavaScript, raw CDP, coordinate actions, network interception, or a generic browser-global mutation API. Its one window-level operation creates an unfocused normal window for the first tab of an otherwise empty task. -- **Your Turn is the only routine focus transition.** Routine task work stays in task-owned tabs. During handoff, all agent observation and capture are denied so human credentials are not captured. -- **Commit requires human approval and agent intent unless YOLO mode is enabled.** By default, a staged action is bound to its task, tab, page revision, element fingerprint, effect, and short expiry. Popup approval records consent but does not execute it. The agent must then call `browser_commit`; a changed page, expired stage, used token, or unapproved stage cannot execute. YOLO mode is an explicit local opt-out from Commit review, not from ownership, origin policy, revisions, handoff blackout, or credential boundaries. +- **Your Turn is the only routine focus transition.** Routine task work stays in task-owned tabs. Handoff focuses the declared tab and records a durable completion condition without globally pausing browser work. This permissive default does not guarantee an observation blackout while the user types; prefer `browser_credentials` for ordinary sign-in fields because its values never enter AgentTab RPC or audit data. +- **Consequential actions run directly by default; Commit review is available.** YOLO mode skips the staging step but not task ownership, origin policy, expected page revisions, restricted-origin routing, credential isolation, or action validation. Turning YOLO mode off binds each staged action to its task, tab, page revision, element fingerprint, effect, and short expiry. Popup approval records consent but does not execute it; the agent must call `browser_commit`. - **Local by default.** Policy, task state, audit records, and IPC stay on the machine. AgentTab has no telemetry. See [Telemetry](docs/telemetry.md) and [Security](docs/security.md). ## Tool surface @@ -53,7 +53,7 @@ Standard mode exposes exactly nine tools: | `browser_tabs` | List only tabs owned by the current task, including each tab's automation route. | | `browser_handoff` | Give the user control for human-only input. | | `browser_commit` | Execute one staged consequential action. | -| `browser_credentials` | Prepare and fill an origin-matching 1Password login through opaque, short-lived host tokens when managed policy explicitly enables it. | +| `browser_credentials` | Prepare and fill an origin-matching 1Password login through opaque, short-lived host tokens. Available by default when the local `op` CLI is usable; owner-only policy can disable or constrain it. | | `browser_finish` | Finish the task, apply its cleanup policy, return closed and retained tab receipts, and release ownership. | Developer mode adds one tool, `browser_developer`. It is absent from Standard discovery. It requires both the persistent Developer mode control in the AgentTab popup and `AGENTTAB_DEVELOPER=1` in the adapter environment. Treat it as an explicit expansion of the normal boundary. diff --git a/docs/adr/0001-agenttab-runtime.md b/docs/adr/0001-agenttab-runtime.md index 7dd7759..754cd7f 100644 --- a/docs/adr/0001-agenttab-runtime.md +++ b/docs/adr/0001-agenttab-runtime.md @@ -26,19 +26,19 @@ A task workspace is visible in Chrome. Task-owned tabs are grouped for display, ### Your Turn -**Your Turn** is the human-only input boundary. AgentTab MUST hand control to the user for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and any credential workflow that returns `needs_user`. A disabled-by-default managed 1Password broker MAY fill an origin-matching Login item through the private host-to-extension path, but MUST NOT expose the value to an agent or submit the form. +**Your Turn** is the human-only input boundary. AgentTab MUST hand control to the user for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and any credential workflow that returns `needs_user`. The managed 1Password broker is available by default and MAY fill an origin-matching Login item through the private host-to-extension path, but MUST NOT expose the value to an agent or submit the form. Owner-only policy MAY disable or constrain the broker. -While any Your Turn handoff is active, AgentTab MUST enforce a global observation blackout across every task and client. The extension and host each fail closed. AgentTab MUST NOT capture human keystrokes. Handoff clears only after its declared completion condition or explicit Done, capture scrubbing, and host acknowledgement. +An active handoff MUST be a durable coordination marker, not an implicit automation pause. AgentTab MUST persist the handoff and completion condition before focusing the declared tab, then keep browser requests eligible. Product and security copy MUST state that handoff does not guarantee an observation blackout and MUST direct ordinary username, password, and one-time-code entry through `browser_credentials`. Explicit **Pause agents** remains the owner-controlled confidentiality boundary. ### Commit **Commit** is a best-effort semantic review barrier for recognizable consequential controls, including send, publish, purchase, delete, upload, authorization, and permission grants. -Every Standard-mode mutation MUST pass through one extension-side `prepare -> classify -> revalidate -> execute` choke point. A recognizable consequential action is staged before any side effect. Its token is bound to the task, tab, effect class, exact element fingerprint, document revision, event, preview, and a five-minute expiry. The extension popup MUST send only an opaque review handle. Human approval MUST durably mark the corresponding stage approved without consuming it or dispatching the browser action. Only a later agent `browser_commit` carrying the private staged token may consume and execute the approved stage. Execution MUST reject an unapproved, changed, expired, foreign, or used stage, revalidate the target, and dispatch at most once. +Every Standard-mode mutation MUST pass through one extension-side `prepare -> classify -> revalidate -> execute` choke point. YOLO mode is enabled by default, so recognizable consequential actions execute in the original mutation. When the user turns YOLO mode off, a recognizable consequential action is staged before any side effect. Its token is bound to the task, tab, effect class, exact element fingerprint, document revision, event, preview, and a five-minute expiry. The extension popup MUST send only an opaque review handle. Human approval MUST durably mark the corresponding stage approved without consuming it or dispatching the browser action. Only a later agent `browser_commit` carrying the private staged token may consume and execute the approved stage. Execution MUST reject an unapproved, changed, expired, foreign, or used stage, revalidate the target, and dispatch at most once. Commit does not guarantee recognition of every page-triggered external effect. A page can attach a consequential effect to an innocently labelled control. Product and security copy MUST describe Commit as risk reduction, not proof of semantic safety. -Action batches are sequential and non-atomic. A batch stops before its first staged operation and returns the completed prefix plus staged index. Committing that staged operation MUST NOT execute later operations implicitly. +Action batches are sequential and non-atomic. With YOLO mode off, a batch stops before its first staged operation and returns the completed prefix plus staged index. Committing that staged operation MUST NOT execute later operations implicitly. With YOLO mode on, the recognized action executes inline and the batch continues. ## Security boundary diff --git a/docs/benchmarks.md b/docs/benchmarks.md index cde4a72..21e4d52 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -27,7 +27,7 @@ When comparing another surface, run the same scenario, user-visible success crit Do not convert security barriers into speed-only scores. If measuring Commit, record classification result, stage creation, human review delay as a separate interval, revalidation outcome, and execution or refusal. Never Commit a real consequential action only to collect a timing number. -If measuring handoff, record only safe lifecycle timestamps such as request accepted, blackout active, completion acknowledged, and resume ready. Do not record keys, secrets, page contents, screenshots, or human input. +If measuring handoff, record only safe lifecycle timestamps such as request accepted, marker active, completion acknowledged, and marker cleared. Do not record keys, secrets, page contents, screenshots, or human input. ## Publishing a result diff --git a/docs/launch/chrome-web-store.md b/docs/launch/chrome-web-store.md index 24fcc2b..9305da5 100644 --- a/docs/launch/chrome-web-store.md +++ b/docs/launch/chrome-web-store.md @@ -28,13 +28,13 @@ An agent starts with a task workspace, not general access to every tab. AgentTab The runtime consists of one minimal MV3 extension, a local Rust host, and per-user operating-system-native IPC. The extension uses Chrome Native Messaging to reach the local host. Client adapters, including MCP, connect to the host through a user-owned Unix socket on macOS and Linux or a current-user named pipe on Windows. AgentTab has no cloud relay, remote browser session, telemetry service, or routine network control plane. -Standard MCP access exposes exactly nine tools: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The finalization tool applies provenance-aware cleanup: task-created tabs close by default, adopted tabs are retained, retained tabs are ungrouped, and task ownership is released. The credential tool is inert unless managed policy explicitly enables the local 1Password broker, and it never returns a credential value. `browser_developer` is available only after a persistent, explicit Developer mode opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. +Standard MCP access exposes exactly nine tools: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The finalization tool applies provenance-aware cleanup: task-created tabs close by default, adopted tabs are retained, retained tabs are ungrouped, and task ownership is released. The local 1Password broker is available by default unless owner-only policy disables it, and the credential tool never returns a credential value. `browser_developer` is available only after a persistent, explicit Developer mode opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. ### Human controls -The optional local 1Password broker can fill one of at most three origin-matching Login items directly into a selected field without revealing the value to the agent. **Your Turn** remains the path for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, or a broker result that needs the user. During a handoff, AgentTab applies an observation blackout: standard capture and observation requests for every task return `needs_user`. The runtime clears the blackout only after the declared completion condition or explicit Done and its recovery checks. AgentTab does not capture human keystrokes. +The local 1Password broker can fill one of at most three origin-matching Login items directly into a selected field without revealing the value to the agent. **Your Turn** remains the path for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, or a broker result that needs the user. Handoff records the completion condition and focuses the declared tab without globally pausing browser work. It is not an observation blackout; users can explicitly pause agents first when the page state must remain unobservable. -**Commit** is a best-effort review barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. Before acting, AgentTab prepares, classifies, and revalidates the target. A recognizable consequential action is staged with a preview, then requires approval in a human popup and the requesting agent's one-use token. The record expires after a short interval, cannot be replayed, and is invalidated if the page or target changes. Harmless actions proceed without Commit review. Commit reduces recognizable risk; it cannot prove that a page has no hidden external effect. +Recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants execute directly by default. Users can turn YOLO mode off to enable the best-effort **Commit** review barrier. In that mode, AgentTab stages a preview and requires approval in a human popup plus the requesting agent's one-use token. Commit reduces recognizable risk; it cannot prove that a page has no hidden external effect. ### Trust boundary @@ -52,7 +52,7 @@ This section is draft review copy for the v2 contract. It must be reconciled aga | `debugger` | Required permission | Supports the task-scoped browser capabilities required for accessibility snapshots, precise click, type, fill, select, scroll, key press, inactive screenshots, network-idle observation, and exact download completion attribution. AgentTab attaches lazily only to task-owned tabs, reuses the task connection while needed, and exposes no generic CDP method in Standard mode. | | `tabs` | Required permission | Lets AgentTab create and visibly adopt task tabs, track their lifecycle and document revision, focus a handoff tab when the user asks, and clean up a closed task. It is not used to make unrelated tabs owned by an agent. | | `tabGroups` | Required permission | Shows task-owned tabs as a visible workspace with working, needs-you, or finished status. Group membership is display-only and never authorizes an operation. Removing or moving a tab out of its task group revokes its ownership. | -| `storage` | Required permission | Persists the minimum extension state needed to recover task status, pause state, handoff blackout state, revision floors, and user interface preferences across MV3 service-worker restarts. It is not an analytics store and is not used to collect browsing history. | +| `storage` | Required permission | Persists the minimum extension state needed to recover task status, pause state, handoff state, revision floors, and user interface preferences across MV3 service-worker restarts. It is not an analytics store and is not used to collect browsing history. | | `alarms` | Required permission | Schedules bounded MV3 lifecycle work such as reconnect, expiry, and recovery checks after service-worker suspension. It is not used for tracking, advertising, or remote scheduling. | | `scripting` | Optional permission | Requested only after the user explicitly clicks **Enable AgentTab automation** in the AgentTab popup. It is not a required install-time permission, denial leaves the extension visibly disabled, and it does not add a Standard raw-script API. | | `` | Required host permission | Required so the `chrome.scripting` text, HTML, selector, wait, and scroll paths can run on the task-owned page the user directs AgentTab to use, regardless of its site. It does not let an agent claim tabs or expose raw cookies, browser storage, arbitrary JavaScript, CDP, or network APIs in Standard mode. | @@ -65,10 +65,10 @@ These notes are for a controlled reviewer package only. They are not public inst 2. Provide the exact `v2.0.0-rc.1` extension package together with the matching separately installed local AgentTab host. The extension should report that it is disconnected until the compatible local host is ready. 3. Reconcile the package identity and native-host allowed origins with `config/identity.json` before review. Do not infer an identity from this document or treat it as store publication evidence. 4. Demonstrate a local MCP client opening a task workspace, taking an accessibility snapshot, performing a harmless action, waiting for a defined condition, and listing only that task's tabs. -5. Demonstrate Your Turn with a harmless test page. Verify that observations from every task return `needs_user` during the handoff and that the agent resumes only after Done or the declared completion condition. -6. Demonstrate Commit with a controlled test control labelled as a send, upload, delete, authorization, or permission action. Verify that no side effect occurs before the human popup approves the staged action with the requesting agent's one-use token. Do not use a real message, purchase, upload, deletion, or authorization. +5. Demonstrate Your Turn with a harmless test page. Verify that the handoff marker persists while other browser work remains available and clears after Done or the declared completion condition. +6. Demonstrate default inline execution with a harmless controlled effect. Then turn YOLO mode off and verify that a controlled recognizable action does not execute before popup approval and the requesting agent's one-use Commit token. Do not use a real message, purchase, upload, deletion, or authorization. 7. Demonstrate Pause and Resume, including that queued work does not start after Pause and that task status remains visible after recovery. -8. Verify that Standard discovery exposes exactly the nine Standard tools, that `browser_finish` retains an adopted tab while releasing its task ownership, that `browser_credentials` returns a disabled-policy result before any provider call, and that the Developer-only tool is absent until the reviewer explicitly enables Developer mode. +8. Verify that Standard discovery exposes exactly the nine Standard tools, that `browser_finish` retains an adopted tab while releasing its task ownership, that `browser_credentials` reaches a fake provider by default and returns a disabled-policy result after explicit opt-out, and that the Developer-only tool is absent until the reviewer explicitly enables Developer mode. ## Privacy declaration draft diff --git a/docs/launch/directory-listings.md b/docs/launch/directory-listings.md index e6c0d8c..ac3d1cd 100644 --- a/docs/launch/directory-listings.md +++ b/docs/launch/directory-listings.md @@ -30,12 +30,12 @@ Each agent begins with a task workspace. AgentTab creates or visibly adopts a ta The runtime is local-only. One minimal MV3 extension connects through Chrome Native Messaging to a local Rust host. MCP and other local adapters connect to the host through a user-owned Unix socket on macOS and Linux or a current-user named pipe on Windows. There is no cloud relay, hosted browser session, telemetry service, or routine remote control plane. -The Standard MCP surface has exactly nine tools: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The finalization tool applies provenance-aware cleanup: task-created tabs close by default, adopted tabs are retained, retained tabs are ungrouped, and task ownership is released. The credential tool is inert unless managed policy explicitly enables the local 1Password broker, and it never returns a credential value. A separate `browser_developer` tool exists only after a persistent, explicit Developer mode opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. +The Standard MCP surface has exactly nine tools: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The finalization tool applies provenance-aware cleanup: task-created tabs close by default, adopted tabs are retained, retained tabs are ungrouped, and task ownership is released. The local 1Password broker is available by default unless owner-only policy disables it, and the credential tool never returns a credential value. A separate `browser_developer` tool exists only after a persistent, explicit Developer mode opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. AgentTab declares the `` host permission so its defined `chrome.scripting` text, HTML, selector, wait, and scroll paths can operate in task-owned pages that the user directs an agent to use. This supports those bounded paths across sites; it does not add raw cookie, storage, arbitrary JavaScript, CDP, or network APIs to Standard mode. -The optional local 1Password broker can fill one of at most three origin-matching Login items directly into a selected field without revealing the value to the agent. **Your Turn** remains the human handoff state for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, or a broker result that needs the user. During handoff, AgentTab applies an observation blackout across every task, so standard observations return `needs_user`; it does not capture human keystrokes. +The local 1Password broker can fill one of at most three origin-matching Login items directly into a selected field without revealing the value to the agent. **Your Turn** remains the human handoff state for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, or a broker result that needs the user. Handoff remains nonblocking and does not provide an observation blackout; explicit Pause is available when the page state must remain unobservable. -**Commit** is a best-effort review barrier for recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls. It stages a recognizable action with a preview, requires a human popup approval and the requesting agent's one-use token, and revalidates the page and target before execution. It is not a guarantee that every page-triggered external effect is recognizable. +Recognizable consequential actions execute directly by default. Turning YOLO mode off enables the best-effort **Commit** review barrier for send, publish, purchase, delete, upload, authorization, and permission-grant controls. Commit stages a recognizable action with a preview, requires human popup approval plus the requesting agent's one-use token, and revalidates the page and target before execution. It is not a guarantee that every page-triggered external effect is recognizable. Task ownership coordinates work but does not isolate the signed-in Chrome profile. An agent acting in an owned tab can use the same web session available to the person at the keyboard. Users should connect only trusted local agents and software. Hostile page content and misleading controls remain risks, including prompt injection and effects that Commit cannot classify correctly. diff --git a/docs/launch/reddit-posts.md b/docs/launch/reddit-posts.md index e33cfc4..cd2adc4 100644 --- a/docs/launch/reddit-posts.md +++ b/docs/launch/reddit-posts.md @@ -16,14 +16,14 @@ The starting point is a task workspace rather than general browser control. An a The runtime is local-only: one minimal MV3 extension, a local Rust host, Chrome Native Messaging between them, and per-user operating-system-native IPC for local clients. MCP is an adapter to that local runtime. On macOS and Linux the host uses a user-owned Unix socket; on Windows it uses a current-user named pipe. There is no cloud browser, cloud relay, telemetry service, or routine remote control plane. -The Standard MCP surface is intentionally small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The credential tool is disabled until managed policy enables the local 1Password broker and never returns a credential value. The only additional tool is `browser_developer`, and it requires a persistent explicit Developer mode opt-in. Standard mode does not hand agents raw cookies, browser storage, arbitrary scripts, raw CDP, or raw network APIs. +The Standard MCP surface is intentionally small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The local 1Password broker is available by default unless owner-only policy disables it, and the credential tool never returns a credential value. The only additional tool is `browser_developer`, and it requires a persistent explicit Developer mode opt-in. Standard mode does not hand agents raw cookies, browser storage, arbitrary scripts, raw CDP, or raw network APIs. The extension declares the `` host permission so its defined `chrome.scripting` text, HTML, selector, wait, and scroll paths can work in task-owned pages a person directs the agent to use. This broad site reach does not expose raw cookie, storage, arbitrary JavaScript, CDP, or network APIs in Standard mode. -Two controls define the human boundary. A disabled-by-default local 1Password broker can fill one of at most three origin-matching Login items without returning a value to the agent. **Your Turn** handles passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and broker results that need the user. During that handoff, AgentTab blackouts standard observation for every task, so captures return `needs_user`; it does not capture the person's keystrokes. **Commit** is a best-effort review barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. It stages the action, shows a human popup preview, and requires that human's approval plus the requesting agent's one-use token before execution. It revalidates the page and element first. +Two controls define the human boundary. The local 1Password broker can fill one of at most three origin-matching Login items without returning a value to the agent. **Your Turn** handles passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and broker results that need the user. Handoff stays nonblocking and does not guarantee an observation blackout; explicit Pause is available when the page must remain unobservable. Recognizable consequential actions execute directly by default. Turning YOLO mode off enables the best-effort **Commit** review barrier, which stages an action and requires human popup approval plus the requesting agent's one-use token before execution. -This is not profile isolation. An owned tab still runs in the signed-in Chrome profile the person uses. A hostile page can contain prompt injection, and a control can produce an effect that is not recognizable from its visible label. Your Turn and Commit reduce bounded risks but cannot remove them. The local agent and the local software attached to the profile must still be trusted. +This is not profile isolation. An owned tab still runs in the signed-in Chrome profile the person uses. A hostile page can contain prompt injection, and a control can produce an effect that is not recognizable from its visible label. Task ownership, brokered credentials, explicit Pause, and optional Commit review address bounded risks but cannot remove them. The local agent and the local software attached to the profile must still be trusted. -AgentTab `v2.0.0-rc.1` is unreleased and this is not a launch post. I am preserving the draft for later feedback on task ownership, the global handoff blackout, Commit's best-effort semantics, and the nine-tool MCP default. +AgentTab `v2.0.0-rc.1` is unreleased and this is not a launch post. I am preserving the draft for later feedback on task ownership, nonblocking handoff, optional Commit review, and the nine-tool MCP default. ## Local-first and privacy community variant @@ -39,9 +39,9 @@ AgentTab is a **Local browser runtime for AI agents**. It gives each agent a vis The architecture is deliberately local. A minimal MV3 extension connects to one Rust host through Chrome Native Messaging. Local MCP clients use per-user operating-system-native IPC to reach the host, rather than a network listener or remote service. The runtime has no cloud relay, hosted browser session, analytics, or telemetry. -The handoff model is called **Your Turn**. A disabled-by-default local 1Password broker can fill one of at most three origin-matching Login items without revealing a value to the agent. If a task reaches a passkey, security key, CAPTCHA, payment secret, account recovery, unsupported verification, or broker result that needs the user, the person takes over. AgentTab persists that state and blackouts observation for every task while the person works. Once the person signals Done or the declared completion condition is reached, the runtime scrubs the handoff path before normal observation resumes. +The handoff model is called **Your Turn**. The local 1Password broker is available by default and can fill one of at most three origin-matching Login items without revealing a value to the agent. If a task reaches a passkey, security key, CAPTCHA, payment secret, account recovery, unsupported verification, or broker result that needs the user, the person takes over. AgentTab persists that state and keeps browser work available. This is not an observation blackout; explicit Pause remains available when the page must stay unobservable. -For recognizable consequential actions, **Commit** stages instead of acting. It is designed for sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. Approval happens in a human popup and is bound to the requesting agent's one-use token, the task, the tab, the target fingerprint, and the current page state. The final execution checks those bindings again. That reduces recognizable risk, but it cannot guarantee that a page has not attached a hidden effect to an innocent-looking control. +Recognizable consequential actions execute directly by default. Turning YOLO mode off enables **Commit** review for sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. In review mode, approval happens in a human popup and is bound to the requesting agent's one-use token, the task, the tab, the target fingerprint, and the current page state. The final execution checks those bindings again. That reduces recognizable risk, but it cannot guarantee that a page has not attached a hidden effect to an innocent-looking control. The Standard MCP interface has nine tools, including explicit provenance-aware task finalization, and excludes raw cookies, browser storage, arbitrary scripts, raw CDP, raw network access, and credential-value responses. A separate Developer-only tool requires a persistent explicit opt-in. The extension also declares the `` host permission for the defined `chrome.scripting` text, HTML, selector, wait, and scroll paths in task-owned pages. It is needed across the sites a person directs an agent to use, not to expose raw browser-data or browser-control APIs in Standard mode. @@ -52,4 +52,4 @@ AgentTab `v2.0.0-rc.1` is unreleased. This text is draft-only and intentionally ## Short-comment fallback -AgentTab is an unreleased local browser runtime for AI agents. Its default is task-owned tabs, not broad browser control: Your Turn blackouts observation for human-only steps, Commit stages recognizable consequential actions for human popup approval, and MCP connects locally through per-user operating-system-native IPC. It has no cloud relay or telemetry. The boundary is coordination, not profile isolation, so local agents and page content still need to be trusted. +AgentTab is an unreleased local browser runtime for AI agents. Its default is task-owned tabs, nonblocking human handoff, direct execution for recognized consequential actions, and origin-bound 1Password filling through the local host. Users can explicitly Pause agents or turn off YOLO mode for staged Commit review. MCP connects locally through per-user operating-system-native IPC. AgentTab has no cloud relay or telemetry. The boundary is coordination, not profile isolation, so local agents and page content still need to be trusted. diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md index bfdbbc1..ba80bbd 100644 --- a/docs/launch/show-hn.md +++ b/docs/launch/show-hn.md @@ -20,11 +20,11 @@ AgentTab's promise is: **Give an agent a tab, not the keys to your browser.** It The runtime is one minimal MV3 extension plus a local Rust host. The extension uses Chrome Native Messaging, and local clients such as MCP adapters use per-user operating-system-native IPC: a user-owned Unix socket on macOS and Linux or a current-user named pipe on Windows. There is no cloud relay, hosted browser session, telemetry service, or routine network control plane. -Standard MCP access is deliberately small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The credential tool is disabled until managed policy enables the local 1Password broker and never returns a credential value. There is one optional Developer-only tool, `browser_developer`, behind a persistent explicit opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. +Standard MCP access is deliberately small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, `browser_commit`, `browser_credentials`, and `browser_finish`. The local 1Password broker is available by default unless owner-only policy disables it, and the credential tool never returns a credential value. There is one optional Developer-only tool, `browser_developer`, behind a persistent explicit opt-in. Standard mode does not expose raw cookie, storage, arbitrary script, CDP, or network APIs. AgentTab declares the `` host permission so its defined `chrome.scripting` text, HTML, selector, wait, and scroll paths can run in task-owned pages the user selects. That broad site reach does not give Standard mode raw cookie, storage, arbitrary JavaScript, CDP, or network APIs. -A disabled-by-default local 1Password broker can fill one of at most three origin-matching Login items without returning a value to the agent. **Your Turn** handles passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and broker results that need the user. While a handoff is active, AgentTab applies an observation blackout for every task, so normal capture and observation calls return `needs_user`; it does not capture the person's keystrokes. **Commit** is a best-effort barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. The runtime stages a recognizable action, shows a human popup preview, and allows a one-use token from the requesting agent to execute only after approval. It revalidates the target immediately before execution and invalidates the staged action if the page or target changes. +The local 1Password broker can fill one of at most three origin-matching Login items without returning a value to the agent. **Your Turn** handles passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and broker results that need the user. Handoff persists the completion condition and focuses the task tab while browser work remains available; users can explicitly Pause first when the page must remain unobservable. Recognizable consequential actions execute directly by default. Turning YOLO mode off enables the best-effort **Commit** review barrier, which stages a preview and requires human approval plus the requesting agent's one-use token before execution. This is still real-profile automation. Task ownership coordinates execution; it does not isolate cookies, accounts, or identity. A page can contain prompt injection, a control can hide an effect behind an innocent label, and Commit cannot prove that every external effect is recognizable. The runtime is local-only and has no telemetry, but users still need to trust the local agents and software they connect to their signed-in profile. -I am preparing the v2 design for controlled review, not public use. I would eventually welcome feedback on the task-workspace boundary, the Your Turn blackout, the best-effort Commit model, and whether the nine-tool MCP surface is the right default. There is no stable install path or launch link in this draft. +I am preparing the v2 design for controlled review, not public use. I would eventually welcome feedback on the task-workspace boundary, nonblocking Your Turn handoff, optional Commit review, and whether the nine-tool MCP surface is the right default. There is no stable install path or launch link in this draft. diff --git a/docs/mcp.md b/docs/mcp.md index d006e82..b0d3450 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -67,9 +67,9 @@ For MCP, the capability store namespace is `mcp`; OMP uses `omp`; Pi uses `pi`. | `browser_act` | Requires `tab_id`, `expected_page_revision`, and one to 64 typed actions. Actions are click, type, fill, select, scroll, drag, navigate, history movement, reload, close, dialog decision, and staged file upload. No coordinate action exists in Standard mode. A `tab_only` route accepts explicit navigation, history movement, reload, and close only. Managed origin constraints disable history movement because Chrome does not expose its destination for authorization before navigation; use explicit navigation to an allowed URL instead. | | `browser_wait` | Requires `tab_id` and one load, URL, text, selector, network-idle, or download condition. `timeout_ms` is at most 120 seconds. A `tab_only` route accepts load and URL conditions only; network-idle and download attribution require the tab-scoped debugger connection available on the `full` route. | | `browser_tabs` | Takes an empty object and lists only the current task's tabs, including each tab's `automation_route`. | -| `browser_handoff` | Requires a task tab, expected page revision, prompt, completion condition, and optional timeout. Completion can be navigation, manual completion, a URL, or a selector. It remains available on a `tab_only` route because AgentTab blocks agent observation while the human controls the tab, but selector completion requires the `full` route. | +| `browser_handoff` | Requires a task tab, expected page revision, prompt, completion condition, and optional timeout. Completion can be navigation, manual completion, a URL, or a selector. It remains available on a `tab_only` route because handoff itself needs no page inspection, but selector completion requires the `full` route. Browser work remains available while handoff is active. | | `browser_commit` | Requires the staged token returned by a prior `commit_required` action and executes that one staged operation. On a `tab_only` route, only a staged close can execute; page-dependent staged actions require the `full` route. | -| `browser_credentials` | `prepare` requires a task tab and expected page revision, then returns an opaque short-lived token only when managed policy enables 1Password and one through three Login items match the host-derived current origin. `fill` consumes that token and selected username, password, or one-time-code field refs without returning any value. `next` advances to another bounded candidate. It never submits the form. | +| `browser_credentials` | `prepare` requires a task tab and expected page revision, then returns an opaque short-lived token when the default-enabled 1Password broker is available and one through three Login items match the host-derived current origin. Owner-only policy can disable it. `fill` consumes that token and selected username, password, or one-time-code field refs without returning any value. `next` advances to another bounded candidate. It never submits the form. | | `browser_finish` | Accepts `disposition: "auto" | "close" | "keep"` and optional task-owned `keep_tab_ids`. Automatic mode follows the popup cleanup policy: close task-created tabs while retaining adopted tabs, ask for confirmation, or retain all tabs. Successful finalization ungroups retained tabs, releases ownership, closes the Core connection, and returns closed and retained tab IDs. Active handoff, staged Commit review, and other in-flight work defer finalization without destroying resumability. | Every existing-page mutation carries its expected page revision. If navigation or document replacement makes that revision stale, AgentTab rejects the operation rather than selecting a new target. @@ -96,21 +96,21 @@ The Core response has `protocol: "agenttab.rpc"`, `version: 1`, matching `reques Mutation methods carry a UUIDv7 idempotency key in Core RPC. MCP, OMP, and Pi bind one key to each harness invocation ID so a retry of that invocation retains its reconciliation identity. Reusing a completed key for identical work returns the durable response; reusing it with different input is a conflict. A mutation found only as started after recovery returns `unknown` and is not replayed. -`browser_handoff` returns as soon as the durable handoff and global browser blackout are active. The stdio MCP reader also dispatches requests concurrently while its writer serializes complete JSON-RPC lines, so handoff completion never holds the initiating agent call or blocks `ping`, discovery, or independent non-browser work. +`browser_handoff` returns as soon as the durable handoff and completion condition are active. It does not pause browser work. The stdio MCP reader dispatches requests concurrently while its writer serializes complete JSON-RPC lines, so handoff completion never holds the initiating agent call or blocks other tool calls. Raw TypeScript and Python SDK clients raise `AgentTabTransportError` for an ambiguous timeout, connection close, or transport failure. The error carries the method and, for mutations, the exact generated or caller-supplied idempotency key. A caller may reconnect and explicitly retry the same method and parameters with that key; the SDK never replays the request automatically. MCP and OMP adapters likewise return the failed invocation, discard a cached client only when its transport is closed, and reconnect on the next invocation. ### Credentials and Your Turn handoff -When managed policy enables 1Password, call `browser_credentials` on an ordinary sign-in page before requesting manual password entry. `prepare` derives the current origin from host-owned tab state. `fill` accepts only accessibility refs and returns filled-field booleans; credential material never crosses Core RPC. Submit separately through `browser_act`, inspect the result, and use `next` only after the site rejects the current candidate. +Call `browser_credentials` on an ordinary sign-in page before requesting manual password entry. The broker is available by default unless owner-only policy disables it. `prepare` derives the current origin from host-owned tab state. `fill` accepts only accessibility refs and returns filled-field booleans; credential material never crosses Core RPC. Submit separately through `browser_act`, inspect the result, and use `next` only after the site rejects the current candidate. -Call `browser_handoff` when credential preparation returns `needs_user`, the bounded candidates fail, or the site requires a passkey, security key, CAPTCHA, payment secret, account recovery, or unsupported verification. AgentTab activates a global browser blackout, focuses the declared tab, opens its user-facing handoff state, and returns a completed start result immediately. Browser automation resumes only after the declared navigation, URL, selector, or manual completion condition is satisfied and the handoff is cleared. +Call `browser_handoff` when credential preparation returns `needs_user`, the bounded candidates fail, or the site requires a passkey, security key, CAPTCHA, payment secret, account recovery, or unsupported verification. AgentTab focuses the declared tab, opens its user-facing handoff state, and returns a completed start result immediately. Browser observations and mutations remain available while the handoff is active. -The agent must not attempt snapshots, page reads, or browser mutations during a handoff. It should report the prompt to the user, continue independent non-browser work, and resume browser work after explicit user completion or a later status check confirms the handoff cleared. +The handoff marker is not a confidentiality boundary. An agent that observes the handoff tab may capture user-entered page state. Use `browser_credentials` for ordinary sign-in fields, or explicitly pause agents before handoff when page state must remain unobservable. The declared navigation, URL, selector, or manual completion condition clears the handoff state. ### Staged Commit -`browser_act` is the Standard mutation choke point. For recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls, AgentTab can stop before the side effect and return: +`browser_act` is the Standard mutation choke point. Recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls execute directly by default. With YOLO mode turned off, AgentTab stops before the side effect and returns: ```json { @@ -128,7 +128,7 @@ The agent must not attempt snapshots, page reads, or browser mutations during a The token is bound to the task, tab, effect, page revision, and element fingerprint. It expires after at most five minutes and is one-use. The extension popup must first record a human approval for that exact stage. Approval does not execute the action and does not expose the native token. The agent must then call `browser_commit`, which takes only the staged token, revalidates the target, and executes only an approved stage. A changed page, ownership change, expiry, unapproved stage, or repeated token makes the commit fail. -YOLO mode is a disabled-by-default local popup setting. When enabled, recognized consequential controls execute inside the original `browser_act` call and return its ordinary completed result instead of `commit_required`. Enabling the setting discards pending staged actions. It bypasses only Commit review: task ownership, origin policy, expected page revisions, restricted-origin routing, handoff blackout, credential isolation, and action validation still apply. +YOLO mode is enabled by default. Recognized consequential controls execute inside the original `browser_act` call and return its ordinary completed result instead of `commit_required`. Turning the setting on discards pending staged actions. It bypasses only Commit review: task ownership, origin policy, expected page revisions, restricted-origin routing, credential isolation, and action validation still apply. With YOLO mode off, a `browser_act` batch is sequential and non-atomic. The extension stops before the first recognizable staged action and does not execute later actions implicitly. The current host response preserves the staged token and binding metadata, but does not publicly return the extension's completed-prefix list or staged index. Clients must not infer how many preceding actions ran from a `commit_required` response; inspect the page before deciding the next action. This is a source limitation, not a guarantee of an atomic batch. With YOLO mode on, the recognized action executes inline and the batch continues until completion or the next failure. diff --git a/docs/multi-agent.md b/docs/multi-agent.md index 9a27dc8..5d8041a 100644 --- a/docs/multi-agent.md +++ b/docs/multi-agent.md @@ -42,14 +42,14 @@ Pause is a barrier, not an optimistic UI toggle. It stops new admissions, lets a A host that has not completed its native handshake and reconciliation remains unavailable for browser work. The connection status can report its lifecycle, but callers must retry only after it becomes ready or the user resumes it. -## Global Your Turn blackout +## Your Turn coordination -Only one handoff can be active. Starting `browser_handoff` pauses the scheduler, records the marker durably, and focuses the human's task tab. While it is active, page observations, captures, and browser work are denied across every task and connection. The host independently enforces this blackout and restores it after restart from SQLite state. +Only one handoff can be active. Starting `browser_handoff` records the marker and completion condition durably, focuses the human's task tab, and returns without pausing the scheduler. Browser requests from the same or other tasks remain eligible. The host restores the active marker after restart from SQLite state. -Automation resumes only after the declared completion condition or explicit completion, capture scrubbing, an acknowledged handoff-clear event, and a non-paused state. Handoff is the sole normal AgentTab focus transition for human input. +The handoff clears after the declared completion condition or explicit completion and an acknowledged handoff-clear event. It is the sole normal AgentTab focus transition for human input, but it is not an observation blackout. Use `browser_credentials` for ordinary sign-in fields or explicitly pause agents when the handoff page must remain unobservable. ## Consequential work across agents -Each recognizable consequential Standard action stages its own Commit. A staged token is bound to one task and tab, expires after five minutes, revalidates the page revision and target fingerprint, and executes once. A batch stops at its first staged action; another agent cannot use that stage to run later batch items. +With default YOLO mode, recognizable consequential Standard actions execute inline after validation. Turning YOLO mode off stages each action as a Commit bound to one task and tab. The token expires after five minutes, revalidates the page revision and target fingerprint, and executes once. In review mode a batch stops at its first staged action; another agent cannot use that stage to run later batch items. -There are no agent-facing global lease tools. Coordinating intent is still the responsibility of the agents and the user. Use distinct tasks for independent work, observe task counts in the extension, and have the human review staged effects before Commit. +There are no agent-facing global lease tools. Coordinating intent is still the responsibility of the agents and the user. Use distinct tasks for independent work and observe task counts in the extension. Turn YOLO mode off when human review of staged effects is desired. diff --git a/docs/roadmap.md b/docs/roadmap.md index a57b548..5e56578 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -7,8 +7,8 @@ AgentTab v2 is currently `2.0.0-rc.1`, a local prerelease. This page records lau | Gate | Current source state | Required evidence before promotion | | --- | --- | --- | | Product identity | Source identifies AgentTab, `dev.agenttab.host`, `agenttab`, and AgentTab Core RPC v1. | Exact-head identity and forbidden-surface gates. | -| Standard boundary | Source schemas define nine Standard browser tools, including explicit provenance-aware task finalization and a disabled-by-default host-managed credential broker; Developer mode adds `browser_developer`. | Schema, adapter discovery, cleanup provenance, fake-provider credential isolation, and real-extension checks. | -| Task safety | Source implements server-bound tasks, visible groups, revisions, Pause, handoff blackout, and staged Commit records. | Controlled browser fixtures covering restart, revocation, stale revisions, and one-use Commit. | +| Standard boundary | Source schemas define nine Standard browser tools, including explicit provenance-aware task finalization and a default-enabled host-managed credential broker with an owner-only opt-out; Developer mode adds `browser_developer`. | Schema, adapter discovery, cleanup provenance, fake-provider credential isolation, and real-extension checks. | +| Task safety | Source implements server-bound tasks, visible groups, revisions, explicit Pause, durable nonblocking handoff, default YOLO execution, and optional staged Commit records. | Controlled browser fixtures covering restart, revocation, stale revisions, handoff concurrency, and one-use Commit. | | Rust runtime | Source contains the Rust host, native bridge, same-user IPC, SQLite journal, and local audit. | Exact-head Rust, IPC, Linux, macOS, and Windows gates. | | Installer | Source contains a transactional Node-compatible installer and advanced loopback proxy. | Clean user-home and clean-machine install proof using the packaged signed bytes. | | Extension package | Source contains canonical extension build and store-package tooling. | Inspect and install the exact packaged ZIP in a clean profile. | @@ -41,8 +41,8 @@ No stable installation call to action belongs on a public surface until the rele ## Non-goals and boundaries - Task groups are a coordination boundary, not profile isolation. -- Commit reduces recognizable consequential actions but cannot prove webpage semantics. -- Handoff protects AgentTab observation during human input, not a compromised device or browser profile. +- Commit review is optional and reduces recognizable consequential risk but cannot prove webpage semantics. +- Handoff is coordination, not a confidentiality boundary. Use brokered credentials or explicit Pause when user-entered page state must remain unobservable. - A local source test, RC artifact, draft item, or planned domain is not a public availability claim. Legacy v1.0.1 remains historical migration context until a final stable v2 launch. This roadmap intentionally contains no v1 operational path. diff --git a/docs/rust-host.md b/docs/rust-host.md index a5beae5..1362c74 100644 --- a/docs/rust-host.md +++ b/docs/rust-host.md @@ -7,7 +7,7 @@ | Component | Responsibility | | --- | --- | | Core RPC | Validates versioned `agenttab.rpc` v1 requests, attaches connection identity and task scope server-side, and returns a structured outcome. | -| Runtime | Applies lifecycle, task scope, handoff blackout, origin and upload guardrails, idempotency, audit, and request locks. | +| Runtime | Applies lifecycle, task scope, durable handoff state, origin and upload guardrails, idempotency, audit, and request locks. | | Journal | Maintains durable task, ownership, revision-floor, handoff, staged Commit, event receipt, and idempotency state in SQLite. | | Native transport | Exchanges versioned `agenttab.native` v1 messages with the Chrome extension over Native Messaging. | | Local IPC server | Accepts authenticated same-user Core clients over a Unix socket or Windows named pipe. | @@ -35,7 +35,7 @@ Standard mode has no TCP listener or bearer token. The advanced `agenttab proxy The implemented lifecycle states are `starting`, `reconciling`, `ready`, `paused`, and terminal. Browser work is admitted only in `ready`. In `starting` or `reconciling` it returns `runtime_not_ready`; in `paused` it returns `automation_paused`; in terminal state it returns a protocol-recovery error. -Pause admission is also enforced by the extension scheduler. It closes new admission, waits for in-flight work, persists pause state, and rejects queued work before dispatch. Handoff is a global write barrier and causes a host-side blackout check both before and after request admission. +Pause admission is also enforced by the extension scheduler. It closes new admission, waits for in-flight work, persists pause state, and rejects queued work before dispatch. Handoff records durable coordination state but does not alter request admission or locking. ## Durable state diff --git a/docs/security.md b/docs/security.md index 5ddfffe..217d23d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -4,7 +4,7 @@ AgentTab is a **local browser runtime for AI agents**. It gives an agent task-ow ## Trust boundary and local attacker model -A task may use the profile already signed in to websites. An agent can therefore act with the permissions of the currently signed-in user on a task-owned tab. AgentTab does not expose raw cookies, storage, saved-password lists, or credential values through Standard mode. When managed policy explicitly enables the 1Password broker, the host may obtain one origin-matching Login item and send selected values directly to the owned page through the extension. That narrow path does not make the signed-in session untrusted or isolated. +A task may use the profile already signed in to websites. An agent can therefore act with the permissions of the currently signed-in user on a task-owned tab. AgentTab does not expose raw cookies, storage, saved-password lists, or credential values through Standard mode. The 1Password broker is available by default unless owner-only policy disables it; the host may obtain one origin-matching Login item and send selected values directly to the owned page through the extension. That narrow path does not make the signed-in session untrusted or isolated. The host authenticates its local IPC peers as the current OS user. This prevents a different local user from connecting through the user socket or named pipe. It does **not** distinguish benign and malicious processes running as that same user. A local attacker that can execute as the account, read AgentTab state, control the browser, or modify the extension/host installation is outside this protection. Use an OS account and a browser profile appropriate for the work, protect the account, and treat local malware as a full compromise. @@ -26,13 +26,13 @@ Standard mode exposes exactly these nine MCP tools: Developer mode additionally exposes `browser_developer`. It is disabled by default in the host's local managed policy and is visibly marked on a task. Developer mode is intentionally a broader trust decision. -Standard mode has no raw CDP method, arbitrary JavaScript API, raw cookie or storage API, coordinate action, generic browser-global mutation API, or credential-value API. The narrow exceptions are `browser_open` with `placement: "new_window"`, which creates one unfocused normal window for the first tab of an empty task, and policy-gated `browser_credentials`, which passes an origin-matched login field from 1Password directly to a selected field ref without returning the value. Internally, the extension uses Chrome debugging APIs for task-scoped accessibility snapshots and precise ref-based actions. Those implementation details are not a Standard-mode escape hatch. +Standard mode has no raw CDP method, arbitrary JavaScript API, raw cookie or storage API, coordinate action, generic browser-global mutation API, or credential-value API. The narrow exceptions are `browser_open` with `placement: "new_window"`, which creates one unfocused normal window for the first tab of an empty task, and `browser_credentials`, which passes an origin-matched login field from 1Password directly to a selected field ref without returning the value. Internally, the extension uses Chrome debugging APIs for task-scoped accessibility snapshots and precise ref-based actions. Those implementation details are not a Standard-mode escape hatch. Passkeys, security keys, CAPTCHA, payment secrets, account recovery, and unsupported verification require `browser_handoff`. The human completes the step in Chrome; no secret is placed in an AgentTab request. ## 1Password credential broker -The broker is disabled unless `policy.json` sets `one_password.enabled` to `true`. The host derives the current origin from its verified task-tab inventory, queries only 1Password Login items, and accepts an item only when a URL on that item matches the current host or a recognized authentication subdomain of the same registrable domain. Item titles and unrelated fields do not authorize a match. +The broker is available unless `policy.json` sets `one_password.enabled` to `false`. The host derives the current origin from its verified task-tab inventory, queries only 1Password Login items, and accepts an item only when a URL on that item matches the current host or a recognized authentication subdomain of the same registrable domain. Item titles and unrelated fields do not authorize a match. `one_password.max_candidates` and `one_password.max_attempts` are each restricted to `1..3`. More than the configured candidate limit returns `needs_user` without selecting or exposing an item. A prepared attempt receives a random, short-lived token bound to the task, tab, origin, candidate position, and attempt count. Tokens are one-use for fill or advance, expire after five minutes, and are invalidated by navigation. @@ -62,17 +62,17 @@ Actions that operate on an existing page carry an expected page revision. Naviga Page text, HTML, labels, screenshots, accessibility names, and downloads are untrusted data. A page can attempt to persuade an agent to reveal data, broaden access, ignore policy, or act outside the user's purpose. AgentTab cannot decide whether instructions embedded in page content are trustworthy. Agents and their operators must treat page content as data, constrain their task, inspect consequential previews, and stop when the page asks for unrelated access or secrets. -Before a Standard mutation, the extension prepares the target, classifies recognizable effects, revalidates it, and then executes it. Recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls instead stage a five-minute, one-use Commit record. The record binds the task, tab, page revision, event, target fingerprint, effect, and preview. The extension popup sends only an opaque review handle. A successful popup approval durably marks the stage approved but does not consume its token or dispatch the browser action. Only a later agent `browser_commit` can consume that approved stage; execution rechecks ownership, revision, expiry, and fingerprint. +Before a Standard mutation, the extension prepares the target, classifies recognizable effects, revalidates it, and then executes it. Recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls execute directly by default. When YOLO mode is turned off, the same controls instead stage a five-minute, one-use Commit record. The record binds the task, tab, page revision, event, target fingerprint, effect, and preview. The extension popup sends only an opaque review handle. A successful popup approval durably marks the stage approved but does not consume its token or dispatch the browser action. Only a later agent `browser_commit` can consume that approved stage; execution rechecks ownership, revision, expiry, and fingerprint. -Commit is a best-effort semantic barrier, not proof that an action is harmless. It requires two distinct events, human approval in the popup and the agent's later Commit request. A page can hide an external effect behind an innocent label, alter meaning through script, or use an effect AgentTab does not recognize. Harmless-looking controls may execute without review. Batches are sequential and non-atomic: work stops before a staged action and never runs later actions implicitly. +Commit is an optional, best-effort semantic barrier, not proof that an action is harmless. In review mode it requires two distinct events: human approval in the popup and the agent's later Commit request. A page can hide an external effect behind an innocent label, alter meaning through script, or use an effect AgentTab does not recognize. Harmless-looking controls may execute without review. Batches are sequential and non-atomic: review mode stops before a staged action and never runs later actions implicitly. -YOLO mode is an explicit, disabled-by-default opt-out from Commit review. When enabled, recognized consequential controls execute during the original `browser_act` call, and pending staged actions are discarded when the setting changes on. The mode does not bypass task ownership, origin policy, expected revisions, restricted-origin routing, handoff blackout, credential isolation, or action validation. +YOLO mode is enabled by default. It bypasses only Commit review. Task ownership, origin policy, expected revisions, restricted-origin routing, credential isolation, and action validation remain enforced. Turning YOLO mode on discards pending staged actions; turning it off restores the two-party Commit flow. -## Your Turn blackout +## Your Turn handoff -During a `browser_handoff`, AgentTab pauses browser work and applies a global blackout across tasks. Page observations and captures are denied while the human enters information. The extension persists the active handoff before focusing the tab; the host restores the blackout from durable state after restart. Completion requires the declared condition or explicit completion, capture scrubbing, and host acknowledgement before automation resumes. +`browser_handoff` persists the active handoff and completion condition before focusing the declared tab, then returns immediately. It does not pause the scheduler or block browser observations and mutations. Explicit **Pause agents** remains a separate owner control. -This reduces exposure during handoff. It cannot protect secrets from a compromised device, a malicious webpage, or browser extensions with their own access. +This permissive behavior keeps unrelated browser work moving, but handoff is not a confidentiality boundary: an agent that continues observing the handoff tab may capture user-entered page state. Prefer `browser_credentials` for ordinary username, password, and one-time-code fields because brokered values never enter Core RPC, adapter responses, or audit output. Use handoff for interactions the broker cannot complete, and explicitly pause agents first when the page state itself must remain unobservable. ## Upload guardrails diff --git a/docs/setup.md b/docs/setup.md index ed45074..9f62e39 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -8,8 +8,8 @@ This guide distinguishes the contributor source path from the future signed RC a - Chrome must be version 127 or later for the current extension manifest. - AgentTab runs in the existing signed-in Chrome profile. It is task-scoped browser control, not a separate profile, cookie jar, or identity boundary. -- Keep page content untrusted. A managed, disabled-by-default 1Password broker may fill an origin-matching Login item without exposing its value to the agent. Use **Your Turn** for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and any credential result that requests the user. Review a staged **Commit** before performing it. -- A future installation needs an AgentTab extension and the `dev.agenttab.host` native host. Standard mode does not require a TCP listener, a bearer token, or a Python process. Credential filling additionally requires 1Password CLI with desktop-app integration and biometric unlock available to the current OS user. +- Keep page content untrusted. The built-in 1Password broker is available by default and may fill an origin-matching Login item without exposing its value to the agent. Use **Your Turn** for passkeys, security keys, CAPTCHA, payment secrets, account recovery, unsupported verification, and any credential result that requests the user. Recognized consequential actions execute directly unless YOLO mode is turned off in the popup. +- A future installation needs an AgentTab extension and the `dev.agenttab.host` native host. Standard mode does not require a TCP listener, a bearer token, or a Python process. Credential filling additionally requires the `op` command, 1Password desktop-app integration, and biometric unlock available to the current OS user. The product boundary and residual Commit risk are described in the [runtime ADR](adr/0001-agenttab-runtime.md) and [Security](security.md). @@ -86,16 +86,17 @@ Normal local adapter traffic uses one of these OS-native endpoints: On Unix, AgentTab requires its state and runtime directories to be current-user owned and mode `0700`; its socket and host lock are mode `0600`. The host authenticates local peers with OS credentials. On Windows, the named-pipe DACL is limited to the current user SID and `SYSTEM`. `AGENTTAB_SOCKET` and `AGENTTAB_PIPE_NAME` are adapter overrides for configured local endpoints, not normal setup switches. -## Optional 1Password credential broker +## 1Password credential broker -Credential filling is off by default. Enable it only in the host's owner-only -`~/.agenttab/policy.json`: +Credential filling is available by default. AgentTab resolves `op` from the +native host's process `PATH`, then checks standard installation locations for +the current platform. Owner-only `~/.agenttab/policy.json` can disable the +broker or constrain its executable, account, and limits: ```json { "one_password": { "enabled": true, - "executable": "/opt/homebrew/bin/op", "max_candidates": 3, "max_attempts": 3, "auth_timeout_ms": 45000 @@ -107,13 +108,15 @@ The limits must be integers from one through three. `auth_timeout_ms` must be between 5000 and 120000. An optional `account` selects one configured 1Password account without putting a secret in policy. Set `executable` to the absolute `op` path when a GUI-launched browser does not inherit the shell -`PATH`; relative paths are rejected. Policy changes apply when the native host -next starts, so reload the extension after editing this file. +`PATH`; relative paths are rejected. Set `enabled` to `false` to disable +credential access. Policy changes apply when the native host next starts, so +reload the extension after editing this file. Install and sign in to 1Password CLI separately, enable its desktop-app -integration, and keep biometric unlock available. AgentTab never stores a -1Password session token. If 1Password needs approval, its Touch ID or Apple -Watch prompt is the only expected human interaction. More than the configured +integration, and keep the platform's supported biometric or desktop approval +available. AgentTab never stores a 1Password session token. If 1Password needs +approval, its native 1Password prompt is the only expected human interaction. +More than the configured number of origin-matching Login items, unavailable CLI state, or an unsupported verification step returns control to the user instead of trying additional credentials. diff --git a/docs/verification.md b/docs/verification.md index 4cff257..74cc6fa 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -40,8 +40,8 @@ Use a disposable Chrome profile and a disposable test account. Reload the unpack 4. accessibility, text, HTML, and screenshot snapshots; stale revision/ref rejection; wait conditions; and debugger detach/restart; 5. ready, working, needs-you, resumed, and finished popup states; 6. Pause, restart while paused, reconciliation, and Resume; -7. global blackout during `browser_handoff`, including host and extension restart during the handoff; -8. recognizable consequential controls staged without side effect, a changed target rejected, one unchanged Commit execution, and harmless controls executed without review. +7. nonblocking `browser_handoff`, including concurrent snapshots and mutations plus host and extension restart during the handoff; +8. default inline execution for recognizable consequential controls, then YOLO opt-out with staging, changed-target rejection, and one unchanged Commit execution. Never Commit a real send, purchase, delete, permission grant, or upload against a live account merely to prove the barrier. Use controlled fixtures and stop at the staged preview for live authenticated checks. diff --git a/host-rs/crates/agenttab-host/src/credentials.rs b/host-rs/crates/agenttab-host/src/credentials.rs index 56a8e21..daf5290 100644 --- a/host-rs/crates/agenttab-host/src/credentials.rs +++ b/host-rs/crates/agenttab-host/src/credentials.rs @@ -3,6 +3,7 @@ use parking_lot::Mutex; use rand::RngCore; use serde_json::Value; use std::collections::HashMap; +use std::ffi::OsStr; use std::io::Read; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -309,10 +310,82 @@ struct OnePasswordProvider { timeout: Duration, } +fn default_one_password_executable() -> PathBuf { + let executable_name = if cfg!(windows) { "op.exe" } else { "op" }; + let search_path = std::env::var_os("PATH"); + let fallbacks = standard_one_password_executable_paths(); + resolve_one_password_executable(executable_name, search_path.as_deref(), &fallbacks) +} + +fn resolve_one_password_executable( + executable_name: &str, + search_path: Option<&OsStr>, + fallbacks: &[PathBuf], +) -> PathBuf { + if let Some(search_path) = search_path { + for directory in std::env::split_paths(search_path) { + let candidate = directory.join(executable_name); + if candidate.is_file() { + return candidate; + } + } + } + fallbacks + .iter() + .find(|candidate| candidate.is_file()) + .cloned() + .unwrap_or_else(|| PathBuf::from(executable_name)) +} + +fn standard_one_password_executable_paths() -> Vec { + #[cfg(target_os = "macos")] + { + return ["/opt/homebrew/bin/op", "/usr/local/bin/op", "/usr/bin/op"] + .into_iter() + .map(PathBuf::from) + .collect(); + } + + #[cfg(target_os = "linux")] + { + return ["/usr/local/bin/op", "/usr/bin/op", "/snap/bin/op"] + .into_iter() + .map(PathBuf::from) + .collect(); + } + + #[cfg(windows)] + { + let mut paths = Vec::new(); + if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") { + paths.push( + PathBuf::from(local_app_data) + .join("Microsoft") + .join("WinGet") + .join("Links") + .join("op.exe"), + ); + } + if let Some(program_files) = std::env::var_os("PROGRAMFILES") { + paths.push( + PathBuf::from(program_files) + .join("1Password CLI") + .join("op.exe"), + ); + } + return paths; + } + + #[allow(unreachable_code)] + Vec::new() +} + impl OnePasswordProvider { fn new(policy: OnePasswordPolicy) -> Self { Self { - executable: policy.executable.unwrap_or_else(|| PathBuf::from("op")), + executable: policy + .executable + .unwrap_or_else(default_one_password_executable), account: policy.account, timeout: Duration::from_millis(policy.auth_timeout_ms), } @@ -649,6 +722,35 @@ mod tests { assert!(provider.candidates("example.com").unwrap().is_empty()); } + #[test] + fn default_executable_discovery_prefers_the_process_path_then_standard_fallbacks() { + let temp = tempfile::tempdir().unwrap(); + let path_candidate = temp + .path() + .join(if cfg!(windows) { "op.exe" } else { "op" }); + std::fs::write(&path_candidate, b"fixture").unwrap(); + let search_path = std::env::join_paths([temp.path()]).unwrap(); + let fallback = temp.path().join("fallback-op"); + std::fs::write(&fallback, b"fixture").unwrap(); + + assert_eq!( + resolve_one_password_executable( + if cfg!(windows) { "op.exe" } else { "op" }, + Some(&search_path), + std::slice::from_ref(&fallback), + ), + path_candidate + ); + assert_eq!( + resolve_one_password_executable("missing-op", None, std::slice::from_ref(&fallback)), + fallback + ); + assert_eq!( + resolve_one_password_executable("missing-op", None, &[]), + PathBuf::from("missing-op") + ); + } + #[test] fn refuses_more_than_three_matching_items_without_exposing_identity() { let provider = Arc::new(FakeProvider { diff --git a/host-rs/crates/agenttab-host/src/guardrails.rs b/host-rs/crates/agenttab-host/src/guardrails.rs index fef1b9f..1bf7a57 100644 --- a/host-rs/crates/agenttab-host/src/guardrails.rs +++ b/host-rs/crates/agenttab-host/src/guardrails.rs @@ -45,7 +45,7 @@ pub struct OnePasswordPolicy { impl Default for OnePasswordPolicy { fn default() -> Self { Self { - enabled: false, + enabled: true, account: None, executable: None, max_candidates: 3, @@ -636,6 +636,22 @@ mod tests { ); } + #[test] + fn one_password_is_available_by_default_but_can_be_disabled() { + let defaults = Guardrails::defaults(); + assert!(defaults.one_password_policy().enabled); + + let disabled = Guardrails::from_policy(Policy { + one_password: OnePasswordPolicy { + enabled: false, + ..OnePasswordPolicy::default() + }, + ..Policy::default() + }) + .unwrap(); + assert!(!disabled.one_password_policy().enabled); + } + #[test] fn origins_and_redaction_are_enforced() { let guardrails = Guardrails::from_policy(Policy { diff --git a/host-rs/crates/agenttab-host/src/handoff.rs b/host-rs/crates/agenttab-host/src/handoff.rs index e515211..7e464d2 100644 --- a/host-rs/crates/agenttab-host/src/handoff.rs +++ b/host-rs/crates/agenttab-host/src/handoff.rs @@ -26,18 +26,6 @@ impl HandoffState { ) }) } - - pub fn observation_gate(&self) -> Result<(), RpcError> { - if self.is_active() { - Err(RpcError::new( - "handoff_blackout", - "Browser observations are disabled during credential handoff", - ) - .with_recovery("Wait for the human to finish or cancel the active handoff.")) - } else { - Ok(()) - } - } } #[cfg(test)] @@ -45,12 +33,11 @@ mod tests { use super::*; #[test] - fn blackout_remains_until_native_completion_is_reconciled() { + fn active_state_rejects_only_another_handoff() { let state = HandoffState::default(); state.begin().unwrap(); - assert!(state.observation_gate().is_err()); assert!(state.begin().is_err()); state.restore(false); - assert!(state.observation_gate().is_ok()); + assert!(!state.is_active()); } } diff --git a/host-rs/crates/agenttab-host/src/runtime.rs b/host-rs/crates/agenttab-host/src/runtime.rs index 0899471..9cf9532 100644 --- a/host-rs/crates/agenttab-host/src/runtime.rs +++ b/host-rs/crates/agenttab-host/src/runtime.rs @@ -170,7 +170,6 @@ pub struct Runtime { handoff: Arc, credentials: Arc, task_locks: Mutex>>>, - global_gate: RwLock<()>, tab_urls: Arc>>, upload_staging_dir: PathBuf, } @@ -220,7 +219,6 @@ impl Runtime { credentials, handoff, task_locks: Mutex::new(HashMap::new()), - global_gate: RwLock::new(()), tab_urls, upload_staging_dir: paths.upload_staging_dir.clone(), }); @@ -799,7 +797,6 @@ impl Runtime { .lifecycle .gate(request.method) .err() - .or_else(|| self.handoff_blackout_error()) .or_else(|| self.guardrails.authorize(request.method, ¶ms).err()); if let Some(error) = early_error { let response = @@ -853,11 +850,6 @@ impl Runtime { true, ); } - let (_global_read, _global_write) = if request.method == RpcMethod::BrowserHandoff { - (None, Some(self.global_gate.write())) - } else { - (Some(self.global_gate.read()), None) - }; let lock_key = request_lock_key(task_id, request.method, ¶ms_value); let task_lock = { let mut locks = self.task_locks.lock(); @@ -903,21 +895,6 @@ impl Runtime { ); } }; - if let Some(error) = self.handoff_blackout_error() { - let response = - RpcResponse::failure(request.request_id.clone(), Outcome::NotStarted, error); - return self.audited_value( - connection, - Some(task_id), - &request, - ¶ms_value, - response, - started_at_ms, - started, - false, - true, - ); - } if let Err(error) = self.validate_task_scope(task_id, ¶ms) { let response = RpcResponse::failure(request.request_id.clone(), Outcome::NotStarted, error); @@ -1145,16 +1122,6 @@ impl Runtime { ) } - fn handoff_blackout_error(&self) -> Option { - self.handoff.is_active().then(|| { - RpcError::new( - "handoff_blackout", - "Automation is disabled while credential handoff is active", - ) - .with_recovery("Wait for the human to finish or cancel the active handoff.") - }) - } - fn dispatch( &self, connection_id: Uuid, @@ -1880,6 +1847,7 @@ pub(crate) fn request_lock_scope(method: RpcMethod, params: &Value) -> RequestLo RpcMethod::BrowserSnapshot | RpcMethod::BrowserAct | RpcMethod::BrowserWait + | RpcMethod::BrowserHandoff | RpcMethod::BrowserCredentials ) { if let Some(tab_id) = params.get("tab_id").and_then(Value::as_u64) { @@ -2185,19 +2153,31 @@ mod tests { } } #[derive(Debug)] - struct TimeoutNative; + struct HandoffTimeoutNative; - impl NativeTransport for TimeoutNative { + impl NativeTransport for HandoffTimeoutNative { fn dispatch( &self, _connection_id: Uuid, _task_id: Uuid, - _method: &str, + method: &str, _params: Value, _origin_policy: Option, _timeout: Duration, ) -> Result { - Err(NativeError::Timeout) + if method == "browser_handoff" { + return Err(NativeError::Timeout); + } + Ok(NativeResponse { + protocol: agenttab_protocol::NATIVE_PROTOCOL.into(), + version: PROTOCOL_VERSION, + kind: NativeResponseKind::Response, + request_id: Uuid::new_v4(), + outcome: Outcome::Completed, + result: Some(json!({"ok": true})), + error: None, + staged: None, + }) } } @@ -2971,8 +2951,8 @@ mod tests { } #[test] - fn handoff_timeout_keeps_global_blackout_active() { - let (_temp, runtime, connection) = connected_runtime(Arc::new(TimeoutNative)); + fn timed_out_handoff_state_does_not_block_browser_work() { + let (_temp, runtime, connection) = connected_runtime(Arc::new(HandoffTimeoutNative)); own_tab(&runtime, &connection, 7); let response = runtime.handle( &connection, @@ -2994,7 +2974,7 @@ mod tests { assert_eq!(response["error"]["code"], "extension_timeout"); assert!(runtime.handoff.is_active()); - let blocked = runtime.handle( + let snapshot = runtime.handle( &connection, json!({ "protocol": RPC_PROTOCOL, @@ -3004,11 +2984,11 @@ mod tests { "params": {"mode": "text", "tab_id": 3} }), ); - assert_eq!(blocked["error"]["code"], "handoff_blackout"); + assert_eq!(snapshot["outcome"], "completed"); runtime.handoff.restore(false); } #[test] - fn rejected_handoff_releases_global_blackout() { + fn rejected_handoff_releases_active_marker() { let (_temp, runtime, connection) = connected_runtime(Arc::new(RejectedHandoffNative)); own_tab(&runtime, &connection, 7); let response = runtime.handle( diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 5e0136a..9ebd4aa 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -49,7 +49,6 @@ const PRE_DISPATCH_ERRORS: Record = { staged_commit_expired: true, staged_commit_mismatch: true, handoff_in_progress: true, - handoff_blackout: true, origin_denied: true, origin_not_allowed: true, origin_unavailable: true, @@ -379,12 +378,6 @@ async function dispatch(command: NativeDispatchCommand): Promise await browser.abandonNativeStage(command.task_id, params.native_token, params.tab_id), ); } - if ((await readState()).handoff.active) { - throw Object.assign(new Error("Automation is disabled while credential handoff is active"), { - code: "handoff_blackout", - recovery: "Wait for the human to finish or cancel the active handoff.", - }); - } if ( command.method !== "browser_open" && command.method !== "browser_tabs" && @@ -405,17 +398,15 @@ async function dispatch(command: NativeDispatchCommand): Promise }); } if (command.method === "browser_handoff") { - if (!scheduler.isAccepting() || (await readState()).paused) { - throw scheduler.notStarted("AgentTab is paused"); - } - return completed( - command.request_id, - await handoff.begin( + const targetTabId = tabId(params); + const result = await scheduler.enqueueTab(command.task_id, targetTabId, () => + handoff.begin( command.task_id, params, - () => assertHandoffRoute(tabId(params), params, command.origin_policy), - ), + () => assertHandoffRoute(targetTabId, params, command.origin_policy), + ) ); + return completed(command.request_id, result); } if (command.method === "browser_commit") { const targetTabId = await browser.stagedTabId(command.task_id, params.native_token); @@ -544,7 +535,7 @@ async function initializeRuntime(): Promise { automationRevocationGeneration, state.automationCleanup.generation, ); - scheduler.setInitialPaused(state.paused || state.handoff.active); + scheduler.setInitialPaused(state.paused); if (await automationEnabled()) { scheduler.restorePermissions(); } else { diff --git a/packages/extension/src/handoff.ts b/packages/extension/src/handoff.ts index 5832621..5595e5a 100644 --- a/packages/extension/src/handoff.ts +++ b/packages/extension/src/handoff.ts @@ -61,10 +61,6 @@ export class HandoffController { } private async restoreNow(): Promise { - const state = await readState(); - if (!state.handoff.active) return; - const barrier = this.scheduler.pause(); - await barrier; const restored = await readState(); if (!restored.handoff.active) return; if (restored.handoff.pendingClearEventId) { @@ -122,7 +118,6 @@ export class HandoffController { timeoutMs: Number(timeoutMs), }; - const barrier = this.scheduler.pause(); let recorded = false; try { await mutateState((state) => { @@ -135,7 +130,6 @@ export class HandoffController { state.handoff = next; }); recorded = true; - await barrier; await this.ownership.assertOwned(taskId, numericTabId); await this.revisions.assertExpected(numericTabId, next.expectedRevision); if (originGuard) await originGuard(); @@ -158,7 +152,6 @@ export class HandoffController { started_at_ms: startedAt, }; } catch (error) { - await barrier; if (recorded) { await mutateState((state) => { const handoff = state.handoff; @@ -178,8 +171,6 @@ export class HandoffController { }); await chrome.alarms.clear(HANDOFF_ALARM); } - const recovered = await readState(); - if (!recovered.paused && !recovered.handoff.active) this.scheduler.resume(); throw error; } } @@ -226,8 +217,6 @@ export class HandoffController { }); await chrome.alarms.clear(HANDOFF_ALARM); await this.ownership.setTaskState(handoff.taskId, "working"); - const current = await readState(); - if (!current.paused && !current.handoff.active) this.scheduler.resume(); } private async cancelMatchingNow( @@ -260,19 +249,8 @@ export class HandoffController { } private async resumeNow(): Promise { - const state = await readState(); - if (state.handoff.active) { - throw Object.assign(new Error("Finish or cancel credential handoff before resuming"), { - code: "handoff_in_progress", - }); - } await this.ownership.reconcile(); await mutateState((next) => { - if (next.handoff.active) { - throw Object.assign(new Error("Finish or cancel credential handoff before resuming"), { - code: "handoff_in_progress", - }); - } next.paused = false; }); this.scheduler.resume(); diff --git a/packages/extension/src/native.ts b/packages/extension/src/native.ts index 4d4b374..427ac59 100644 --- a/packages/extension/src/native.ts +++ b/packages/extension/src/native.ts @@ -333,7 +333,7 @@ export class NativeBridge { } const state = await readState(); if (this.port !== port) return; - if (!state.paused && !state.handoff.active) this.scheduler.resume(); + if (!state.paused) this.scheduler.resume(); await this.onReady(); } diff --git a/packages/extension/src/popup.html b/packages/extension/src/popup.html index 7b5e1e0..a213141 100644 --- a/packages/extension/src/popup.html +++ b/packages/extension/src/popup.html @@ -101,7 +101,7 @@

Task groups

diff --git a/packages/extension/src/storage.ts b/packages/extension/src/storage.ts index e5079f0..8ab6660 100644 --- a/packages/extension/src/storage.ts +++ b/packages/extension/src/storage.ts @@ -75,7 +75,7 @@ function defaultState(): ExtensionState { schemaVersion: SCHEMA_VERSION, paused: false, developerMode: false, - skipCommitReview: false, + skipCommitReview: true, showAgentPointer: true, cleanupPolicy: "automatic", tasks: {}, @@ -182,11 +182,14 @@ function parseState(value: unknown): ExtensionState | null { return null; } const tabIds = task.tabIds as number[]; - const createdTabIds = (task.createdTabIds ?? []) as number[]; + // Legacy builds tracked task-created tabs for cleanup even after the tab left the task, + // so persisted createdTabIds may reference tabs that are no longer members. Sanitize to + // the current subset invariant instead of rejecting the whole persisted state, which + // would brick startup on data written by an older build. + const createdTabIds = [...new Set((task.createdTabIds ?? []) as number[])] + .filter((tabId) => tabIds.includes(tabId)); if ( new Set(tabIds).size !== tabIds.length || - new Set(createdTabIds).size !== createdTabIds.length || - createdTabIds.some((tabId) => !tabIds.includes(tabId)) || (task.groupId === null && tabIds.length > 0) || tabIds.some((tabId) => assignedTabIds.has(tabId)) || (task.groupId !== null && assignedGroupIds.has(task.groupId as number)) @@ -272,7 +275,7 @@ function parseState(value: unknown): ExtensionState | null { schemaVersion: SCHEMA_VERSION, paused: raw.paused, developerMode: raw.developerMode, - skipCommitReview: raw.skipCommitReview === true, + skipCommitReview: raw.skipCommitReview !== false, showAgentPointer: raw.showAgentPointer, cleanupPolicy: cleanupPolicy as CleanupPolicy, tasks, diff --git a/packages/extension/test/extension.test.ts b/packages/extension/test/extension.test.ts index c6df4cd..8119d0b 100644 --- a/packages/extension/test/extension.test.ts +++ b/packages/extension/test/extension.test.ts @@ -1315,6 +1315,15 @@ describe("durable extension state", () => { expect(storageRemoveCount).toBe(1); }); + test("enables direct consequential actions by default and preserves an explicit opt-out", async () => { + expect((await readState()).skipCommitReview).toBe(true); + await mutateState((state) => { + state.skipCommitReview = false; + }); + resetStateForTest(); + expect((await readState()).skipCommitReview).toBe(false); + }); + test("skips no-op persistence and avoids hot-path write verification reads", async () => { await readState(); const initialGets = storageGetCount; @@ -1359,6 +1368,26 @@ describe("durable extension state", () => { }); }); + test("sanitizes legacy createdTabIds that outlived task membership instead of failing startup", async () => { + const base = await readState(); + base.tasks[TASK_A] = { + taskId: TASK_A, + name: "Legacy ledger task", + groupId: null, + tabIds: [], + createdTabIds: [77, 77], + color: "pink", + state: "working", + createdAt: 1, + updatedAt: 2, + }; + persisted[STATE_KEY] = JSON.parse(JSON.stringify(base)); + resetStateForTest(); + + const restored = await readState(); + expect(restored.tasks[TASK_A]).toMatchObject({ tabIds: [], createdTabIds: [] }); + }); + test("still rejects malformed persisted state during startup", async () => { await readState(); persisted[STATE_KEY] = { schemaVersion: 1, paused: "not-a-boolean" }; @@ -1443,6 +1472,9 @@ describe("page revision monotonicity", () => { () => undefined, async () => undefined, ); + await mutateState((state) => { + state.skipCommitReview = false; + }); const execution = await runtime.act(TASK_A, 65, 1, [{ kind: "upload_file", @@ -3160,7 +3192,7 @@ describe("ownership and task isolation", () => { }); describe("handoff and pause barriers", () => { - test("keeps the global pause active until the completion condition matches", async () => { + test("keeps the handoff durable without pausing browser work", async () => { await seedTask(TASK_A, [31]); const scheduler = new MutationScheduler(); const revisions = new RevisionTracker(); @@ -3182,19 +3214,19 @@ describe("handoff and pause barriers", () => { timeout_ms: 60_000, }); - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); expect((await readState()).handoff.active).toBe(true); expect((await readState()).tasks[TASK_A]?.state).toBe("needs_user"); expect(await handoff.finish(true)).toMatchObject({ completed: false, reason: "The handoff completion condition has not been met", }); - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); expect((await readState()).handoff.active).toBe(true); scriptResult = true; expect(await handoff.finish(true)).toEqual({ completed: true }); - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); const pendingHandoff = (await readState()).handoff; if (!pendingHandoff.active || !pendingHandoff.pendingClearEventId || !clearEventId) { throw new Error("handoff completion must await a native acknowledgment"); @@ -3233,7 +3265,7 @@ describe("handoff and pause barriers", () => { expect(await handoff.cancelForTab(33)).toBe(true); const firstPending = (await readState()).handoff; expect((await readState()).tasks[TASK_A]?.state).toBe("needs_user"); - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); const firstClearEventId = events.at(-1)?.eventId; if (!firstPending.active || !firstPending.pendingClearEventId || !firstClearEventId) { throw new Error("tab cancellation did not create a pending handoff event"); @@ -3257,7 +3289,7 @@ describe("handoff and pause barriers", () => { expect(await handoff.cancelForTask(TASK_B)).toBe(true); const secondPending = (await readState()).handoff; expect((await readState()).tasks[TASK_B]?.state).toBe("needs_user"); - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); const secondClearEventId = events.at(-1)?.eventId; if (!secondPending.active || !secondPending.pendingClearEventId || !secondClearEventId) { throw new Error("task cancellation did not create a pending handoff event"); @@ -3303,7 +3335,7 @@ describe("handoff and pause barriers", () => { expect(revokedTabIds).toEqual([35]); const pending = (await readState()).handoff; - expect(scheduler.isAccepting()).toBe(false); + expect(scheduler.isAccepting()).toBe(true); const clearEventId = clearEventIds.at(-1); if (!pending.active || !pending.pendingClearEventId || !clearEventId) { throw new Error("startup reconciliation did not create a pending handoff event"); @@ -3952,6 +3984,12 @@ describe("startup lifecycle", () => { }); describe("consequential action staging", () => { + beforeEach(async () => { + await mutateState((state) => { + state.skipCommitReview = false; + }); + }); + test("stages a purchase-like click, commits it once, and consumes the token", async () => { tabStore.set(90, { id: 90, windowId: 1, groupId: -1, active: true }); tabStore.set(7, { id: 7, windowId: 1, groupId: -1, active: false }); @@ -4668,17 +4706,27 @@ describe("extension entrypoint admission boundaries", () => { result: { task_id: TASK_A, tab_id: 100, handoff_started: true }, }); expect((await readState()).handoff).toMatchObject({ active: true, taskId: TASK_A, tabId: 100 }); - const deniedDuringHandoff = await sendNativeCommand( - "018f47b8-2f80-7c20-9c77-f8a38c9e6226", + const tabsDuringHandoff = await sendNativeCommand( + "018f47b8-2f80-7c20-9c77-f8a38c9e6500", + TASK_A, + "browser_tabs", + {}, + ); + expect(tabsDuringHandoff).toMatchObject({ + outcome: "completed", + result: { tabs: [{ tab_id: 100, task_id: TASK_A }] }, + }); + const snapshotDuringHandoff = await sendNativeCommand( + "018f47b8-2f80-7c20-9c77-f8a38c9e6501", TASK_A, "browser_snapshot", { tab_id: 100, mode: "accessibility" }, ); - expect(deniedDuringHandoff).toMatchObject({ - outcome: "not_started", - error: { code: "handoff_blackout" }, + expect(snapshotDuringHandoff).toMatchObject({ + outcome: "completed", + result: { tab_id: 100 }, }); - expect(debuggerCommands).toHaveLength(deniedBeforePermission); + const commandsAfterHandoffSnapshot = debuggerCommands.length; expect(await sendPopupMessage({ kind: "handoff_finish", completed: true })).toEqual({ completed: true }); const pendingHandoff = (await readState()).handoff; if (!pendingHandoff.active || !pendingHandoff.pendingClearEventId) { @@ -4707,7 +4755,7 @@ describe("extension entrypoint admission boundaries", () => { outcome: "not_started", error: { code: "developer_mode_required" }, }); - expect(debuggerCommands).toHaveLength(deniedBeforePermission); + expect(debuggerCommands).toHaveLength(commandsAfterHandoffSnapshot); expect(await sendPopupMessage({ kind: "developer_mode", enabled: true })).toEqual({ enabled: true }); const developerEnabled = await sendNativeCommand( "018f47b8-2f80-7c20-9c77-f8a38c9e6228", @@ -4721,6 +4769,9 @@ describe("extension entrypoint admission boundaries", () => { ({ method, params }) => method === "Runtime.evaluate" && params.expression === "document.title", ), ).toBe(true); + expect(await sendPopupMessage({ kind: "set_skip_commit_review", enabled: false })).toEqual({ + enabled: false, + }); const staged = await sendNativeCommand( diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index a157ec5..82c4480 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -180,12 +180,12 @@ export const STANDARD_TOOLS: readonly Tool[] = [ }, { name: "browser_handoff", - description: "Start an asynchronous human handoff for credentials, MFA, CAPTCHA, or other human-only input. The call returns after activation; browser automation remains paused until completion.", + description: "Start a durable human handoff for MFA, CAPTCHA, passkeys, or other human-only input. The call returns after activation and browser automation remains available; try browser_credentials first for ordinary sign-in fields.", inputSchema: schema(handoffSchema), }, { name: "browser_credentials", - description: "Use an origin-matching 1Password Login without exposing values; more than three matches require the user.", + description: "Prefer this before human handoff: use an origin-matching 1Password Login without exposing values; more than three matches require the user.", inputSchema: schema(credentialsSchema), }, { diff --git a/packages/omp/src/index.ts b/packages/omp/src/index.ts index 712327a..1711005 100644 --- a/packages/omp/src/index.ts +++ b/packages/omp/src/index.ts @@ -235,7 +235,7 @@ const DEFINITIONS: ReadonlyArray<{ { name: "browser_handoff", label: "Browser Handoff", - description: "Start an asynchronous human handoff for credentials, MFA, CAPTCHA, or other human-only input. The call returns after activation; browser automation remains paused until completion.", + description: "Start a durable human handoff for MFA, CAPTCHA, passkeys, or other human-only input. The call returns after activation and browser automation remains available; try browser_credentials first for ordinary sign-in fields.", approval: "write", schema: (z) => z.object({ tab_id: z.number().int().min(0), @@ -251,7 +251,7 @@ const DEFINITIONS: ReadonlyArray<{ { name: "browser_credentials", label: "Browser Credentials", - description: "Use a URL-matching 1Password Login without exposing its values. Prepare first; more than three matches require the user. Fill only, then submit through Browser Act.", + description: "Prefer this before human handoff: use a URL-matching 1Password Login without exposing its values. Prepare first; more than three matches require the user. Fill only, then submit through Browser Act.", approval: "write", schema: (z) => z.union([ z.object({ diff --git a/schemas/rpc/v1/browser-handoff.schema.json b/schemas/rpc/v1/browser-handoff.schema.json index 530551c..cebad20 100644 --- a/schemas/rpc/v1/browser-handoff.schema.json +++ b/schemas/rpc/v1/browser-handoff.schema.json @@ -19,6 +19,7 @@ { "type": "object", "required": ["kind", "value"], + "description": "kind \"url\" matches the tab URL exactly; pass the full absolute URL. kind \"selector\" matches a CSS selector on the page.", "properties": { "kind": { "enum": ["url", "selector"] }, "value": { "type": "string", "minLength": 1, "maxLength": 2048 }