Skip to content

Screen only external tool results in Auto posture - #970

Merged
16francej merged 6 commits into
mainfrom
security-screen-provenance
Sep 8, 2026
Merged

Screen only external tool results in Auto posture#970
16francej merged 6 commits into
mainfrom
security-screen-provenance

Conversation

@16francej

@16francej 16francej commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Auto mode was quarantining tool results that never left the agent's own workspace: skill docs read through the read tool, repo files, and control-plane tool echoes. Across a month of live screenings, roughly two thirds of quarantines were false positives of this kind and none were a third-party injection. The classifier had no provenance signal, so it judged content shape.

Change

  • Every tool result now carries a provenance class decided by the tool. Internal bookkeeping tools (goals, cron create/patch, background status, finish_silently, write, publish, share) skip the classifier. The read tool is workspace and skips it too, except shared handles from another scope. Everything that can carry outside content is external and screened: surface reads, MCP, credential_exec, shared-handle reads, reached-room execs, shared crons, memory, history, and all execute and background output.
  • Shell output stays screened exactly as on main. An earlier revision tried to decide per command whether it reached the network, first from command text and then from egress-proxy accounting; both depended on state outside core's control and were dropped in favor of screening by tool.
  • The two screening hooks collapse into one that returns an outcome with the verdict reason. The reason lands in the audit row and the quarantined session entry, and the quarantine stub names its source. Oversize external output is screened in overlapping bounded chunks, four at a time, stopping at the first strict verdict.
  • Codex, Claude, and OpenCode harnesses gain the same hook, so tool results are screened consistently across harnesses.
  • The rubric frames injection as an authority problem and adds benign examples of documentation and code. On the live corpus this rubric change alone clears more false positives than the provenance table does.

Replay against live data

672 recent screens (222 quarantined, 450 allowed) replayed through the new pipeline.

Corpus Before After
Most recent 472 screens 22 flagged 0
All 222 quarantines 222 29
450 previously allowed 0 0

Of the 29 that remain: 13 are deliberate injection test fixtures, 8 are the agent's own credential material (keychain responses, materialized env files), 6 are first-party skill docs read through the shell rather than the read tool, and 2 are session-dump inspections. Classifier calls drop by about 22%.

The remaining noise is local shell reads of first-party files. A prompt nudge toward the read tool for files is the follow-up that shrinks it without weakening the screen.

Known trade-off

Content fetched to disk and read back later through read is not re-screened. Binary shared files and binary inbound attachments share this gap; the attachment one predates this change.

Auto mode quarantined 236 tool results in prod over four weeks; two thirds were
qm's own skill docs, repo files, and control-plane tool echoes, and none were a
third-party injection. The classifier had no provenance signal, so it judged
content shape, and instruction-shaped text is a coding agent's daily diet.

Every tool result now carries a provenance class. Internal tools (goals, cron,
background bookkeeping, finish_silently, write, publish) and workspace reads
skip the classifier. Only external content is screened: surface reads, MCP,
credential_exec, shared-handle reads, reached-room execs, and execute or
background output whose command fetched from the network. The two screening
hooks (screenToolResult and screenExternalContent) collapse into one that
returns an outcome with the verdict reason, which now lands in the audit row
and the quarantined session entry. Codex, Claude, and OpenCode harnesses gain
the same hook, so tool results are screened consistently across harnesses.

The rubric now frames injection as an authority problem and gives the
classifier benign examples of documentation, code, and skill files.
Background polls read the launching command from the durable process
registry instead of a per-instance map.

Replaying 672 live screens (222 quarantined, 450 allowed): 10 quarantines
remain, all deliberate injection tests or credential material, and no
previously allowed screen is newly flagged. Over the most recent 472 screens
the flag count drops from 22 to 0. Model calls drop by roughly 70 percent.
@16francej
16francej force-pushed the security-screen-provenance branch from c787013 to 295fb7f Compare September 8, 2026 02:14
Screen oversize external output in bounded chunks instead of failing open
past the payload cap, so an injection buried deep in a long Slack thread or
MCP response is still classified. Treat cron list/get results as external
when they can carry other members' crons. Treat a background job whose
stored command hit the redaction length cap as external, since the network
shape of the full command is unknown. Drop the rubric example that declared
a calendar-connect instruction safe, which would have misled the classifier
on a fetched skill file.
The network-command regex guessed whether a shell command reached the
network from its text and failed open for anything it did not list. Replace
the guess with ground truth from the egress proxy.

Every execute and background start now runs under its own egress credential:
the tool context re-mints the turn's egress token with an execId claim and
overrides the sandbox's proxy environment for that command only. The authz
service stamps the core synchronously on an execution's first allowed
connection (once per execId, over a signed POST to /v1/egress-stamp, or
directly into the store when no core relay is configured) and denies the
connection if the stamp cannot be recorded. After the command finishes, the
tool context reads the stamp and reports egressed on the result. Background
jobs persist their egress id in the process registry so later polls can read
the same stamp.

Provenance is then a lookup: no stamp means workspace, a stamp means
external, no accounting at all fails closed to external. The regex is gone.
Tests drive the same path through a fake proxy that stamps when a command
mentions a URL.
An egress stamp that never arrives must not read as "this command stayed
local". Three ways that could happen are now closed: the authz process
without a shared stamp store denies per-execution connections instead of
stamping into its own memory; the tool context only mints per-command
credentials when the sandbox enforces egress at the network layer, so
env-only proxy backends stay fully screened; and the store's table name is
one shared constant.

Chunked screening now overlaps adjacent windows and splits any window whose
serialized form still exceeds the bound, so an instruction can neither
straddle a boundary nor hide in an excised middle. Chunks classify four at
a time and stop at the first strict verdict.

cron runs, history, and memory results are external content: they can carry
other members' text. The quarantine stub names its source so the model knows
which read was flagged. The release key and tool label live in one helper
shared by the tool layer and the orchestrator.
Drop the per-command egress credential, the proxy stamp, the stamp store
and route, the registry column, and the canary plan. All of it existed to
decide whether one shell command reached the network, and every version of
that decision depended on a separately deployed proxy being upgraded and
reachable, with silence misread as safety when it was not.

Provenance is now purely by tool. execute and background output is always
external and screened, as on main. The read tool is workspace. Internal
bookkeeping tools are never screened. This keeps every security property of
main for shell output and still removes the false positives from internal
tools and workspace reads. The remaining noise is local shell reads, which
a prompt nudge toward the read tool can chip at later.
@16francej
16francej merged commit c0f8c97 into main Sep 8, 2026
20 checks passed
@16francej
16francej deleted the security-screen-provenance branch September 8, 2026 17:13
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