diff --git a/CHANGELOG.md b/CHANGELOG.md index f60ac7b..8f188f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## Unreleased -- Replaced the Chrome Bridge v1 runtime with the AgentTab 2.0 release candidate: a Rust production host over OS-native local IPC, seven task-scoped Standard methods, explicit resumable capabilities, a developer-only eighth 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`. +- Replaced the Chrome Bridge v1 runtime with the AgentTab 2.0 release candidate: a Rust production host over OS-native local IPC, seven task-scoped Standard methods, explicit resumable capabilities, a developer-only eighth method, TypeScript and Python SDKs, MCP and OMP adapters, a transactional installer, and a minimal extension. Fresh extension state starts in unattended Autopilot; existing pre-policy state migrates to Strict to preserve former Commit behavior. Review selected and Strict use a two-party Commit flow in which `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 extension-local persistent Autopilot, Review selected, and Strict action-policy profiles, remembered effect-category approvals scoped to a task, HTTP(S) origin, or all sites, and an idempotent `agenttab policy allow-upload PATH` command for the separate host upload-root policy. Pause is now only a logical admission toggle, while `scripting` joins `debugger` as an install-time permission so unattended work does not stop on a runtime grant prompt. Sensitive password, passkey, OTP, CAPTCHA, and payment fields still require Your Turn in every profile. - 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. - Migrated the Python MCP server to SDK 2.0 and the `2026-07-28` protocol while preserving legacy stdio compatibility, scoped tool registration, resources, annotations, per-request HTTP bridge tokens, and streamable HTTP configuration. diff --git a/README.md b/README.md index 186700d..03f332e 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. Passwords, passkeys, 2FA, CAPTCHA, payment secrets, and other human-only input belong to **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. Passwords, passkeys, 2FA, CAPTCHA, payment secrets, and other human-only input belong to **Your Turn**. Fresh installs start in **Autopilot** to avoid unattended Commit prompts; existing pre-policy state upgrades to Strict so former Commit behavior is preserved. Review selected and Strict can stage recognizable effects for **Commit**. ## Release status @@ -25,18 +25,18 @@ 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 a site requires human-only input, the agent calls `browser_handoff`. AgentTab 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. +4. Under Review selected or Strict, 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. Autopilot executes recognized effects directly. 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. Chrome does not expose page scripting or debugger access on browser-restricted origins such as `chrome://`, `chrome-extension://`, `devtools://`, and the Chrome Web Store. AgentTab reports these task tabs with `automation_route: "tab_only"`. Explicit navigation, reload, close, load or URL waits, and human-only `browser_handoff` remain available. History movement is also available when managed origin constraints are absent; with constraints, AgentTab rejects it because Chrome does not expose the destination for authorization before navigation. Download waits require the `full` route because exact task-tab attribution comes from tab-scoped debugger events, not browser-global download state. Page snapshots, element actions, page-content waits, and raw Developer-mode CDP fail immediately with `browser_restricted_origin` and `outcome: "not_started"` before AgentTab attempts the blocked route. Use a focus-safe OS accessibility driver bound to the exact browser window when native UI work is required. -Commit is a two-party, best-effort semantic barrier, not proof that a page has no external effect. The popup records the human approval, while only the agent's later `browser_commit` can execute the staged action. Page content is untrusted data and a page can attach an effect to an innocently labelled control. Inspect the page and staged action before approving or committing. +When enabled by the action policy, Commit is a two-party, best-effort semantic barrier, not proof that a page has no external effect. The popup records the human approval, while only the agent's later `browser_commit` can execute the staged action. Page content is untrusted data and a page can attach an effect to an innocently labelled control. Autopilot deliberately removes this review barrier. See [Action policy](docs/action-policy.md). ## 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.** 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. +- **Commit is policy-controlled.** Review selected and Strict stage recognized effects; Autopilot does not. 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. - **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 diff --git a/docs/action-policy.md b/docs/action-policy.md new file mode 100644 index 0000000..d3b8fde --- /dev/null +++ b/docs/action-policy.md @@ -0,0 +1,53 @@ +# Action policy + +AgentTab separates three controls that solve different problems: + +- **Action policy** decides whether a recognizable effect runs immediately or stages a Commit. +- **Pause agents** is a persistent logical admission barrier. It does not add or remove Chrome permissions. +- **Your Turn** remains mandatory for passwords, passkeys, one-time codes, CAPTCHA, payment-card secrets, and other sensitive fields in every policy profile. + +## Profiles + +| Profile | Recognized send, purchase, delete, permission, upload, or dialog effect | Owned-tab close | Intended use | +|---|---|---|---| +| `autopilot` | Runs immediately | Runs immediately | Fresh-install default for unattended local operation. No semantic Commit prompt is inserted. | +| `review_selected` | Stages Commit | Runs immediately | Review recognizable external effects without interrupting routine tab cleanup. | +| `strict` | Stages Commit | Stages Commit | Preserve the former supervised behavior. | + +Autopilot deliberately permits recognized high-cost and irreversible actions. It is not a low-risk mode. It exists for users who trust the local agent and value unattended completion over review prompts. The semantic classifier is best effort in every profile: a webpage can disguise an effect behind an innocent label or attach an unexpected handler to a control. + +Changing profiles affects future actions only. A stage already created under a review profile remains staged until it is approved, declined, expires, or is abandoned. + +Fresh extension state starts in Autopilot. Any existing persisted extension state that lacks either action-policy field, including imported legacy task or preference state, migrates to Strict with no remembered allowances. This preserves the Commit behavior that installation had before selectable profiles existed. + +The action profile and remembered decisions live only in extension storage. They are not added to the native v1 hello, events, host status, or Rust protocol, so the extension policy can deploy without a lockstep host update. + +## Remembered approvals + +When a Commit is shown in the extension popup, approval can be remembered for: + +- the same effect category in the current task; +- the same effect category on the current HTTP or HTTPS origin; or +- the same effect category on all sites. + +These are persistent allow decisions. They never bypass sensitive-field handoff, ownership checks, expected page revision, origin policy, or protocol validation. Task-scoped decisions are deleted when the task is finished. The popup shows the number of remembered decisions and can clear all of them in one action. + +## Chrome permissions + +`debugger` and `scripting` are required install-time permissions. AgentTab does not request or revoke `scripting` as a routine runtime toggle. Pause and Resume change only persisted scheduler admission. Disabling the extension from `chrome://extensions` remains the browser-level off switch. + +## Upload roots + +The host still requires every upload source to be inside an explicitly configured directory. Add a directory once: + +```text +agenttab policy allow-upload PATH +``` + +The command canonicalizes an existing current-user directory, serializes same-state-directory updates across processes, updates `policy.json` idempotently, and reports whether it added a new root. Restart the AgentTab host when it reports `restartRequired: true`; a repeated no-op reports `false`. `--state-dir PATH` targets a non-default host state directory. + +This path grant and the popup action profile are independent. The host rejects a file outside the configured roots before the extension can apply Autopilot. + +## Developer mode + +The raw `browser_developer` surface retains its adapter discovery flag, managed host policy, and visible extension toggle. Those gates are intentionally separate from Standard action policy because raw DevTools Protocol access is a broader API boundary, not another recognizable webpage effect. diff --git a/docs/adr/0001-agenttab-runtime.md b/docs/adr/0001-agenttab-runtime.md index 904bb03..d387335 100644 --- a/docs/adr/0001-agenttab-runtime.md +++ b/docs/adr/0001-agenttab-runtime.md @@ -32,9 +32,9 @@ While any Your Turn handoff is active, AgentTab MUST enforce a global observatio ### Commit -**Commit** is a best-effort semantic review barrier for recognizable consequential controls, including send, publish, purchase, delete, upload, authorization, and permission grants. +**Commit** is a policy-controlled, 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. The persistent action policy has three profiles: `autopilot` executes without semantic Commit, `review_selected` stages recognizable external effects while allowing owned-tab close, and `strict` also stages owned-tab close. Fresh state defaults to `autopilot`; persisted pre-policy state lacking either policy field MUST migrate to `strict` with no remembered allowances. The profile and remembered decisions remain extension-local and MUST NOT change native protocol v1 or host status. When policy requires review, the 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. Approval MAY persist as an effect-category allow decision scoped to the current task, current HTTP(S) origin, or that effect on all sites. Such decisions MUST NOT bypass sensitive-field handoff or structural authorization checks. 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. @@ -140,19 +140,19 @@ The host is guarded by one per-user OS lock. A stale socket is unlinked only aft ## Automation permissions -Chrome deterministically rejects `debugger` in `optional_permissions`; it must be a required install-time permission. AgentTab v2 Standard therefore requires `debugger` and makes only `scripting` optional. Current accessibility snapshots, precise click, type, and fill behavior, inactive screenshots, and task-scoped CDP helpers require debugger; a separately implemented non-debugger runtime is required before that install-time tradeoff can change. +Chrome deterministically rejects `debugger` in `optional_permissions`; it must be a required install-time permission. AgentTab v2 Standard requires both `debugger` and `scripting` at install time. Current accessibility snapshots, precise click, type, and fill behavior, inactive screenshots, and task-scoped CDP helpers require these capabilities; a separately implemented reduced-capability runtime is required before that install-time tradeoff can change. -The one-time install disclosure warns that AgentTab can use debugger, native messaging, downloads, and host access. This is deliberate: the popup's Automation control requests and removes only optional `scripting`, directly in its click gesture. Denial or revocation leaves the required debugger grant installed, but immediately detaches every AgentTab debugger target, clears debugger-backed runtime state, and fails Standard operations closed until scripting is re-granted. `chrome.permissions.onRemoved` performs the same cleanup for revocation outside the popup; AgentTab never attempts to remove required `debugger`. +The one-time install disclosure warns that AgentTab can use debugger, scripting, native messaging, downloads, and host access. Pause and Resume are persistent logical admission controls and do not repeatedly request or revoke Chrome permissions. If a required browser capability is abnormally unavailable, AgentTab fails Standard operations closed and its permission-removal recovery detaches debugger targets and clears debugger-backed runtime state. Chrome Split View is not an ownership or workspace boundary. Chrome 140 and later can place a newly created tab into the active Split View of the last-focused window. That tab cannot join a tab group, and a split paired with another extension can reject a debugger attachment. Background navigation therefore chooses the most recently accessed normal window whose active tab has `splitViewId == chrome.tabs.SPLIT_VIEW_ID_NONE`. If no such window exists, AgentTab creates an unfocused normal window. AgentTab never modifies the user's existing Split View to make a task tab usable. Top-document target resolution MUST pin an explicit main-frame isolated execution context. Chrome's implicit `Runtime.evaluate` context is not stable after another extension injects a frame. On inactive task tabs, `type` and `fill` update the resolved DOM target and dispatch input events without synthesizing browser focus; active tabs retain `Input.insertText`. This prevents password-manager and other extension frames from capturing task input or detaching the task debugger. -Debugger attachment MUST be lazy and limited to task-owned tabs, and the task connection is reused only until its idle timeout. The installed debugger grant is not an attachment grant: Standard mode exposes no raw CDP escape hatch and no attachment may remain while optional scripting is off. The Chrome Web Store rationale MUST disclose this exact limited use. +Debugger attachment MUST be lazy and limited to task-owned tabs, and the task connection is reused only until its idle timeout. The installed debugger grant is not an attachment grant: Standard mode exposes no raw CDP escape hatch. The Chrome Web Store rationale MUST disclose this exact limited use. -The stable target requires `nativeMessaging`, `tabs`, `tabGroups`, `storage`, `alarms`, and `debugger`, with exactly `scripting` optional and `` host access. `cookies`, `history`, `bookmarks`, `contentSettings`, `activeTab`, and browser-global `downloads` access are absent from that target. PR1 locks and statically verifies this transformation without changing or reloading the installed v1 extension. +The stable target requires `nativeMessaging`, `tabs`, `tabGroups`, `storage`, `alarms`, `debugger`, and `scripting`, with no optional permissions and with `` host access. `cookies`, `history`, `bookmarks`, `contentSettings`, `activeTab`, and browser-global `downloads` access are absent from that target. -Permission evidence follows the implementation boundary. PR1 proves the deterministic required/optional manifest split, the installed-debugger versus live-attachment boundary, and the platform IPC security primitives. Its three-run live matrix varies optional scripting while requiring debugger to remain granted; it verifies zero AgentTab debugger attachments when scripting is off or revoked, then preserves denial, re-grant, Pause, reload, disable, and cleanup checks. PR2 verifies the production host lifecycle and IPC implementation. PR3 implements the full ownership/UI lifecycle. A failed `activeTab` matrix run records the exact Chrome API call before retention. The live probe never changes Chrome permissions itself, rejects raw CDP at the non-configurable Standard boundary, and cleans only task-owned tabs plus its exact fixture download. +Permission evidence follows the implementation boundary. The offline gate proves the deterministic required manifest, the installed-debugger versus live-attachment boundary, and the platform IPC security primitives. Its live matrix verifies that required permissions remain present across logical Pause and Resume, plus reload, disable, cleanup, and raw-CDP denial. The live probe never changes Chrome permissions itself and cleans only task-owned tabs plus its exact fixture download. ## Persistence and crash behavior diff --git a/docs/benchmarks.md b/docs/benchmarks.md index cde4a72..7668c69 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -9,7 +9,7 @@ Every measured claim must retain a raw artifact and record: - the exact source commit, dirty-state status, build identity, host version, extension version, and adapter version; - the complete scenario and success criterion, including URL or controlled fixture, requested action, payload size, and whether an external side effect was intentionally avoided; - hardware, OS, browser channel/version, Chrome profile class, network conditions, and display/browser state where relevant; -- configuration that can affect the result, including Standard or Developer mode, optional permissions, policy, IPC endpoint, proxy use, and timeout; +- configuration that can affect the result, including Standard or Developer mode, browser permissions, action policy, IPC endpoint, proxy use, and timeout; - iteration count, warmup policy, cache state, concurrency, retry policy, start/end time, and per-iteration raw output; - summary method, exclusions, failures, timeouts, and the raw artifact path. diff --git a/docs/commands.md b/docs/commands.md index c9f73c0..32f6b02 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -12,6 +12,7 @@ agenttab --version agenttab install [--version X.Y.Z] [--verify-readiness] [--development --manifest-url URL --signature-url URL] agenttab status agenttab doctor [--layer ipc|extension] +agenttab policy allow-upload PATH [--state-dir PATH] agenttab mcp agenttab proxy --token-file PATH [--port 9224] ``` @@ -55,7 +56,7 @@ The current source contains the stable Ed25519 verification public key, but no m agenttab status ``` -Connects to local AgentTab IPC and prints the Core `agenttab.status` result as JSON. The status response reports the host lifecycle state, protocol version, whether a handoff is active, and the current connection's task identifier when one exists. +Connects to local AgentTab IPC and prints the Core `agenttab.status` result as JSON. The status response reports the host lifecycle state, protocol version, current action-policy profile, whether a handoff is active, and the current connection's task identifier when one exists. Use this only after the extension and native host are installed. It does not start a browser, create a task, use a port, or authenticate with a token. @@ -76,6 +77,18 @@ agenttab doctor --layer extension The extension layer is a diagnostic label around the status check. It does not reload Chrome for you. +## `agenttab policy allow-upload` + +```text +agenttab policy allow-upload PATH +``` + +Canonicalizes an existing current-user directory and adds it once to the host's `dlp_allowed_roots`. Updates targeting the same state directory are serialized across CLI processes so distinct concurrent roots are merged rather than lost. Repeating the same command is idempotent and reports `added: false` with `restartRequired: false`. Existing valid managed-policy fields are preserved, and an unknown or malformed policy is rejected instead of overwritten. + +The command writes the host `policy.json` with owner-only permissions where the platform supports them. The default file is `~/.agenttab/policy.json` on Unix and `%LOCALAPPDATA%\AgentTab\policy.json` on Windows. `--state-dir PATH` selects an explicit host state directory. Restart the AgentTab host only when the result reports `restartRequired: true`; the running host does not hot-reload policy. + +This is a one-time source-directory grant, not approval of a particular destination or webpage action. Whether an allowed upload also requires Commit is determined separately by the popup's [action policy](action-policy.md). + ## `agenttab mcp` and `agenttab-mcp` ```text @@ -125,5 +138,6 @@ Malformed JSON or YAML is reported and left untouched. The installer does not in - [Setup, identities, endpoints, migration, and uninstall status](setup.md) - [MCP setup, tools, outcomes, Commit, and handoff](mcp.md) +- [Action policy profiles and remembered approvals](action-policy.md) - [Core RPC request schema](../schemas/rpc/v1/request.schema.json) - [Core RPC response schema](../schemas/rpc/v1/response.schema.json) diff --git a/docs/launch/chrome-web-store.md b/docs/launch/chrome-web-store.md index 1544993..dcd5cca 100644 --- a/docs/launch/chrome-web-store.md +++ b/docs/launch/chrome-web-store.md @@ -34,7 +34,7 @@ Standard MCP access exposes exactly seven tools: `browser_open`, `browser_snapsh **Your Turn** is for passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. 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. -**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. +**Commit** is a policy-controlled, best-effort review barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. Fresh state starts in unattended Autopilot and inserts no semantic Commit prompt; existing pre-policy state migrates to Strict. Review selected stages recognizable external effects, and Strict also stages owned-tab close. A staged record requires popup approval and the requesting agent's one-use token, expires after a short interval, and is invalidated if the page or target changes. Commit reduces recognizable risk; it cannot prove that a page has no hidden external effect. ### Trust boundary @@ -54,7 +54,7 @@ This section is draft review copy for the v2 contract. It must be reconciled aga | `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. | | `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. | +| `scripting` | Required permission | Supports the task-scoped text, HTML, selector, wait, scroll, and pointer paths. Keeping it installed avoids a runtime prompt that can strand unattended work. Pause is a logical scheduler control; Standard mode still exposes no arbitrary 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. | ## Reviewer setup notes @@ -66,7 +66,7 @@ These notes are for a controlled reviewer package only. They are not public inst 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. +6. Select Review selected or Strict, then 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. 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 seven Standard tools and that the Developer-only tool is absent until the reviewer explicitly enables Developer mode. diff --git a/docs/launch/directory-listings.md b/docs/launch/directory-listings.md index 5c983d1..c3950f1 100644 --- a/docs/launch/directory-listings.md +++ b/docs/launch/directory-listings.md @@ -20,7 +20,7 @@ Give an agent a task workspace in your signed-in Chrome profile, not broad contr ## Short description -AgentTab is a local browser runtime for AI agents. It gives each agent task-owned tabs, uses Your Turn for human-only input, stages recognizable consequential actions with Commit, and connects local MCP clients through per-user operating-system-native IPC. +AgentTab is a local browser runtime for AI agents. It gives each agent task-owned tabs, uses Your Turn for human-only input, starts fresh state in unattended Autopilot while preserving Strict on pre-policy upgrades, offers Commit review profiles, and connects local MCP clients through per-user operating-system-native IPC. ## Long description @@ -35,7 +35,7 @@ AgentTab declares the `` host permission so its defined `chrome.script **Your Turn** is the human handoff state for passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. During handoff, AgentTab applies an observation blackout across every task, so standard observations return `needs_user`; it does not capture human keystrokes. -**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. +**Commit** is a policy-controlled, best-effort review barrier for recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls. Autopilot executes directly; Review selected and Strict can stage a recognizable action with a preview, human popup approval, and the requesting agent's one-use token. 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 eb6f017..57a87b7 100644 --- a/docs/launch/reddit-posts.md +++ b/docs/launch/reddit-posts.md @@ -19,7 +19,7 @@ The runtime is local-only: one minimal MV3 extension, a local Rust host, Chrome The Standard MCP surface is intentionally small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, and `browser_commit`. 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. **Your Turn** is for passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. 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. **Your Turn** always handles passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. During that handoff, AgentTab blackouts standard observation for every task, so captures return `needs_user`; it does not capture the person's keystrokes. Fresh installs start in unattended **Autopilot**, while existing pre-policy state upgrades to Strict. Review selected and Strict use **Commit** as a best-effort review barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants. 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. @@ -41,7 +41,7 @@ The architecture is deliberately local. A minimal MV3 extension connects to one The handoff model is called **Your Turn**. If a task reaches a password, passkey, two-factor challenge, CAPTCHA, payment secret, or another human-only step, 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. -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. +For recognizable consequential actions, fresh state defaults to direct **Autopilot** execution; existing pre-policy state migrates to Strict. Review selected and Strict use **Commit** 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. The Standard MCP interface has seven tools and excludes raw cookies, browser storage, arbitrary scripts, raw CDP, and raw network access. 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. It uses task-owned tabs, with unattended Autopilot for fresh state and Strict for pre-policy upgrades, not broad browser control: Your Turn blackouts observation for human-only steps, review profiles stage recognizable effects for Commit, 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. diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md index 3ae696f..328d7cf 100644 --- a/docs/launch/show-hn.md +++ b/docs/launch/show-hn.md @@ -23,8 +23,8 @@ The runtime is one minimal MV3 extension plus a local Rust host. The extension u Standard MCP access is deliberately small: `browser_open`, `browser_snapshot`, `browser_act`, `browser_wait`, `browser_tabs`, `browser_handoff`, and `browser_commit`. 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. -Two human controls are central. **Your Turn** handles passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. 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. +Two human controls are central. **Your Turn** always handles passwords, passkeys, two-factor authentication, CAPTCHA, payment secrets, and other human-only input. 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. Fresh state starts in unattended **Autopilot**; existing pre-policy state migrates to Strict. Review selected and Strict turn **Commit** into a best-effort barrier for recognizable sends, publishes, purchases, deletes, uploads, authorizations, and permission grants; Strict also reviews owned-tab close. -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. +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. Autopilot deliberately removes that review barrier. 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 seven-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 650cb97..7645227 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -9,7 +9,7 @@ The adapter is stdio only. Standard mode uses local AgentTab IPC behind the adap Before starting the adapter, an approved local installation must have: 1. An enabled AgentTab extension in Chrome 127 or later. -2. Chrome's required `debugger` permission present from installation and the optional `scripting` permission granted from the AgentTab popup. +2. Chrome's required `debugger` and `scripting` permissions present from installation. 3. A running `dev.agenttab.host` connected to the extension through Native Messaging. 4. Local IPC readiness. Check it with `agenttab doctor --layer ipc` after the command is available. @@ -106,7 +106,7 @@ The agent must not attempt snapshots, page reads, or mutations during this inter ### 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. Under Review selected or Strict, recognizable send, publish, purchase, delete, upload, authorization, and permission-grant controls can stop before the side effect and return: ```json { @@ -126,7 +126,7 @@ The token is bound to the task, tab, effect, page revision, and element fingerpr 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. -Commit reduces recognizable risk only. It requires both the popup's human approval and the agent's later `browser_commit`, but it cannot prove that a page's labels, event handlers, or side effects are benign. +Autopilot does not insert semantic Commit stages. Fresh installs start there; existing pre-policy state migrates to Strict. Review selected stages recognizable effects while allowing owned-tab close; Strict also stages owned-tab close. A popup approval may be remembered for the same effect in the current task, on the current site, or on all sites. Commit reduces recognizable risk only: it cannot prove that a page's labels, event handlers, or side effects are benign. See [Action policy](action-policy.md). ## Schemas and related documentation diff --git a/docs/multi-agent.md b/docs/multi-agent.md index 9a27dc8..efa6d47 100644 --- a/docs/multi-agent.md +++ b/docs/multi-agent.md @@ -38,7 +38,7 @@ Every existing-page mutation includes `expected_page_revision`. The host and ext ## Pause and recovery -Pause is a barrier, not an optimistic UI toggle. It stops new admissions, lets already-dispatched work settle, rejects waiting work as not started, and persists the paused state. On restart, the extension restores paused state before reconciliation. Resume reconciles ownership before reopening admission. +Pause is a barrier, not an optimistic UI toggle. It stops new admissions, lets already-dispatched work settle, rejects waiting work as not started, and persists the paused state. It does not request or revoke Chrome permissions. On restart, the extension restores paused state before reconciliation. Resume reconciles ownership before reopening admission. 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. @@ -50,6 +50,6 @@ Automation resumes only after the declared completion condition or explicit comp ## 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. +Under Review selected or Strict, each recognizable consequential Standard action stages its own Commit. Strict also stages owned-tab close; Autopilot inserts no semantic Commit stage. Fresh installs start in Autopilot, while existing pre-policy state migrates to Strict. 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. Remembered approvals remain bound to an effect category and a task, site, or all-sites scope. 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. diff --git a/docs/roadmap.md b/docs/roadmap.md index 3f99e9b..b99ba6c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,7 +8,7 @@ AgentTab v2 is currently `2.0.0-rc.1`, a local prerelease. This page records lau | --- | --- | --- | | 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 seven Standard browser tools; Developer mode adds `browser_developer`. | Schema, adapter discovery, 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. | +| Task safety | Source implements server-bound tasks, visible groups, revisions, logical Pause, handoff blackout, persistent action-policy profiles, remembered approvals, and staged Commit records. | Controlled browser fixtures covering restart, required-permission continuity, policy profiles, stale revisions, 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. | diff --git a/docs/rust-host.md b/docs/rust-host.md index a5beae5..1664722 100644 --- a/docs/rust-host.md +++ b/docs/rust-host.md @@ -49,9 +49,9 @@ Mutation idempotency is keyed by task and UUIDv7 key with a canonical method/par ## Guardrails and effects -The host validates optional origin policy before dispatch and revalidates the known current origin for later actions. Uploads must be regular files under configured allowed roots and size limits; they are copied into private staging before the extension receives them. +The host validates optional origin policy before dispatch and revalidates the known current origin for later actions. Uploads must be regular files under configured allowed roots and size limits; `agenttab policy allow-upload PATH` adds one canonical root idempotently. Authorized files are copied into private staging before the extension receives them. -The extension classifies recognizable consequential actions and the host durably binds the resulting Commit stage to its task, tab, revision, effect, fingerprint, expiry, one-use token, and popup review handle. Popup approval marks that host record approved without dispatch. Only a later public `browser_commit` with the private token can consume the approved record and execute it. The host does not infer that a staged label makes the underlying page effect safe. +The extension applies its action policy locally and leaves native protocol v1 unchanged. Under a review profile, the extension returns the existing Commit stage shape, which the host durably binds to its task, tab, revision, effect, fingerprint, expiry, one-use token, and popup review handle. Popup approval marks that host record approved without dispatch. Only a later public `browser_commit` with the private token can consume the approved record and execute it. The host does not mirror the selected profile or infer that a staged label makes the underlying page effect safe. See [Action policy](action-policy.md). ## Build and test scope diff --git a/docs/security.md b/docs/security.md index 952377e..d0a2d6c 100644 --- a/docs/security.md +++ b/docs/security.md @@ -30,15 +30,15 @@ Sensitive password, passkey, one-time-code, payment, and similar fields require ## Permissions -The manifest declares `nativeMessaging`, `debugger`, `tabs`, `tabGroups`, `storage`, and `alarms`, with `` host access. Chrome does not permit `debugger` in `optional_permissions`, so it is a required install-time permission. `scripting` remains optional: the popup requests it when automation is enabled and removes it when automation is turned off. +The manifest declares `nativeMessaging`, `debugger`, `scripting`, `tabs`, `tabGroups`, `storage`, and `alarms`, with `` host access. Both browser-execution capabilities are install-time permissions, so an unattended run cannot stop on a later scripting prompt. Pause and Resume are logical scheduler state and never request or revoke Chrome permissions. -`debugger` is retained because the current task-scoped implementation needs Chrome's accessibility tree, ref-based action support, inactive captures, dialog handling, network-idle observation, and exact download completion attribution. Attachments are lazy, limited to owned tabs, reused briefly, and detached after idle work or optional `scripting` revocation. The permission is not evidence that Standard mode grants generic debugging access. +`debugger` is retained because the current task-scoped implementation needs Chrome's accessibility tree, ref-based action support, inactive captures, dialog handling, network-idle observation, and exact download completion attribution. Attachments are lazy, limited to owned tabs, reused briefly, and detached after idle work or extension shutdown. The permission is not evidence that Standard mode grants generic debugging access. ## Browser-restricted origins Chrome refuses extension page scripting or debugger access on browser-owned surfaces, including `chrome://`, `chrome-extension://`, `devtools://`, and the Chrome Web Store. Local host policy and extension permissions cannot remove that platform boundary. -AgentTab classifies every opened or listed task tab as `automation_route: "full"` or `"tab_only"`. Ownership remains required for both routes. On `tab_only`, agent-driven controls are limited to tab-lifecycle operations that Chrome still exposes: explicit navigation, reload, close, and load or URL waits. History movement is also available when managed origin constraints are absent. When constraints exist, AgentTab rejects history movement because Chrome does not expose its destination for authorization before navigation; callers must navigate explicitly to an allowed URL. Download waits require the `full` route because Chrome's browser-global downloads API does not identify the initiating tab, while the debugger events used for exact attribution are unavailable on restricted origins. `browser_handoff` remains available because AgentTab blacks out observation while the human controls the task tab. Snapshot, page-content wait, element action, page-dependent Commit, and raw Developer-mode CDP requests fail before script or debugger execution with `browser_restricted_origin` and `outcome: "not_started"`. A staged close remains executable through `browser_commit` because it uses Chrome's tab-lifecycle API without page access; ownership, revision, approval, token, and expiry checks still apply. Managed origin policy still validates HTTP and HTTPS tab-only pages and every explicit navigation target. Browser-owned non-HTTP pages cannot match an origin allowlist, so AgentTab admits only the same tab-only recovery operations there. In-page interaction requires an exact-tab human handoff; do not escalate to desktop-wide or browser-window-scoped input. +AgentTab classifies every opened or listed task tab as `automation_route: "full"` or `"tab_only"`. Ownership remains required for both routes. On `tab_only`, agent-driven controls are limited to tab-lifecycle operations that Chrome still exposes: explicit navigation, reload, close, and load or URL waits. History movement is also available when managed origin constraints are absent. When constraints exist, AgentTab rejects history movement because Chrome does not expose its destination for authorization before navigation; callers must navigate explicitly to an allowed URL. Download waits require the `full` route because Chrome's browser-global downloads API does not identify the initiating tab, while the debugger events used for exact attribution are unavailable on restricted origins. `browser_handoff` remains available because AgentTab blacks out observation while the human controls the task tab. Snapshot, page-content wait, element action, page-dependent Commit, and raw Developer-mode CDP requests fail before script or debugger execution with `browser_restricted_origin` and `outcome: "not_started"`. Under Strict, a staged close remains executable through `browser_commit` because it uses Chrome's tab-lifecycle API without page access; under Autopilot and Review selected, owned-tab close executes directly. Managed origin policy still validates HTTP and HTTPS tab-only pages and every explicit navigation target. Browser-owned non-HTTP pages cannot match an origin allowlist, so AgentTab admits only the same tab-only recovery operations there. In-page interaction requires an exact-tab human handoff; do not escalate to desktop-wide or browser-window-scoped input. ## Ownership, revisions, and human control @@ -52,9 +52,9 @@ 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. Autopilot executes recognizable effects directly. Review selected and Strict instead stage recognizable send, publish, purchase, delete, upload, authorization, permission-grant, and dialog effects as five-minute, one-use Commit records; Strict also stages owned-tab close. 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 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. Autopilot deliberately removes this semantic barrier, and remembered allow decisions narrow it in review profiles. Neither mechanism bypasses sensitive-field handoff or structural protocol checks. Batches are sequential and non-atomic: work stops before a staged action and never runs later actions implicitly. See [Action policy](action-policy.md). ## Your Turn blackout @@ -64,9 +64,9 @@ This reduces exposure during handoff. It cannot protect secrets from a compromis ## Upload guardrails -`upload_file` is available only for regular files below a configured `dlp_allowed_roots` path and under the configured size limit. The host canonicalizes the path, rejects symlink races and Unix hard-linked files, verifies the opened file, copies it into a private staging directory, and uses the staged copy for the action. On Unix, staging files are mode `0600`; staged files are removed after the terminal Commit path when cleanup succeeds. +`upload_file` is available only for regular files below a configured `dlp_allowed_roots` path and under the configured size limit. Run `agenttab policy allow-upload PATH` once to add an existing current-user directory idempotently, then restart the host. The host canonicalizes the path, rejects symlink races and Unix hard-linked files, verifies the opened file, copies it into a private staging directory, and uses the staged copy for the action. On Unix, staging files are mode `0600`; staged files are removed after the terminal action or Commit path when cleanup succeeds. -These checks limit accidental path selection. They do not establish that a permitted file is safe to disclose or that the destination is trustworthy. Upload is a recognizable Commit effect and should be reviewed by the human. +These checks limit accidental path selection. They do not establish that a permitted file is safe to disclose or that the destination is trustworthy. Upload executes directly in Autopilot and is a recognizable Commit effect in Review selected and Strict. ## Resume capabilities and durable state diff --git a/docs/setup.md b/docs/setup.md index 57c8080..5e58a0b 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -8,7 +8,7 @@ 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. Use **Your Turn** for passwords, passkeys, 2FA, CAPTCHA, payment secrets, and other human-only input. Review a staged **Commit** before performing it. +- Keep page content untrusted. Use **Your Turn** for passwords, passkeys, 2FA, CAPTCHA, payment secrets, and other human-only input. **Autopilot** does not insert Commit prompts; choose Review selected or Strict when recognizable effects should be staged. Fresh installs start in Autopilot, while existing pre-policy state migrates to Strict. - 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. The product boundary and residual Commit risk are described in the [runtime ADR](adr/0001-agenttab-runtime.md) and [Security](security.md). @@ -58,10 +58,12 @@ The installer deliberately stages the extension but does not silently install or 2. Enable **Developer mode**. 3. Choose **Load unpacked** and select the installer-reported AgentTab extension directory. For a source build, that is `packages/extension/dist/`. 4. Confirm that **AgentTab** is enabled. -5. Open the AgentTab popup and choose **Enable AgentTab automation**. Chrome requests the optional `scripting` permission. The required `debugger` permission is already present from extension installation; both capabilities are required for Standard browser automation. +5. Open the AgentTab popup and confirm the action policy. Fresh state starts in unattended **Autopilot**; upgraded pre-policy state starts in Strict to preserve former Commit behavior. Review selected and Strict add Commit review. 6. Run `agenttab doctor --layer extension` after the extension is enabled. Use `agenttab doctor --layer ipc` to check the local host path. -The manifest keeps `nativeMessaging`, `debugger`, `tabs`, `tabGroups`, `storage`, and `alarms` as required permissions because Chrome rejects `debugger` in `optional_permissions`. `scripting` is optional and is requested only from the user-facing popup. Removing it disables automation and detaches active task debugger sessions until it is enabled again. +The manifest keeps `nativeMessaging`, `debugger`, `scripting`, `tabs`, `tabGroups`, `storage`, and `alarms` as required permissions. This keeps setup to Chrome's install confirmation and prevents unattended work from stopping on a later scripting prompt. **Pause agents** is a persisted logical scheduler toggle and never requests or revokes a Chrome permission. + +To permit file uploads from a working directory, run `agenttab policy allow-upload PATH` once and restart the AgentTab host. The command is idempotent and updates the same `policy.json` read by the host. See [Action policy](action-policy.md) for profile and remembered-approval behavior. ## Native identity, registration, and local paths diff --git a/docs/telemetry.md b/docs/telemetry.md index d665800..2cb5386 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -28,7 +28,7 @@ A user-visible website confirmation, transaction receipt, or download is indepen ## Extension local state -The extension keeps task state, paused state, active handoff marker, staged Commit records, and revision information in Chrome extension storage. This lets it restore safety barriers after service-worker restart. Chrome may sync or back up browser-profile data according to the user's browser/account configuration; AgentTab does not initiate a telemetry upload. +The extension keeps task state, paused state, action-policy profile and remembered allow decisions, active handoff marker, staged Commit records, and revision information in Chrome extension storage. This lets it restore runtime policy and safety barriers after service-worker restart. Chrome may sync or back up browser-profile data according to the user's browser/account configuration; AgentTab does not initiate a telemetry upload. ## Explicit network paths diff --git a/docs/verification.md b/docs/verification.md index 325afbc..341463e 100644 --- a/docs/verification.md +++ b/docs/verification.md @@ -22,22 +22,22 @@ cargo test --workspace --locked --manifest-path host-rs/Cargo.toml cargo build --release --locked --manifest-path host-rs/Cargo.toml -p agenttab-host ``` -The workspace checks TypeScript adapters, extension code, installer, OMP adapter, and package builds. The architecture gates cover manifest identity, required and optional permission behavior, RPC schemas, forbidden legacy surface, and Rust IPC framing. They do not operate a real signed-in browser. +The workspace checks TypeScript adapters, extension code, installer, OMP adapter, and package builds. The architecture gates cover manifest identity and permission behavior, RPC schemas, forbidden legacy surface, and Rust IPC framing. They do not operate a real signed-in browser. ## Live browser evidence Use a disposable Chrome profile and a disposable test account. Reload the unpacked extension through Chrome's extension UI, then observe the actual surface after every UI action. Exercise: -1. required `debugger` availability plus optional `scripting` grant and revocation from the popup; +1. required `debugger` and `scripting` availability, with Pause and Resume leaving both grants unchanged; 2. host handshake and reconciliation to ready; 3. create and adopt-active task tabs, child popup inheritance, visible grouping, and ownership revocation after an ungroup or move; 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. +8. Autopilot direct execution on controlled fixtures; Review selected and Strict staging recognizable controls without side effect; remembered task/site/effect approval; 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. +Never exercise a real send, purchase, delete, permission grant, or upload against a live account merely to prove policy behavior. Use controlled fixtures; select a review profile and stop at the staged preview for live authenticated checks. ## Platform evidence diff --git a/host-rs/crates/agenttab-host/src/guardrails.rs b/host-rs/crates/agenttab-host/src/guardrails.rs index fcf1c1e..8b11a5a 100644 --- a/host-rs/crates/agenttab-host/src/guardrails.rs +++ b/host-rs/crates/agenttab-host/src/guardrails.rs @@ -418,7 +418,9 @@ impl Guardrails { "upload_file_not_allowed", "Upload file is outside AgentTab policy dlp_allowed_roots", ) - .with_recovery("Add a narrow user-owned directory to dlp_allowed_roots.")); + .with_recovery( + "Run agenttab policy allow-upload PATH once, then restart the AgentTab host.", + )); } let mut options = OpenOptions::new(); options.read(true); diff --git a/packages/extension/scripts/build.ts b/packages/extension/scripts/build.ts index 71b14fe..83fc0e7 100644 --- a/packages/extension/scripts/build.ts +++ b/packages/extension/scripts/build.ts @@ -85,8 +85,7 @@ if (channel === "development") { } delete manifest.key; } -const required = ["nativeMessaging", "debugger", "tabs", "tabGroups", "storage", "alarms"]; -const optional = ["scripting"]; +const required = ["nativeMessaging", "debugger", "tabs", "tabGroups", "storage", "alarms", "scripting"]; const forbiddenKeys = ["content_scripts", "web_accessible_resources", "externally_connectable", "side_panel", "commands"]; for (const key of forbiddenKeys) { if (key in manifest) throw new Error(`Forbidden manifest surface: ${key}`); @@ -94,7 +93,7 @@ for (const key of forbiddenKeys) { if (JSON.stringify(manifest.permissions) !== JSON.stringify(required)) { throw new Error(`Required permission drift: ${JSON.stringify(manifest.permissions)}`); } -if (JSON.stringify(manifest.optional_permissions) !== JSON.stringify(optional)) { +if ("optional_permissions" in manifest) { throw new Error(`Optional permission drift: ${JSON.stringify(manifest.optional_permissions)}`); } if (JSON.stringify(manifest.host_permissions) !== JSON.stringify([""])) { diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index c860ecf..e3e0884 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -21,7 +21,8 @@ import { } from "./routes"; import { MutationScheduler, NotStartedError } from "./scheduler"; import { IdempotentStartup, StartupOperationQueue } from "./startup"; -import { mutateState, readState } from "./storage"; +import { POLICY_PROFILES, mutateState, readState, type PolicyProfile } from "./storage"; +import type { PolicyRememberScope } from "./policy"; import { isRecord } from "./type-guards"; const RUNTIME_INSTANCE_ID = crypto.randomUUID(); @@ -116,9 +117,9 @@ async function automationEnabled(): Promise { } function automationRequired(): Error { - return Object.assign(new Error("AgentTab automation permissions have not been enabled"), { + return Object.assign(new Error("AgentTab required browser permissions are unavailable"), { code: "permissions_required", - recovery: "Open the AgentTab popup and choose Enable automation.", + recovery: "Reload or reinstall AgentTab so Chrome restores its required permissions.", }); } @@ -676,6 +677,8 @@ async function handlePopupMessage(message: Record): Promise ({ @@ -693,9 +696,31 @@ async function handlePopupMessage(message: Record): Promise { + state.policyProfile = profile; + }); + return { profile }; + } + if (message.kind === "clear_policy_allowances") { + const cleared = await mutateState((state) => { + const count = Object.keys(state.policyAllowances).length; + state.policyAllowances = {}; + return count; + }); + return { cleared }; + } if (message.kind === "set_pointer" && typeof message.enabled === "boolean") { const enabled = message.enabled; await mutateState((state) => { @@ -731,11 +756,33 @@ async function handlePopupMessage(message: Record): Promise; dialog?: PreparedDialog; } @@ -446,7 +455,7 @@ export class StandardBrowserRuntime { }); } await this.revisions.assertExpected(tabId, pageRevision); - const stagedConsequence = await this.consequence(tabId, pageRevision, action); + const stagedConsequence = await this.consequence(taskId, tabId, pageRevision, action); if (stagedConsequence) { const staged: StagedCommit = { native_token: randomToken(), @@ -465,6 +474,8 @@ export class StandardBrowserRuntime { action: { action }, preview: { effect: stagedConsequence.effect, + policy_effect: stagedConsequence.policyEffect, + ...(stagedConsequence.origin === undefined ? {} : { origin: stagedConsequence.origin }), kind: action.kind, target: stagedConsequence.target, ...(typeof action.ref === "string" ? { ref: action.ref } : {}), @@ -536,7 +547,13 @@ export class StandardBrowserRuntime { return { review_bound: true }; } - async reviewBinding(reviewHandle: string): Promise<{ task_id: string; tab_id: number }> { + async reviewBinding(reviewHandle: string): Promise<{ + task_id: string; + tab_id: number; + effect: string; + policy_effect?: PolicyEffect; + origin?: string; + }> { const staged = Object.values((await readState()).stagedCommits).find( (candidate) => candidate.review_handle === reviewHandle && candidate.approved !== true, ); @@ -545,15 +562,42 @@ export class StandardBrowserRuntime { code: "invalid_staged_token", }); } - return { task_id: staged.task_id, tab_id: staged.tab_id }; + return { + task_id: staged.task_id, + tab_id: staged.tab_id, + effect: staged.effect, + ...(POLICY_EFFECTS.includes(staged.preview.policy_effect as PolicyEffect) + ? { policy_effect: staged.preview.policy_effect as PolicyEffect } + : {}), + ...(typeof staged.preview.origin === "string" ? { origin: staged.preview.origin } : {}), + }; } - async approveReview(reviewHandle: string): Promise { + async approveReview( + reviewHandle: string, + rememberScope: PolicyRememberScope = "once", + ): Promise { return mutateState((state) => { const staged = Object.values(state.stagedCommits).find( (candidate) => candidate.review_handle === reviewHandle && candidate.approved !== true, ); if (!staged) return false; + if (rememberScope !== "once") { + const effect = staged.preview.policy_effect; + if (!POLICY_EFFECTS.includes(effect as PolicyEffect)) { + throw Object.assign( + new Error("This staged action cannot create a remembered policy decision"), + { code: "policy_scope_unavailable" }, + ); + } + rememberPolicyAllowance( + state, + rememberScope, + staged.task_id, + typeof staged.preview.origin === "string" ? staged.preview.origin : undefined, + effect as PolicyEffect, + ); + } staged.approved = true; return true; }); @@ -952,28 +996,61 @@ export class StandardBrowserRuntime { private async consequence( + taskId: string, tabId: number, pageRevision: number, action: Record, ): Promise { + if ( + action.kind !== "close" && + action.kind !== "upload_file" && + action.kind !== "click" && + action.kind !== "select" && + action.kind !== "fill" && + action.kind !== "type" && + !(action.kind === "dialog" && action.decision === "accept") + ) { + return null; + } + const state = await readState(); + // Autopilot inserts no semantic Commit stage. Keep a preflight descriptor + // only for value-entry controls so sensitive fields fail before dispatch; + // clicks and structural actions take the default fast path. + if ( + state.policyProfile === "autopilot" && + action.kind !== "fill" && + action.kind !== "type" && + action.kind !== "select" + ) return null; + const tab = await chrome.tabs.get(tabId); + const origin = policyOrigin(tab.url ?? tab.pendingUrl); if (action.kind === "close") { + if (!policyRequiresReview(state, taskId, origin, "owned_tab_close")) return null; return { effect: "Close an AgentTab-owned browser tab", + policyEffect: "owned_tab_close", + ...(origin === undefined ? {} : { origin }), target: { kind: action.kind }, }; } if (action.kind === "upload_file") { const count = Array.isArray(action.files) ? action.files.length : 0; + if (!policyRequiresReview(state, taskId, origin, "upload")) return null; return { effect: `Upload ${count} ${count === 1 ? "file" : "files"} to the page`, + policyEffect: "upload", + ...(origin === undefined ? {} : { origin }), target: typeof action.ref === "string" ? await this.targetDescriptor(tabId, pageRevision, action.ref) : { kind: action.kind }, }; } if (action.kind === "dialog" && action.decision === "accept") { + if (!policyRequiresReview(state, taskId, origin, "dialog_accept")) return null; return { effect: "Accept a browser confirmation dialog", + policyEffect: "dialog_accept", + ...(origin === undefined ? {} : { origin }), target: { kind: action.kind }, dialog: await this.stageDialog(tabId), }; @@ -987,6 +1064,7 @@ export class StandardBrowserRuntime { return null; } const target = await this.targetDescriptor(tabId, pageRevision, action.ref, action); + if (state.policyProfile === "autopilot") return null; const label = [ target.role, target.text, @@ -1004,13 +1082,12 @@ export class StandardBrowserRuntime { // immediately consequential selection even when its submitted value is opaque. target.requested_option_label, ].filter((value): value is string => typeof value === "string").join(" ").replace(/\s+/g, " ").trim(); - if ( - /\b(buy|purchase|pay|send|transfer|delete|remove|publish|post|deploy|merge|approve|authorize|grant|revoke|unsubscribe|cancel subscription|place order|checkout|submit order|confirm order|permission)\b/i.test( - label, - ) - ) { + const policyEffect = classifyControlEffect(label); + if (policyEffect && policyRequiresReview(state, taskId, origin, policyEffect)) { return { effect: `${action.kind === "click" ? "Activate" : "Change"} consequential control: ${label.slice(0, 160)}`, + policyEffect, + ...(origin === undefined ? {} : { origin }), target, }; } @@ -1155,9 +1232,8 @@ export class StandardBrowserRuntime { } if (kind === "dialog") { if (action.decision === "accept") { - throw Object.assign(new Error("Accepting a dialog requires a staged Commit"), { - code: "invalid_request", - }); + await this.send(tabId, "Page.handleJavaScriptDialog", { accept: true }); + return { kind, completed: true }; } await this.send(tabId, "Page.handleJavaScriptDialog", { accept: false }); return { kind, completed: true }; diff --git a/packages/extension/src/manifest.json b/packages/extension/src/manifest.json index 1b7f472..a46dc5a 100644 --- a/packages/extension/src/manifest.json +++ b/packages/extension/src/manifest.json @@ -12,9 +12,7 @@ "tabs", "tabGroups", "storage", - "alarms" - ], - "optional_permissions": [ + "alarms", "scripting" ], "host_permissions": [ diff --git a/packages/extension/src/ownership.ts b/packages/extension/src/ownership.ts index 75e7513..10c1d14 100644 --- a/packages/extension/src/ownership.ts +++ b/packages/extension/src/ownership.ts @@ -418,6 +418,11 @@ export class OwnershipLedger { for (const [token, staged] of Object.entries(state.stagedCommits)) { if (staged.task_id === taskId) delete state.stagedCommits[token]; } + for (const [key, allowance] of Object.entries(state.policyAllowances)) { + if (allowance.scope === "task" && allowance.taskId === taskId) { + delete state.policyAllowances[key]; + } + } return ownedTabIds; }); for (const tabId of tabIds) await this.revisions.remove(tabId); diff --git a/packages/extension/src/policy.ts b/packages/extension/src/policy.ts new file mode 100644 index 0000000..dfc40a6 --- /dev/null +++ b/packages/extension/src/policy.ts @@ -0,0 +1,90 @@ +import { + policyAllowanceKey, + type ExtensionState, + type PolicyAllowance, + type PolicyAllowanceScope, + type PolicyEffect, +} from "./storage"; + +export type PolicyRememberScope = "once" | PolicyAllowanceScope; + +const EFFECT_PATTERNS: ReadonlyArray = [ + // Financial wins when a control has multiple verbs (for example, + // "Send payment"), so a communication allowance cannot authorize money movement. + ["financial", /\b(buy|purchase|pay|payment|transfer|place order|checkout|submit order|confirm order)\b/i], + ["destructive", /\b(delete|remove|revoke|unsubscribe|cancel subscription)\b/i], + ["authorization", /\b(approve|authorize|grant|permission)\b/i], + ["external_communication", /\b(send|publish|post|deploy|merge)\b/i], +]; + +export function classifyControlEffect(label: string): PolicyEffect | null { + for (const [effect, pattern] of EFFECT_PATTERNS) { + if (pattern.test(label)) return effect; + } + return null; +} + +export function policyOrigin(rawUrl: string | undefined): string | undefined { + if (!rawUrl) return undefined; + try { + const url = new URL(rawUrl); + return url.protocol === "http:" || url.protocol === "https:" ? url.origin : undefined; + } catch { + return undefined; + } +} + +function hasAllowance( + state: ExtensionState, + taskId: string, + origin: string | undefined, + effect: PolicyEffect, +): boolean { + return ( + Object.hasOwn(state.policyAllowances, policyAllowanceKey("task", effect, taskId)) || + (origin !== undefined && + Object.hasOwn(state.policyAllowances, policyAllowanceKey("domain", effect, undefined, origin))) || + Object.hasOwn(state.policyAllowances, policyAllowanceKey("effect", effect)) + ); +} + +export function policyRequiresReview( + state: ExtensionState, + taskId: string, + origin: string | undefined, + effect: PolicyEffect, +): boolean { + if (state.policyProfile === "autopilot") return false; + if (hasAllowance(state, taskId, origin, effect)) return false; + return state.policyProfile === "strict" || effect !== "owned_tab_close"; +} + +export function rememberPolicyAllowance( + state: ExtensionState, + scope: Exclude, + taskId: string, + origin: string | undefined, + effect: PolicyEffect, + createdAt = Date.now(), +): PolicyAllowance { + if (scope === "domain" && origin === undefined) { + throw Object.assign(new Error("This review has no HTTP or HTTPS site to remember"), { + code: "policy_scope_unavailable", + }); + } + const allowance: PolicyAllowance = { + scope, + effect, + createdAt, + ...(scope === "task" ? { taskId } : {}), + ...(scope === "domain" ? { origin } : {}), + }; + const key = policyAllowanceKey(scope, effect, allowance.taskId, allowance.origin); + if (!Object.hasOwn(state.policyAllowances, key) && Object.keys(state.policyAllowances).length >= 256) { + const oldest = Object.entries(state.policyAllowances) + .sort(([, left], [, right]) => left.createdAt - right.createdAt)[0]; + if (oldest) delete state.policyAllowances[oldest[0]]; + } + state.policyAllowances[key] = allowance; + return allowance; +} diff --git a/packages/extension/src/popup.css b/packages/extension/src/popup.css index 8bfbba4..32b4459 100644 --- a/packages/extension/src/popup.css +++ b/packages/extension/src/popup.css @@ -165,12 +165,22 @@ button:disabled { opacity: .45; cursor: default; } button:disabled:hover { background: var(--violet-fill); } button.quiet:disabled:hover { background: var(--surface-lift); } -body[data-busy] button, body[data-busy] input { pointer-events: none; opacity: .55; } +body[data-busy] button, body[data-busy] input, body[data-busy] select { pointer-events: none; opacity: .55; } .actions { display: flex; justify-content: flex-end; gap: var(--step-1); margin-top: var(--step-3); } input[type="checkbox"] { flex: none; width: 17px; height: 17px; margin: 0; accent-color: var(--violet); } +select { + min-width: 104px; + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 6px 8px; + background: var(--surface-lift); + color: var(--text); + font: inherit; +} + :focus-visible { outline: 2px solid var(--violet-bright); outline-offset: 2px; } /* your turn */ @@ -196,7 +206,8 @@ input[type="checkbox"] { flex: none; width: 17px; height: 17px; margin: 0; accen border-top: 1px solid var(--line-soft); } .review:first-child { padding-top: 0; border-top: 0; } -.review-actions { display: flex; justify-content: flex-end; gap: var(--step-1); } +.review-actions { display: flex; align-items: center; justify-content: flex-end; gap: var(--step-1); } +.review-scope { flex: 1; min-width: 0; } /* tasks */ .tasks { diff --git a/packages/extension/src/popup.html b/packages/extension/src/popup.html index 1e76e90..069348a 100644 --- a/packages/extension/src/popup.html +++ b/packages/extension/src/popup.html @@ -36,18 +36,6 @@

