Skip to content

flow: every teach path runs under the declared teach scope - #1386

Open
abrichr wants to merge 2 commits into
mainfrom
claude/teach-scope-everywhere
Open

abrichr wants to merge 2 commits into
mainfrom
claude/teach-scope-everywhere

Conversation

@abrichr

@abrichr abrichr commented Oct 20, 2026

Copy link
Copy Markdown
Member

Stacked on #1384 (which holds #1359, #1335 and #1357). Retarget to main once #1384 merges; the diff here is only this change.

What changes

Before this change, only openadapt_flow.jit.compile_agent enforced the teach scope from #1359. compile-agent, teach over MCP and the airbag guard read the same untrusted app content without it. Now all three pass the task's declared inputs, origins and writes, and the MockMed injection fixtures run through each path.

compile-agent (FirstRunCompiler, compile_first_run, openadapt-flow compile-agent)

  • The scope is built from what the command already declares before the agent reads a page: the app URL's origin, --expect (the record), the inputs and --on-screen.
  • New: --allow-origin, --allow-text and --write-adapter MODULE:ATTR.
  • A violation ends run 1 as scope_refused. A read-back that doesn't show the declared write ends it as readback_refused. Neither leaves a draft.
  • The Claude and code-as-action agents re-raise a scope refusal instead of handing it to the model as a tool error.

JIT capture

  • Any violation during run 1 refuses the draft, even when the agent caught the refusal and went on.

Teach over MCP (openadapt-agent serve --teach)

  • A scope violation ends run 1 like a refused save.
  • --teach-allow-origin and --teach-write-adapter set the scope.
  • --teach-tasks DIR takes run 1's record and inputs from the operator's task files (openadapt.guard-task/v1). A hijacked agent then can't re-declare another record after reading the app.

Airbag guard (openadapt-flow guard)

  • With a scope, every request of the tab is paused, not only documents, fetch and XHR.
  • Reads to undeclared origins are blocked: image beacons, links, scripts.
  • With write_adapter, each save is read against the task's record and values before it is sent.
  • guard.yaml gains allow_origins, allow_text and write_adapter.
  • A run that left its scope is never handed to run 2.

teach_scope

  • New: TeachScope.for_task, a pure request_refusal and claim_refusal (shared with the guard), load_write_adapter, and scopes with no browser origin (desktop surfaces).

Docs

  • THREAT_MODEL has a per-path table and the results by path.
  • LIMITS, GUARD_AN_AGENT, COMPILE_AGENT (new statuses and flags) and the agent README (new flags) are updated.
  • The CAPABILITIES Limits line about unscoped paths is deleted; the capability row now covers every path.
  • llms-full.txt is rebuilt.

Tests (no model calls)

  • tests/test_injection_paths.py, 16 cases with agents scripted to obey the injected text:
    • compile-agent: another patient (save gate), other fields by typing or by clicking Consult (scope_refused), an outside origin (scope_refused, no request reaches it), skip the check against an app that writes nothing (readback_refused), and a clean run that is a draft.
    • MCP teach: the same four goals, plus --teach-tasks refusing a re-declared record.
    • Airbag guard, real Chromium over CDP: a page beacon and a link to another origin (blocked, no handoff), another patient and other fields (blocked before sending), and a clean save that is VERIFIED.
  • tests/test_injection_suite.py: new case where an agent swallows a refusal and still gets no draft.
  • tests/test_teach_scope.py: the builder, the request check and the adapter loader.
  • tests/e2e/test_mcp_teach_e2e.py::test_a_save_on_the_wrong_record_fails_the_read_back now expects readback_refused. The scope's read-back of run 1 refuses before the older record check runs. It is still refused with nothing to admit.

Local runs:

  • Flow: 260 passed across the injection suites, the compile-agent, MCP teach and agent-guard e2e and unit tests, JIT, self-repair, guarded save and the capability map.
  • packages/agent: 534 passed, 9 skipped.
  • ruff, format and mypy are clean.

Still true after this

  • The airbag guard sees the agent's requests, not its keystrokes. Without a write adapter, a save's values are judged by the read-back after the save lands.
  • Over MCP without --teach-tasks, the agent declares the record at teach_start. The person who confirms the record check sees it.
  • No live model has been run against the fixtures. The live-model test is off by default.

🤖 Generated with Claude Code

abrichr and others added 2 commits October 19, 2026 20:08
compile-agent, teach over MCP and the airbag guard now pass the task's
teach scope (inputs, origins and writes), so text on the page an agent
reads can't widen what run 1 may type, reach or write.

- compile-agent (FirstRunCompiler, compile_first_run, CLI): the scope is
  built from the app URL's origin, --expect, the inputs and --on-screen,
  plus --allow-origin, --allow-text and --write-adapter. A violation ends
  run 1 as scope_refused; a read-back that doesn't show the declared write
  ends it as readback_refused. The Claude and code-as-action agents no
  longer swallow a scope refusal.
- Any violation during a JIT capture refuses the draft, even when the
  agent caught the refusal and went on.
- Teach over MCP: a scope violation ends run 1 like a refused save.
  --teach-allow-origin and --teach-write-adapter set the scope;
  --teach-tasks takes run 1's record and inputs from operator task files,
  so the agent can't re-declare them after reading the app.
- Airbag guard: with a scope, every request of the tab is paused; reads to
  undeclared origins (image beacons, links, scripts) are blocked, and with
  write_adapter each save is read before it is sent. guard.yaml gains
  allow_origins, allow_text and write_adapter. A run that left its scope is
  never handed to run 2.
- teach_scope: TeachScope.for_task, request_refusal and claim_refusal for
  reuse, load_write_adapter, scopes with no browser origin.
- tests/test_injection_paths.py runs the MockMed injection fixtures through
  each path with agents that obey them. THREAT_MODEL, LIMITS, GUARD_AN_AGENT,
  COMPILE_AGENT and the agent README describe the shipped behavior; the
  CAPABILITIES Limits line about unscoped paths is gone.

All data is synthetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from batch/repair-safety-agent to main October 20, 2026 02:18
@abrichr abrichr closed this Oct 20, 2026
@abrichr abrichr reopened this Oct 20, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant