# Local browser agents The administrator enables `policy.agents.enabled`, chooses action-name scopes, and optionally restricts `allowedHosts`. Empty hosts inherits browser destination policy. Each native tab has a stable UUID for its lifetime. Obtain these IDs with `sessions`; an omitted sessionId captures the foreground tab once and pins the operation to it. Closed IDs are invalid. The separately approved `screenshot` action exports an inspected foreground webpage viewport under the limits below. ## Windows pairing and CLI The browser creates `agent-connection.json` in its Windows user-data directory. It contains `format: windows-dpapi-v1` and a DPAPI CurrentUser-protected connection capability. The trusted shell's Agent connection control provides its path; remote pages never receive the token. Pairing grants access to trusted same-user local processes. It does not authenticate separate named applications or resist a compromised Windows account. Protect the profile and installation using endpoint management. Create a local JSON request file containing: ```json {"action":"read","args":{}} ``` Run the packaged helper while the managed browser is running: ```powershell & 'C:/path/to/SafetyNet.exe' --agent-connection='C:/absolute/profile/agent-connection.json' --agent-action='C:/absolute/request.json' ``` Development equivalent from the repository: ```powershell ./node_modules/electron/dist/electron.exe scripts/agent-client.cjs --connection 'C:/absolute/profile/agent-connection.json' --request 'C:/absolute/request.json' ``` The helper uses a separate profile, decrypts the descriptor with Windows DPAPI, verifies literal loopback, and uses the authenticated post-quantum transport helper. It follows no redirects and sets a deadline. Results go to stdout; failures exit with code 1. Protect/delete request files: prompts and typed text in those files are plaintext. A fresh capability is generated at startup; central revocation disables the current capability. A descriptor surviving a crash can point to an inactive endpoint and must not be treated as an authenticated response source without a running trusted browser. ## Actions Use `agent-transport-client.cjs` or the trusted Electron helper. The generated numeric-loopback endpoint requires a fresh HMAC-SHA384 proof bound to the exact body, route, timestamp and nonce; the capability token never travels on the wire. A pinned Ed25519/ML-DSA-87 signed one-use challenge establishes ML-KEM-1024/AES-256-GCM protection for action and reply. Plaintext successful replies and plaintext action envelopes are rejected. Browser Origin/Fetch Metadata requests are rejected. No CORS, CDP, arbitrary JavaScript, cookie export or arbitrary filesystem API is exposed. | Action | Arguments | Policy scope | Internal scope | Result | |---|---|---|---|---| | sessions | none | sessions | sessions:list | `[{id,title,foreground}]` | | sessions:save | name, sessionIds, optional id and expectedRevision | sessions:save | sessions:save | owned collection ID, revision, tabCount | | sessions:list | none | sessions:list | sessions:list | revision and DLP-inspected owned collections | | sessions:restore | id OR planId | sessions:restore | sessions:restore | one-use plan, then restored native tab IDs | | vault:status | optional sessionId | vault:status | vault:status | bounded exists/locked/enabled/autofill flags | | vault:list | optional sessionId | vault:list | vault:list | approved origins and one-use credential references | | vault:fill | sessionId, credentialRef | vault:fill | vault:fill | `{completed}`; does not authorize submission | | vault:prepare-submit | sessionId, credentialRef | vault:prepare-submit | vault:prepare-submit | `{completed,prepared}`; never submits the form | | screenshot | optional sessionId | screenshot | browser:screenshot | `{mime:"image/png",width,height,data}`; canonical bounded base64 PNG | | navigate | sessionId, url | navigate | browser:navigate | `{completed}` | | back | sessionId | back | browser:navigate | `{completed}` | | forward | sessionId | forward | browser:navigate | `{completed}` | | reload | sessionId | reload | browser:navigate | `{completed}` | | read | sessionId | read | browser:read | `{text}` | | inspect | sessionId | inspect | browser:read | sanitized controls and non-sensitive values | | scroll | sessionId, x, y | scroll | browser:interact | `{completed}` | | click | sessionId, selector | click | browser:interact | `{completed}` | | type | sessionId, selector, text | type | browser:interact | `{completed}` | | select | sessionId, selector, value | select | browser:interact | `{completed}` | | check | sessionId, selector, checked | check | browser:interact | `{completed}` | | ai | sessionId, prompt | ai | ai:request | `{text}` | | application:state | none | application:state | application:state | ready, policyVersion, activeTabId, tabCount | | settings:get | none | settings:get | settings:get | visible, hiddenSections, lockedSections, appearance | | settings:update | appearance | settings:update | settings:update | current managed settings | | bookmarks:list | none | bookmarks:list | bookmarks:list | managed and personal bookmarks, revision and permissions | | bookmarks:mutate | action, bookmark/id, expectedRevision, policyVersion | bookmarks:mutate | bookmarks:mutate | `{completed}` | | tabs:create | optional group, optional HTTP(S) url | tabs:create | tabs:create | `{completed,sessionId}` | | tabs:activate | sessionId | tabs:activate | tabs:activate | `{completed,sessionId}` | | tabs:close | sessionId | tabs:close | tabs:close | `{completed}` | | tabs:group | sessionId, group | tabs:group | tabs:group | `{completed,sessionId}` | Application control scopes are disabled by default and must be explicitly published by an administrator. They provide fixed operations on the same privileged helpers used by the browser UI. `settings:update` accepts only `appearance.mode` (`system`, `light`, `dark`), `appearance.accent` (`blue`, `teal`, `violet`) and `appearance.tabMode` (`separate`, `compact`). Hidden or locked settings remain enforced. Agents cannot edit signed security policy, enrollment, credentials or management keys. Bookmark changes target personal bookmarks only and require the current signed policy version and personal revision; company bookmarks remain protected. Bookmark titles, URLs and folders pass local DLP before export. Policy changes during an operation suppress its result, even if an authorized central bookmark mutation already committed; refresh before retrying. Navigation accepts HTTP(S) URLs without credentials. Selectors use a conservative CSS subset. Targets must be visible and eligible; password, file, hidden, token and authentication fields are denied. Typing emits input/change events without directly submitting a form. Normal outbound DLP still gates site requests. Read extracts at most 10,000 visible characters, excluding form/editable fields, hidden content, scripts/styles and sensitive elements. It returns no attributes, cookies or browser storage. Local inspection runs before return. `allowSensitiveRead` can permit known deterministic corporate DLP classifications; credentials, malware, unknown blocks and incomplete inspection stay denied. AI uses the centrally configured inspected gateway, with bounded inspected output. Detection does not guarantee recognition of every visible secret. Inventory works on the start page. Read and AI can target an explicitly identified background tab; click/type require the foreground tab. Destination and host restrictions apply to the target tab. Adapters check readiness, expiry/version, action scope, destination and captured native identity before/after awaited operations. Navigation, same-URL reload, closure and policy changes invalidate captured operations. Foreground captures additionally become invalid after switching away and back; explicitly captured background reads remain bound to their original tab while other tabs are selected. Intentional navigation returns a freshly captured context for the same tab and preserves the foreground activation generation. Fixed DOM scripts bind to the captured document URL before interaction. No policy bypass or upload exception is generated. Page actions already performed cannot be undone by subsequent revocation; network requests still pass browser enforcement. ## Trusted integration contract `startAgentControl({authorize,actions,audit,persistCredential?,credentialFile?,onError?,timeoutMs?,maxRequestsPerMinute?})` returns `{address,token,edPublic,pqPublic,protocol,close,revoke}` asynchronously. It binds an ephemeral port only on 127.0.0.1. Defaults: 10-second action deadline, 60 authenticated requests/minute, 128 KiB encrypted wire-body limit (the CLI request file remains bounded to 32 KiB). Configure it exclusively in trusted main-process code. `authorize({action,args,scope,sessionId,signal})` returns `{allowed:true,role:'agent'|'admin',scopes:[requiredInternalScope]}` after current policy checks. Action adapters receive `(args,{signal})`. `createAgentAdapters({getContext,getSessions,contextCurrent,navigate,ai,inspect,audit,appControls?})` expects `getContext(sessionId)` to return `{ready,sessionId,foreground,interactive?,token,policy,policyVersion?,webContents,destinationAllowed}`. `interactive:false` prevents click/type when chrome panels cover the website. Omitted IDs capture the current foreground tab. `getSessions()` returns trusted native inventory. `contextCurrent(token)` verifies native identity, navigation generation, activation generation and policy generation. `navigate(url,capturedContext,{signal})` must enforce the captured context before loading, and return the refreshed context for the same native tab after loading. `appControls` supplies fixed callbacks keyed by application action names; callbacks receive `(args,{signal,policyVersion})` and must enforce managed visibility/locks, native tab identity and central bookmark CAS. The destination predicate must return true for allowed URLs. Adapters use fixed DOM code with JSON-escaped data, and honor abort. Prefer `persistCredential({address,token,edPublic,pqPublic,protocol})` backed by protected trusted storage; Windows integration uses DPAPI. Plaintext `credentialFile` persistence is rejected; use the protected persistence callback. Never accept callback/path configuration from pages. Audit contains action/scope/session/outcome metadata, not raw content. Audit failure prevents API adapter execution. Optional trusted `onError` reports static allowlisted reasons only. `revoke()` permanently disables the instance; `close()` shuts it down. Node deadlines cannot forcibly stop adapters that ignore cancellation. Debug spans separately measure challenge, decryption, authorization, audit, dispatch, projection and encryption. Individually signed grants, generation rotation and targeted revocation are implemented below; protected pairing still trusts processes of the paired Windows user. `select` and `check` are separate opt-in policy scopes and require a visible interactive foreground tab. Selection chooses one existing enabled option; sensitive/hidden options or groups are excluded. Checkbox control is idempotent, uses page interaction semantics and verifies the requested state after handlers. Duplicate selectors, unknown options, radio inputs and credential targets are denied. Inspect exposes up to 20 safe options per select and checkbox states; option values longer than 128 characters are omitted instead of truncated. All returned metadata passes the existing DLP gate. History navigation is native and centrally opt-in per action. It checks the native entry URL before dispatch and the refreshed managed context after completion; same-URL reload requires a real navigation event. Blocked entries, redirects away from the captured target, policy/tab races, unavailable entries and cancellation fail closed. This does not expose a browsing-history database or arbitrary URLs, and provides no policy bypass. Authenticated agent replies now carry their status inside the encrypted `agent-response` object; the bundled client derives success/failure from that field and refuses missing or invalid encrypted status. Outer HTTP status is not authoritative. A typed `browser-management-busy` response for a bookmark mutation occurs before the change begins. Only that authenticated encrypted 409 is eligible for up to three retries (200/400/600 ms), each with a fresh challenge and unchanged policy/CAS arguments. Conflicts, revocation, uncertain failures and plaintext error responses never trigger automatic retry. Durable agent events now carry a strict `agent` object with fixed action, matching scope, outcome and optional requested native session UUID. Arguments, selectors, prompts, tokens and fabricated principal IDs are excluded. Outcomes distinguish authorization, completion, denial and failure; an untyped adapter exception is reported as failure rather than inventing a policy-denial status. Completion and failure audit phases now have explicit recording spans and trace correlation. ECS preserves these fields under `safetynet.agent`; CEF uses bounded standard custom-string fields without overwriting trace or audit-integrity references. Legacy events without this object remain readable; they are not retroactively attributed. Individual credential identities and targeted revocation are implemented in [individual agent administration](docs/individual-agent-grants.md). ## Individually managed local identities The signed `agents.identityMode` selects `legacy` shared pairing or `individual`. In individual mode, select a centrally published, enabled and unexpired grant in **Local agents**. The shell returns a hashed descriptor filename protected with Windows DPAPI; it does not expose the token. Use that file with the existing trusted helper. There is no shared master credential in individual mode. Grants contain public ID/name, action scopes, additional allowed hosts, expiry and credential generation. Effective authority intersects global policy, assigned profile/group, grant rights and the adapter's own restrictions. The endpoint is not an admin policy editing API. Empty grant scopes grant no actions. Published revocation aborts owned work; unmodified peer credentials remain usable. A newly published policy is needed for offline devices to receive revocation. A device retains the highest observed generation per ID in its encrypted enrollment. Restoring lower-generation grant metadata cannot revive it after a restart. Re-pair after generation rotation or browser restart: descriptors pin an ephemeral service identity and cannot authenticate the new listener. Credential possession identifies a capability; DPAPI CurrentUser does not prove which executable is using it. Agent-initiated page actions bind subsequent tab network requests and popups to the initiating grant. Redirects and resource hosts must satisfy its allowlist. Explicit human address navigation releases this additional tab restriction while normal browser/DLP policy still applies. Library organization checks all involved metadata destinations before processing; it refuses mixed unauthorized scopes. The broker's individual integration APIs are `reconcilePrincipals(grants)` and `issuePrincipal(id,generation,expiresAt)`. Issuance requires a matching approved grant; main persists the returned descriptor atomically. Individual HMAC proofs bind route, ID, generation, time, nonce and wire digest. Signed one-use challenges also bind the service instance. Verified identity reaches adapters and durable agent events as `principalId`/`credentialGeneration`; failed authentication is unattributed. ECS uses `safetynet.agent.principal_id`; CEF `suid` represents the agent capability and `flexNumber1` its generation, not the employee identity. Each request has an `agent.request` parent trace with timed child phases. No payload arguments or credential secrets enter ordinary diagnostics. Revocation cannot undo an already committed side effect; it prevents later work and suppresses protected result release, including during asynchronous encryption. Individual mode currently refuses browser-session HTTP requests without a known, guarded native tab owner, including unattributed service-worker/background traffic. This containment remains after human navigation clears an agent tab restriction. Supporting such traffic requires reliable actor ownership or isolated sessions; that compatibility work is open and is not satisfied by inferring the active tab. ## Controlled credential preparation All vault scopes require explicit central permission. Unlock the vault through the trusted human UI, then obtain a fresh `vault:list` reference for the exact native login tab. `vault:fill` fills only. The separate `vault:prepare-submit` consumes its reference, fills the approved form and issues a single-use local allowance; its reply contains only completion/preparation booleans. Neither action submits the form, unlocks the vault or exports username/password values. A later explicit website submission still needs normal interaction permission and network checks. An administrator must additionally enable `passwordManager.controlledSubmission` and approve exact canonical HTTPS action URLs plus distinct username/password field names. The current allowance expires after 15 seconds and covers one bounded native main-frame, same-origin POST using `application/x-www-form-urlencoded` (64 KiB, at most 128 distinct fields). Exact credential values, document/frame, actor/generation, foreground activation, policy and vault state are checked again. Other fields, request destination and headers retain mandatory inspection. A mismatched candidate consumes permission; never retry automatically. JS fetch/XHR, JSON, multipart and arbitrary login protocols are outside this allowance. A 302-style GET receives ordinary inspection; 307/308 credential-body replay is blocked. The approved website receives credentials in its DOM, where its JavaScript can read them. The lease is a narrow network authorization mechanism, not password isolation, form attestation or a guarantee against phishing. Website authentication success is not returned by preparation or inferred from HTTP completion. Security audit separates prepared, authorized, denied, HTTP-completed and transaction-failure events. Authorization does not prove transmission; completion means the native HTTP transaction finished, including error responses. Ordinary metadata excludes credential values, form IDs, hosts, action URLs and body hashes. Protected debug spans name `credential.issue`, `credential.claim`, `credential.headers`, `credential.authorize`, `credential.redirect`, `credential.complete` and `credential.failure`, with correlated inspection/audit phases. Diagnostics remain centrally disableable and are not durable security audit. The isolated native fixture has exercised approved delivery, safe 302, blocked307, residual/duplicate/action/header refusals, human handoff and central revocation. The latest source increment's final native rerun, full suite and Windows package verification are pending; historical green builds do not prove this source state. See [controlled credential policy](docs/controlled-credential-policy.md). ## Inspected foreground visual export `screenshot` starts unchecked in central policy and requires the scope in both the baseline and the individual grant. It accepts only an optional native session ID; omission pins the current foreground tab. It cannot target a background/loading page, full desktop, other windows or browser chrome. The supported HTTPS viewport reply contains exactly MIME, dimensions and canonical base64 PNG data. Limits are 2 MiB, dimensions≤4096 and pixels≤8 Mi. There is no clip, resizing or full-page scroll mode. It never exposes CDP or arbitrary capture configuration. Mandatory local image/OCR complete coverage precedes export, together with enabled YARA/semantic checks and configured upload ICAP. Sensitive-read permission is no override. All credential/card/authentication-bearing controls are refused, even hidden/offscreen ones. Open shadow roots are observed recursively and pending mutation records drained; newly encountered roots advance the revision. Closed/ declarative shadow roots and child frames are refused. Active media/animations remain a compatibility/security limitation; classifiers are not proof against rendered secrets or steganography. Privileged code keeps an opaque private release guard outside the API result. After audit, encryption and final authorization, it rechecks the original policy, frame/process/routing identity, loading state, context token, actor and foreground activation immediately before returning the encrypted reply. This applies to legacy and individual callers. The PNG is byte-bound through inspection; bounded PNG validation excludes arbitrary text/EXIF/profile chunks and allows only required image chunks plus the supported standard sRGB marker. No image/OCR payload enters ordinary audit or diagnostic fields. Buffers are wiped best effort; returned base64 strings and caller copies cannot be guaranteed erased from managed memory. The protected channel encrypts the reply on the wire; the trusted helper decrypts its result for the caller. Treat received pixels and stdout/request artifacts as sensitive. This scope does not authorize sending an image onward to a cloud model. See [agent visual export](docs/agent-visual-export.md) for native proof scope and current validation status. The current native late-policy, final foreground and open-shadow mutation/revert proof passes with all eight named visual phases and final request parentage. 695 tests and syntax checks pass; current Windows build/package verification remain pending.