Review before action

- -
@@ -80,12 +68,23 @@

Task groups

Settings -
@@ -128,7 +128,7 @@

Let the agent do the routine work

Take your turn when it matters

-

Human handoff protects secret and other human-only input. Recognizable consequential actions wait for Commit.

+

Human handoff always protects secret and other human-only input. Your selected action policy decides when recognizable effects wait for Commit.

@@ -146,7 +146,7 @@

What AgentTab does

  • Runs locally beside your signed-in Chrome profile.
  • Coordinates task-owned browser workspaces and visible handoff.
  • -
  • Stages recognizable consequential actions before they take effect.
  • +
  • Lets you choose unattended Autopilot or Commit review profiles.
@@ -159,7 +159,7 @@

What task ownership does not do

The remaining risk is real

-

Web pages can contain prompt injection or attach an external effect to an innocent-looking control. Commit is a best-effort review barrier for recognizable actions, not a guarantee that every external effect is recognized. Review the page and the staged action before you proceed.

+

Web pages can contain prompt injection or attach an external effect to an innocent-looking control. Commit is a best-effort review barrier when a review profile is selected, not a guarantee that every external effect is recognized. Autopilot deliberately removes that review barrier.

diff --git a/packages/site/src/privacy/index.html b/packages/site/src/privacy/index.html index e5f186b..010140f 100644 --- a/packages/site/src/privacy/index.html +++ b/packages/site/src/privacy/index.html @@ -76,12 +76,12 @@

When an agent uses a website

Security boundaries and limits

Task ownership limits agent execution and coordination to visible task workspaces. It is not cookie, identity, or browser-profile isolation. Standard mode does not provide raw cookies, storage, passwords, arbitrary JavaScript, raw Chrome DevTools Protocol access, or coordinate actions.

For passwords, passkeys, 2FA, CAPTCHAs, payment secrets, and other human-only input, AgentTab uses Your Turn handoff. During handoff, the agent is not allowed to observe or capture that interaction.

-

Commit stages recognizable sends, publishing, purchases, deletion, uploads, authorization, and permission grants for review. This is a best-effort safety measure. Prompt injection and page behavior can still create risks, and Commit cannot guarantee recognition of every external effect.

+

The action policy controls Commit. Autopilot executes recognized effects directly; Review selected and Strict stage recognizable sends, publishing, purchases, deletion, uploads, authorization, and permission grants for review. This is a best-effort measure, and Commit cannot guarantee recognition of every external effect.

diff --git a/tests/architecture/verify_permissions.py b/tests/architecture/verify_permissions.py index 631d4f1..e7f180b 100755 --- a/tests/architecture/verify_permissions.py +++ b/tests/architecture/verify_permissions.py @@ -2,20 +2,19 @@ """Verify AgentTab's offline permission decision and explicit PR3 live lifecycle. Without ``--live-lifecycle`` this remains the PR1 decision gate: it validates -the canonical required/optional split and a normalized copy in memory. It +the canonical required permission set and a normalized copy in memory. It neither writes extension files, opens Chrome, reloads an extension, nor changes Chrome permissions. The explicit live mode runs only against a preloaded AgentTab candidate on the -trusted macOS runner. It observes permission changes made by a human through -the AgentTab UI; this probe never calls ``chrome.permissions.request`` or -``chrome.permissions.remove``. Probe-initiated mutations are local fixture HTTP +trusted macOS runner. Required browser permissions remain installed while the +human exercises logical Pause and Resume; this probe never calls +``chrome.permissions.request`` or ``chrome.permissions.remove``. Probe-initiated mutations are local fixture HTTP requests, task-owned tabs created through AgentTab Core RPC, a transient DevTools download route into the explicit test directory, a background AgentTab control page recreated after deliberate extension reloads, and the reviewed extension/host restart commands supplied by the operator. Cleanup closes -task-owned tabs, restores the original optional-scripting state through the -human UI checkpoint, and removes only the exact fixture download. +task-owned tabs and removes only the exact fixture download. """ from __future__ import annotations @@ -50,12 +49,13 @@ REQUIRED_PERMISSIONS = ( "nativeMessaging", "debugger", + "scripting", "tabs", "tabGroups", "storage", "alarms", ) -OPTIONAL_PERMISSIONS = ("scripting",) +OPTIONAL_PERMISSIONS: tuple[str, ...] = () REMOVED_PERMISSIONS = ( "activeTab", "bookmarks", @@ -106,7 +106,10 @@ def string_set(manifest: dict[str, Any], key: str) -> set[str]: def reduced_manifest(source: dict[str, Any]) -> dict[str, Any]: staged = json.loads(json.dumps(source)) staged["permissions"] = list(REQUIRED_PERMISSIONS) - staged["optional_permissions"] = list(OPTIONAL_PERMISSIONS) + if OPTIONAL_PERMISSIONS: + staged["optional_permissions"] = list(OPTIONAL_PERMISSIONS) + else: + staged.pop("optional_permissions", None) staged["host_permissions"] = list(HOST_PERMISSIONS) return staged @@ -116,7 +119,7 @@ def canonical_bytes(value: dict[str, Any]) -> bytes: def verify_extension_permission_contract() -> None: - """Check the canonical Automation source cannot dynamically toggle debugger.""" + """Check required browser capabilities are not used as a routine UI toggle.""" background = manifest_bytes(EXTENSION_SOURCE / "background.ts").decode("utf-8") for required in ( "automationEnabled", @@ -133,14 +136,11 @@ def verify_extension_permission_contract() -> None: raise GateFailure("background must not dynamically request or remove debugger") popup = manifest_bytes(EXTENSION_SOURCE / "popup.ts").decode("utf-8") - for required in ( - "changeOptionalScriptingPermission", - 'permissions: ["scripting"]', - "optional scripting", - "install-time debugger grant stays installed", - ): - if required not in popup: - raise GateFailure(f"popup permission disclosure omits {required!r}") + scripting_mutation = re.compile( + r"permissions\.(?:request|remove)\s*\(\s*\{\s*permissions\s*:\s*\[\s*['\"]scripting['\"]" + ) + if scripting_mutation.search(popup): + raise GateFailure("popup must not request or remove required scripting access") if debugger_mutation.search(popup): raise GateFailure("popup must not request or remove debugger") @@ -161,6 +161,8 @@ def verify() -> dict[str, Any]: raise GateFailure("source manifest required permissions differ from the ADR target") if source_optional != set(OPTIONAL_PERMISSIONS): raise GateFailure("source manifest optional permissions differ from the ADR target") + if not OPTIONAL_PERMISSIONS and "optional_permissions" in source: + raise GateFailure("source manifest must omit the empty optional_permissions surface") if source_host != set(HOST_PERMISSIONS): raise GateFailure("source manifest host permissions differ from the ADR target") @@ -191,7 +193,7 @@ def verify() -> dict[str, Any]: "host_permissions": list(HOST_PERMISSIONS), "removed_permissions": list(REMOVED_PERMISSIONS), "debugger_required_at_install": True, - "scripting_optional_at_runtime": True, + "scripting_required_at_install": True, "active_tab_retained": False, "source_manifest_sha256": hashlib.sha256(source_bytes).hexdigest(), "staged_manifest_sha256": hashlib.sha256(staged_bytes).hexdigest(), @@ -832,13 +834,6 @@ def debugger_is_attached(self, tab_id: int) -> bool: raise ChromeOperationFailure("chrome.debugger.getTargets unavailable while required debugger is granted") return tab_id in state["debugger_attached_tab_ids"] - def assert_no_debugger_attachments(self, operation: str) -> None: - state = self.state() - if state["debugger_required_granted"] and not state["debugger_targets_available"]: - raise ChromeOperationFailure(f"{operation}: chrome.debugger.getTargets unavailable") - if state["debugger_attached_tab_ids"]: - raise ChromeOperationFailure(f"{operation}: scripting is off but AgentTab retained debugger attachments") - def task_ids(self) -> set[str]: return set(self.state()["task_ids"]) @@ -852,16 +847,6 @@ def runtime_instance(self) -> str: raise ChromeOperationFailure("AgentTab candidate omitted its private runtime instance") return value - def automation_revocation_generation(self) -> int: - result = self._evaluate( - "chrome.runtime.sendMessage({ kind: 'automation_revocation_state' })", - "candidate-automation-revocation-state", - ) - value = result.get("generation") - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise ChromeOperationFailure("AgentTab candidate omitted its private revocation generation") - return value - def assert_candidate_files(self, file_digests: dict[str, str]) -> None: encoded = json.dumps(file_digests, separators=(",", ":"), sort_keys=True) result = self._evaluate( @@ -1185,6 +1170,8 @@ def validate_candidate(candidate_dir: Path) -> tuple[str, str, dict[str, str], s raise GateFailure("candidate manifest required permissions differ from the locked target") if string_set(manifest, "optional_permissions") != set(OPTIONAL_PERMISSIONS): raise GateFailure("candidate manifest optional permissions differ from the locked target") + if not OPTIONAL_PERMISSIONS and "optional_permissions" in manifest: + raise GateFailure("candidate manifest must omit the empty optional_permissions surface") if string_set(manifest, "host_permissions") != set(HOST_PERMISSIONS): raise GateFailure("candidate manifest host permissions differ from the locked target") @@ -1238,7 +1225,6 @@ def __init__(self, args: argparse.Namespace, candidate_file_digests: dict[str, s self.candidate_file_digests = candidate_file_digests self.owned_tabs: set[int] = set() self.replay_request: tuple[dict[str, Any], str] | None = None - self.initial_automation_permissions: bool | None = None def prompt(self, phase: str, instruction: str) -> None: if not self.args.interactive or not sys.stdin.isatty(): @@ -1341,23 +1327,6 @@ def act( result = require_completed(operation, response) return result, key - def assert_scripting_denied(self, tab_id: int, operation: str) -> None: - response, _ = self.client.call("browser_snapshot", {"tab_id": tab_id, "mode": "text"}) - require_denied( - operation, - response, - allowed_codes={ - "automation_disabled", - "permissions_required", - "scripting_not_granted", - "scripting_permission_required", - "scripting_required", - }, - ) - error = response.get("error") - if not isinstance(error, dict) or not isinstance(error.get("recovery"), str) or not error["recovery"]: - raise GateFailure(f"{operation}: scripting denial omitted enablement recovery") - def assert_raw_cdp_denied(self, tab_id: int) -> None: response, _ = self.client.call( "browser_developer", @@ -1572,35 +1541,12 @@ def run_one(self, run_number: int, fixture: LifecycleFixture) -> dict[str, Any]: run_start_permissions = initial_state["scripting"] if initial_state["debugger_required_granted"] is not True: raise ChromeOperationFailure("AgentTab's required debugger install grant is unavailable") - if self.initial_automation_permissions is None: - self.initial_automation_permissions = run_start_permissions - if run_start_permissions: - denial_instruction = ( - "Open AgentTab Settings and choose Turn off under Automation access. " - "The popup must return to Automation is off." - ) - else: - denial_instruction = "Leave AgentTab at Automation is off; no Chrome permission prompt is expected." - self.prompt(f"run {run_number}: automation denial state", denial_instruction) - self.inspector.assert_permission_model(False, "denial observation") + if run_start_permissions is not True: + raise ChromeOperationFailure("AgentTab's required scripting install grant is unavailable") + self.inspector.assert_permission_model(True, "required install grants") self.connect() self.status("initial lifecycle status") selection_before = self.inspector.selection() - response, _ = self.client.call( - "browser_open", - {"mode": "create", "url": fixture.base_url, "background": True}, - mutation=True, - ) - require_denied("browser_open while scripting denied", response, {"permissions_required"}) - self.inspector.assert_selection_unchanged(selection_before, "browser_open permission denial") - self.inspector.assert_no_debugger_attachments("initial automation denial") - - self.prompt( - f"run {run_number}: optional automation grant", - "Choose Enable AgentTab automation. Chrome grants only optional scripting; the required debugger grant remains installed.", - ) - self.inspector.assert_permission_model(True, "grant observation") - selection_before = self.inspector.selection() tab_id, _ = self.open_tab(fixture.base_url, "browser_open background task") self.inspector.assert_selection_unchanged(selection_before, "browser_open background task") self.assert_raw_cdp_denied(tab_id) @@ -1635,29 +1581,6 @@ def run_one(self, run_number: int, fixture: LifecycleFixture) -> dict[str, Any]: self.inspector.assert_selection_unchanged(selection_before_host_restart, "Rust host crash recovery") self.assert_handoff(fixture, run_number) - self.snapshot(tab_id, "accessibility", "browser_snapshot debugger revocation source") - if not self.inspector.debugger_is_attached(tab_id): - raise ChromeOperationFailure("debugger revocation source tab was not attached") - revocation_generation = self.inspector.automation_revocation_generation() - self.prompt( - f"run {run_number}: optional automation revocation", - "Open AgentTab Settings and choose Turn off under Automation access. This removes optional scripting only.", - ) - self.inspector.assert_permission_model(False, "revocation observation") - detach_deadline = time.monotonic() + self.args.timeout_seconds - while self.inspector.automation_revocation_generation() <= revocation_generation: - if time.monotonic() >= detach_deadline: - raise ChromeOperationFailure("automation revocation did not acknowledge debugger-session cleanup") - time.sleep(0.1) - self.inspector.assert_no_debugger_attachments("automation revocation") - self.assert_scripting_denied(tab_id, "browser_snapshot after automation revocation") - self.prompt( - f"run {run_number}: optional automation re-grant", - "Choose Enable AgentTab automation. This restores optional scripting only.", - ) - self.inspector.assert_permission_model(True, "re-grant observation") - self.snapshot(tab_id, "accessibility", "browser_snapshot after automation re-grant") - self.prompt( f"run {run_number}: pause", "Use the AgentTab popup's Pause agents control and wait for its confirmed PAUSED state.", @@ -1717,8 +1640,8 @@ def run_one(self, run_number: int, fixture: LifecycleFixture) -> dict[str, Any]: "run": run_number, "task_cleanup": True, "active_window_tab_preserved": True, - "automation_permission_lifecycle_observed": True, - "debugger_detach_on_revocation_observed": True, + "required_permission_continuity_observed": True, + "pause_without_permission_revocation_observed": True, "raw_cdp_standard_denied": True, "handoff_blackout_observed": True, "crash_recovery_observed": True, @@ -1736,22 +1659,6 @@ def restore_and_cleanup(self, fixture_filename: str | None) -> None: self.close_owned_tabs() if fixture_filename: self.remove_fixture_download(fixture_filename) - if self.initial_automation_permissions is not None: - expected = "granted" if self.initial_automation_permissions else "not granted" - action = ( - "choose Enable AgentTab automation" - if self.initial_automation_permissions - else "open AgentTab Settings and choose Turn off under Automation access" - ) - self.prompt( - "transactional restoration", - f"Restore AgentTab automation permissions to their state before this run ({expected}): {action}. " - "Ensure the extension remains enabled, agents are resumed, and any active handoff is complete.", - ) - self.inspector.assert_permission_model( - self.initial_automation_permissions, - "transactional restoration", - ) finally: self.client.close() self.inspector.close() @@ -1815,7 +1722,7 @@ def print_live_prerequisites() -> None: print(" 2. Chrome was started with a loopback remote-debugging endpoint and an AgentTab extension page is open.") print(" 3. The AgentTab Rust host is READY on a current-user 0600 Unix socket; no TCP token endpoint is accepted.") print(" 4. --download-dir already exists and is disposable; the probe temporarily routes Chrome downloads there.") - print(" 5. An operator can perform the prompted AgentTab/Chrome UI permission, pause, disable, and restore steps.") + print(" 5. An operator can perform the prompted AgentTab Pause, Resume, disable, and restore steps.") print(" 6. --host-restart-command is the runner's reviewed Rust-host restart argv encoded as JSON; it is never shell-evaluated.") print("") print("CLI (run only after the prerequisites are deliberately prepared):") @@ -1843,7 +1750,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--report", type=Path, help="write the scrubbed JSON report") parser.add_argument("--print-live-prerequisites", action="store_true", help="print live CLI/environment prerequisites and exit") parser.add_argument("--live-lifecycle", action="store_true", help="run the explicit PR3 macOS lifecycle matrix") - parser.add_argument("--interactive", action="store_true", help="allow prompts for human Chrome UI permission/state changes") + parser.add_argument("--interactive", action="store_true", help="allow prompts for human Chrome UI lifecycle changes") parser.add_argument( "--request-extension-reload", action="store_true",