Repository navigation
Conversation
Adds openadapt.teach-authorization/v1 (person-signed scope: task, inputs, origins, permitted writes, forbidden actions), binds it on the JIT capture path, records its digest in the signed recorder block and admission manifest, and requires it at admission for agent-taught drafts. Other authoring paths, the live-model harness and full test runs are not done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fuse every teach path The signed teach authorization (openadapt.teach-authorization/v1) now uses the approver keys and the approver verifier instead of GitHub SSH keys. Admission of every JIT draft requires one, keyed on the signed recorder block. compile-agent, MCP teach, the skill cache, browser-teach writes, agent-demonstrated fixes and self-repair of agent-taught programs are bound; the airbag handoff's drafts are refused. Adds openadapt-flow teach-authorization create/sign/verify.
Refuse browser-teach writes instead of half-binding them. Pin the roster from the deployment config for servers an agent runs; pass recorded heads; add --approver-root to jit admit. Check the task at repair approval and at admission from the signed recorder block. Keep the signed write adapter in narrowed scopes. Refuse a click whose label probe fails when controls are forbidden. Drop the Desktop stdio signing path. Default expiry 24 hours. Changelog with the breaking changes and upgrade notes.
Benchmarks and the JIT example sign a throwaway authorization per run (demo_authority) so their drafts still admit. admission_status uses the same trust as admit. create() defaults to a 24-hour expiry, at most 90 days. Document the remaining trust limits.
Run 1 of a write starts only when the person's statement verifies and covers the write: the app's origin, the contract as record kind, a write adapter, run 1's inputs, and exactly the contract's checked record fields with run 1's values. The adapter reads every write before it is sent, and only signed values may be typed. A taught tool keeps the statement, is signed with the local admission key, and every call checks both again (roster, purpose, task, expiry). The example app ships its write adapter. The two browser write e2e files run again.
Independent adversarial review (round 1), head 0acc432A fresh reviewer got only the diff and the goal. It read the code and ran one Verdict: request changes. What holds: a write can't be taught without a statement that verifies for Findings:
|
teach_run's scripts act through the same authorized run 1 as teach_act, so the scope bound from the statement checks every action and the save. Tests with a real browser, roster and admission gate: a teach_run draft without a statement is refused at admission, one under a statement passes the gate, and a script that types a value the statement doesn't allow saves nothing.
…test nits; merge the #1436 head Merge claude/teach-authorization (teach_run binding) into this branch, and: - run 1's adapter check pins the record's key wherever the statement lists it; - the program-signing key directory must come from the config's approvers section, the key is never created on demand, the program carries a signature domain, and running tools needs only the config's roster pin; - the verified copy of the statement is the one kept, and teaching again replaces it; - coverage maps literal matches and refuses effects it can't map, the row count must match, expiry is required (at most 90 days) at teach and run time, and run 1 counts its writes and watches the forbidden actions; - press in run 1 takes no character keys; - forbidden controls are read from Chromium's accessibility tree, and every key is checked against the control that has focus when it arrives; - the teach_run tests assert the refusal code, reach the save under a statement (wrong record, and a person-approved save), and no longer say admissible.
Independent adversarial review (round 2), head a8bb412A fresh reviewer got only the diff and the goals. It read the code and ran two small headless-Chromium probes; it did not run the test suite. Verdict: request changes. The write-lane binding is mostly sound. Teaching or running a write without a valid statement that covers it is refused, and edited, swapped, expired or wrongly signed statements fail closed. Run 1's writes go through the adapter in the route handler. The keyboard guard has two holes, which the reviewer reproduced in Chromium. Findings:
Missing tests: frame focus, implicit submission, a forbidden control reached through |
- test_agent_guard_e2e: the handoff's draft is refused at admission by an AdmissionError, not a returned detail; assert the raise. - docs/phi_at_rest.md lists the new teach-authorization/* bundle entry (test_sealed_at_rest checks every inventory entry is documented). - The two browser-teach write e2e files assert the refusal against the real example app instead of skipping: flow-critical fails on any skipped test.
- Keys are checked where they land: focus is found across the page's frames, and Enter in a form field is also checked against the form's default button (implicit submission). Modifier chords are refused while controls are forbidden. - A click is read from the accessibility tree by DevTools; a node that can't be found is unknown and refused; with no page, clicks are refused too. - Browser teach checks click, select and press targets by accessible name over DevTools, not by the listing label. - The admission key must exist (private and public) and is checked before run 1; it is never created on the write lane. - Run 1 reaches only the statement's origins, reads included. - expected_count 0 is read as 0. - Docs: what the accessibility tree does and doesn't defeat; the serving process must run as a user the agent can't act as.
The two browser-write e2e files keep this branch's full versions, which teach and run writes under a signed statement.
Independent adversarial review (round 3), head 21c18d8A fresh reviewer read the code and ran headless-Chromium probes. It did not run the test suite. Verdict: request changes. The authorization binding held: the reviewer couldn't teach or run a write without a valid statement that covers it.
Findings
|
- Run 1 of a taught write opens no WebSocket (route_web_socket; Playwright without it refuses the write). - Enter is checked against the browser's own default button (:default), read in an isolated world; no re-implemented form rules. - A click is matched by its accessible name, the text of a control with no role and three ancestors, and the control a <label> is for (isolated world); the point is converted to document coordinates. - Browser-teach clicks scroll the target into view before they're checked. - Browser tools have their own task namespace, browser-tool.<name>. - A tool's program name must be its folder's. - The example adapter reports every posted field. - Docs say what binds later calls, and the rollback limit.
Independent adversarial review (round 4), head c92461fRecorded by the pr-backlog session, which paused before posting it. The round 1–3 fixes hold. Verdict: request changes. Blockers
Major
Minor
Next: land #1828 (after its review), wire it in here, add real-browser tests for a SharedWorker POST, a dedicated-worker fetch and a worker WebSocket on this lane, then request round 5. |
Builds on #1436. That PR stops browser teach from teaching or running any write tool. This one turns writes back on, under the signed teach authorization.
What changes
Teaching a write.
teach_startwith a contract needs the server to be started with--teach-authorization. The statement must:teachand tasktaught.<name>, and not be expired;record_kind;If it's missing or anything differs, nothing starts.
During run 1.
Running a taught tool.
program.jsonis signed with the local admission key.did_not_run.Other changes.
openadapt_flow.agent.example_app:OrderWrites.serve --teach-browsertakes the roster pin only from the deployment config.Limits
Tests