Skip to content

feat(externagent): state each provider's episode lockdown, and refuse one that cannot be stripped - #532

Merged
ion-alpha-dev merged 2 commits into
mainfrom
feat/episode-lockdown-contract
Aug 14, 2026
Merged

feat(externagent): state each provider's episode lockdown, and refuse one that cannot be stripped#532
ion-alpha-dev merged 2 commits into
mainfrom
feat/episode-lockdown-contract

Conversation

@ion-alpha-dev

Copy link
Copy Markdown
Collaborator

What

externagent.Adapter now states a Lockdown: how the CLI's native writes and native commands are taken away for an episode, and whether the operator's own configuration reaches the child. The runner reads it before it binds or spawns anything and refuses a provider it does not strip, naming every class that failed. The zero value is a refusal, so an adapter that declares nothing is treated exactly like one that cannot strip its surface.

An episode also stops running on the operator's codex configuration: only the credential is seeded into the per-episode home now, not config.toml.

Why

Both bundled adapters lock their CLI down correctly, and both do it inside the argv their Command builds. Nothing outside the adapter can read the posture, and nothing checks that it is still there, so the rule is a property of who wrote the adapter rather than of the port. A third adapter that omits it produces episodes that look governed in the record.

The lockdown is also the part of an integration least likely to transfer. The binary, the flag spelling and the tool names are all provider-specific, so a rule written as "pass --disallowedTools Edit Write" holds on exactly one CLI and silently holds on none of the others. The contract names the mechanism instead: denied by the CLI's own controls, or contained by the boundary. The two bundled adapters reach different verdicts from it, which is the point: claude denies its effectors outright, and codex, which has no flag for its shell or patch tools, declares containment.

config.toml is where a codex user keeps their MCP servers, model providers and settings. Seeding it into the episode home let whoever launched the run steer the harness, and gave the child servers this run neither hosts nor governs. Everything an episode needs is passed on the command line, so the child's configuration is now a function of the run.

How to verify

go test ./externagent/... ./cmd/flynn/...

The new tests are the contract: every bundled adapter must declare a stripped lockdown, each declaration is checked against the argv that adapter actually builds (claude's denial list and permission mode, codex's read-only sandbox and denied approval path), and the runner refuses both an unstrippable provider and an undeclared one without spawning anything.

Notes for reviewers

Adding a method to externagent.Adapter is an incompatible change on the domain surface, which ./dev/apidiff reports rather than gates. It is deliberate: an optional interface would make the omission silent, which is the failure this closes.

Dropping config.toml from the seed changes behavior for anyone whose codex config sets something the episode relies on. A model comes from the run and the rmcp client is set on the command line, so the case left is a non-default model provider, which an episode should not inherit from the host anyway.

The three cmd/flynn external-agent tests fail on a machine that has codex.cmd installed without the vendored binary beside it. That reproduces on main and is unrelated to this change.

one that cannot be stripped

Both bundled adapters lock their CLI's native surface down correctly,
and both do it inside the argv their Command builds, where nothing
outside the adapter can read the posture and nothing checks it is still
there. A third adapter that omits the lockdown produces episodes that
look governed in the record.

Adapter now states a Lockdown: how native writes and native commands are
taken away, and whether the operator's own configuration reaches the
child. It names the mechanism (denied by the CLI's own controls, or
contained by the boundary) rather than the flag, since the binary, the
flag spelling and the tool names are the parts that do not transfer
between providers. The runner reads it before it binds or spawns
anything and refuses a provider it does not strip, naming every class
that failed. The zero value is a refusal, so an omitted declaration
cannot pass for a lockdown.

The two adapters reach different verdicts from the same contract: claude
denies its effectors outright, codex has no flag for its shell or patch
tools and declares containment. Tests check each declaration against the
argv that adapter actually builds, so the statement cannot drift from
what it describes.

An episode also no longer runs on the operator's codex config. Seeding
config.toml handed the child the MCP servers, model providers and
settings of whoever launched the run; only the credential is seeded now,
and everything an episode needs is passed on the command line.

Signed-off-by: Ion Alpha <contact@ionalpha.io>
@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ion-alpha-dev
ion-alpha-dev merged commit 686ac66 into main Aug 14, 2026
28 checks passed
@ion-alpha-dev
ion-alpha-dev deleted the feat/episode-lockdown-contract branch August 14, 2026 19:57
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant