Repository navigation
[RFC] Strict delegation: host-enforced monotonic authority ceilings for subagents #36381
Description
Summary
Codex should support a strict, host-enforced least-privilege preflight for every subagent
delegation. Before either V1 or V2 spawn, the parent proposes the filesystem, network, environment,
and tool surface needed for the exact task. The host proves that proposal is a monotonic subset of
the immediate parent's current authority, issues a bounded one-use receipt, and atomically installs
the compiled ceiling before a child thread exists.
This is a harness/runtime property, not a prompt convention or role overlay. In strict mode the spawn
schema requires the receipt, so the parent cannot simply forget the preflight. The host proves
monotonicity; task-semantic minimality remains a planning/product-policy question.
Why the existing primitives are insufficient
A narrower role or prompt can be overwritten or bypassed by live inherited state, direct router or
control calls, runtime approvals, config reload, fork/resume, and host-side capability sources. The
existing request_permissions flow addresses approved expansion during a turn; it does not freeze a
least-privilege child ceiling at delegation time.
Filesystem sandbox policy alone is also incomplete. Hosted tools, MCP/apps/connectors, plugins,
skills, hooks, memory, dynamic tools, code-mode callbacks, and environment/config discovery can have
effects outside a local pathname policy. A strict contract must bind the executable tool/source plan
and the OS sandbox object identity as one capability snapshot.
Product shape: a Strict delegation mode
This could become an opt-in permission mode alongside today's delegation behavior: Strict
delegation mode. Enabling it would make least-privilege preflight a protocol requirement rather
than an instruction the model may omit. A strict parent would have to prepare and present a valid
receipt for every child; a strict child would inherit a sticky, monotonically narrower ceiling and
the same rule for any grandchild. If the selected platform, sandbox backend, tool runtime, or
capability source cannot enforce the compiled snapshot, the UI/API should report that surface as
unsupported and refuse the spawn before creating a thread.
The reference branch implements this strict delegated execution path behind a default-off feature.
It is not yet a proposal to silently replace existing permission modes or to label every current
sandbox configuration as strict. Productizing it would require an explicit mode name, support
matrix, migration/compatibility decision, and a durable provenance design for resume after process
restart.
Proposed contract
prepare_agent_permissions({
task_name,
agent_type?,
reason,
environment_id?,
permission_profile,
tools
}) -> {
permission_grant_id?,
effective_profile?,
effective_tools,
is_subset_of_parent,
diagnostics
}
spawn_agent({
...existing_fields,
task_name,
permission_grant_id
})
The host should:
- resolve and seal the current parent's effective filesystem, network, tool, environment, and
source authority; - reject lexical non-subsets before any path existence/canonicalization oracle;
- compile the requested child policy and prove it is no broader than the immediate parent;
- acquire backend-owned filesystem object leases where path-scoped authority requires them;
- bind a short-lived, one-use receipt to the parent, exact task, agent kind, local environment,
canonical policy, tools, source policy, and current parent snapshot; - revalidate and atomically claim the complete snapshot below all public spawn handlers, before
thread creation; and - repeat the same proof when a child delegates to a grandchild.
Missing, forged, expired, replayed, mismatched, concurrently consumed, or parent-drifted receipts
must fail before a child exists. Ordinary runtime approvals must remain inside the sealed birth
ceiling unless a future explicit replacement-ceiling protocol is designed.
Reference implementation
A default-off research implementation is available on
ayanamislover/agent-permission-preflight
at commit eab87d1f0f035c4bc4f13878a0ab4d90ae8ee16c. The
reviewer packet
contains a design decision, threat model, exact test interpretation, reviewer map, and deliberate
limitations. This is a reference branch, not an implementation PR.
The implementation demonstrates:
- a mandatory model-visible prepare step for strict V1 and V2 spawn;
- one shared consume path and an independent lowest-layer atomic claim;
- immediate-parent monotonicity for nested delegation;
- a sticky ceiling across permission requests, cached network decisions, shell,
apply_patch,
config mutation, follow-up/interrupt, fork/resume, and cold reload; - default-deny delegation support for local, hosted, dynamic, and external tool runtimes;
- raw parent tool definitions are sealed for consume-time drift detection, while each child receipt
separately binds the deterministic definition that strict source policy will actually expose; - hosted web is withheld for restricted networks and local managed proxy/domain policies unless the
provider can prove equivalent enforcement, preventing a hosted path from bypassing the local
network ceiling; - source isolation for MCP, apps/connectors, plugins, skills, memory, hooks, shell snapshots, project
instructions, login shells, role reload, and related pre-tool host entry points; - no-child denial assertions below V1/V2 public handlers, event-driven wait over host watch channels,
and explicit resumable-Completedversus terminal-close lifecycle semantics; - bounded receipt state: 8 live grants per parent, 16 per tree, five-minute lifetime, 64 terminal
tombstones per parent, and 32 object leases per receipt; and - fail-closed recovery when a process restart cannot recreate descriptor authority.
Filesystem object identity
On Linux, positive path grants are opened during prepare with openat2 and retained as O_PATH
descriptors. The sandbox helper validates the exact descriptor map and object identity; bubblewrap
uses --bind-fd/--ro-bind-fd instead of reopening a mutable pathname. Descendant grants are derived
relative to the retained parent descriptor with RESOLVE_BENEATH and no-symlink flags.
Real-process adversarial tests cover ordinary replacement, symlink replacement, exact read-only and
deny behavior, .git protection, shell plus apply_patch, and root-child-grandchild derivation. If
neither system nor bundled bubblewrap supports the required FD binds, launch fails closed. Dynamic
deny globs combined with object-bound positive paths are rejected because future matching names
cannot be sealed by a finite descriptor set.
Windows strict children require the elevated backend. Path-scoped grants are rejected until the
Windows sandbox has an equivalent handle-lease contract; the legacy restricted-token backend is
rejected rather than treated as secure.
Verification
Native Windows final workspace gate:
13,010/13,010 passed, 102 skipped, 0 observed retries, 0 failures/errors
One legacy restricted-token delete test is explicitly excluded and separately reproduced on the
unmodified upstream base. It is not counted as branch GREEN, and strict delegation refuses that
backend. A separate zero-retry sweep exposed two unrelated full-load timing flakes; both passed three
isolated zero-retry rounds (6/6), and the clean final full sweep used no retry.
Ubuntu 24.04.3 final workspace gate with real bubblewrap:
13,667/13,667 passed, 23 skipped, 0 retries, 0 failures/errors
The Ubuntu full run used a trap-protected temporary user-namespace window. The AppArmor sysctl was
independently confirmed restored to 1, and /tmp/.git, /tmp/.agents, and /tmp/.codex were
absent afterward. Directed default-AppArmor tests passed 118/118; Linux lifecycle/race stress passed
150/150; the two Windows full-load CLI cases passed 20/20 zero-retry stress before the final full
gate.
Security invariants
child_filesystem <= current_parent_filesystemchild_network <= current_parent_networkchild_toolsand strict-child definition digests are a supported subset/projection of the
parent's effective executable tools; raw parent definitions are re-read at consume- every nested spawn proves the same relation against its immediate parent
- denied tools are absent from model-visible schemas and rejected again at dispatch/runtime
- checked Linux paths stay bound to the same filesystem objects through sandbox launch
- role/config/source overlays, runtime approvals, fork/resume/reload, and direct control paths cannot
widen the child - unsupported environments, policies, tools, and backends fail before execution
- model self-report, prompt text, path strings, and diagnostic hashes are not security boundaries
Deliberate limits
- Local managed environments only; remote executor attestation is out of scope.
- The host proves monotonicity, not natural-language task minimality.
- Process-local receipt/descriptor state does not survive restart; durable strict provenance makes
recovery fail closed. - Exact path delegation is unavailable on platforms without an object-handle sandbox contract.
- External capability sources remain disabled until they expose a receipt-bindable enforcement
contract. MCP remains unsupported until a provider offers a session-pinned normalized catalog,
an enforceable revision invalidated bynotifications/tools/list_changed, and no silent behavior
mutation behind an unchanged advertised definition. - The research diff is intentionally broad and needs an ownership/landing plan before an
implementation PR.
Maintainer feedback requested
- Does the mandatory prepare -> one-use receipt -> lowest-layer atomic claim shape fit Codex's
scheduler and protocol ownership model? - Is the Linux descriptor-lease handoff (
openat2/O_PATHthrough bubblewrap FD binds) the right
backend contract to standardize? - Where should the cross-platform capability object and durable strict provenance live so a partial
landing cannot expose a bypassable mode? - Should live-child expansion require a replacement receipt/guardian, or should a strict child be
permanently non-expandable? - Which external tool/source providers, if any, can meet an explicit enforceable and attestable
delegation contract?
The main request is architectural direction and ownership, not review of a ready-to-merge PR. The
branch leaves unsupported surfaces fail closed and provides the code plus reproducible evidence so
the remaining upstream work can focus on integration choices rather than reconstructing the threat
model.
Activity
[-][RFC] Require a monotonic per-delegation permission preflight before spawning subagents[/-][+][RFC] Strict delegation: host-enforced monotonic authority ceilings for subagents[/+]sattyamjjain commentedon Aug 4, 2026
Read through the reviewer packet. The descriptor-lease work is more careful than most of what's shipping, so this is only about Q5, which is where I've spent most of my time.
For MCP I think tool identity by name is the weak point in the invariant set.
child_tools ⊆ parent's effective executable toolsholds at prepare time, but an MCP server can change what sits behind a name after the receipt is sealed.notifications/tools/list_changedexists precisely so a server can announce a new or altered tool surface mid-session, and a server that simply stays quiet and swaps a tool's description doesn't even need that. So the sealed set stays a subset nominally while the actual authority behind it widens.The version that survives is binding the tool schema at prepare rather than the tool name. A digest over the normalized tool definition (name, input schema, description text), recorded in the receipt and re-checked at dispatch, with any mismatch failing closed instead of refreshing. Description text matters more than it looks here, since that's the injection surface, and a "harmless" description edit is exactly how the tool-poisoning class works.
That also gives a concrete shape for Q5. An MCP server can meet a receipt-bindable contract if it pins tool definitions per session and either doesn't emit
list_changedinside a sealed ceiling or is refused when it does. Servers that mutate their surface freely can't meet it, and I don't think that's fixable from the host side, which might be the honest answer to give rather than keeping the door open.On Q4, from operating unattended agents rather than from your codebase: most mid-run expansion pressure comes from under-specified tasks rather than genuine need. Permanently non-expandable children are workable when respawn is cheap, and painful when a long run dies near the end over one missing tool. A replacement receipt that re-proves against the original parent snapshot rather than the current one seems like the middle path, though that moves the drift problem rather than removing it.
For context on where I'm standing: my own work here is in-process policy on the MCP call path, not an OS sandbox contract, so take this as a note on the source-isolation surface and not on the bubblewrap handoff.
jaebit commentedon Aug 6, 2026
Downstream implementation evidence from a Codex policy-harness prototype
I independently reached the same boundary while integrating Codex plugin hooks and custom agents on Windows. We implemented a deterministic local lifecycle contract in Pathmaru/Orchmaru, but deliberately kept its operational status at HOLD because a plugin/local ledger cannot provide the host-enforced property described by this RFC.
Public prototype: jaebit/pathmaru@15ac74e
The prototype models:
That work is useful as a contract and fixture, but it exposed three places where a downstream harness cannot close the boundary:
I suggest extending the RFC's receipt/provenance acceptance criteria to cover the complete collaboration lifecycle, not only birth-time authority. Field names below are illustrative rather than a request to freeze a particular API:
The host should be able to prove these invariants:
A minimal regression gate would therefore assert:
This seems complementary to:
$homecollision let a read-only subagent recursively delete%USERPROFILE%#32684 for the concrete risk of treating a read-only assignment as a prompt convention;The local prototype is not proposed as an alternative security boundary. Its useful result is the negative one: contract fixtures can define these invariants, but only the Codex scheduler/control plane and sandbox backends can issue trustworthy lifecycle and no-side-effect receipts.
ayanamislover commentedon Aug 8, 2026
Thank you @sattyamjjain and @jaebit — I treated both comments as concrete acceptance criteria and hardened the reference branch before publishing this update.
Tool identity and mutable providers
The receipt no longer binds only tool names. It now seals canonical SHA-256 digests over namespace/name, description, input schema, and execution-facing options. The host keeps the raw parent definition and the deterministic strict-child projection separate: the former is re-read at consume time, while the latter is what the child router and final dispatch must match. The fresh full-workspace sweep caught why this distinction matters: strict source policy removes the optional shell
loginfield, so binding the raw parent schema directly caused the real strict child to fail closed. That regression is fixed and covered at receipt and real-child levels. The host now:MCP remains unsupported in strict children. The documented future contract requires a session-pinned normalized catalog and enforceable revision, treats
notifications/tools/list_changedas ceiling invalidation, and rejects providers that can silently mutate behavior behind an unchanged advertised definition. I agree that this is the honest boundary rather than implying arbitrary MCP servers can be made safe from the host side.Host lifecycle evidence
The strict V1 and V2 missing-receipt regressions now assert that the thread manager remains empty after denial, so no child thread or child model loop can start. The lifecycle notes also make the existing semantics explicit:
Completedis resumable and retains the same sealed ceiling;I did not claim a unified requested/effective role/model/sandbox audit stream that does not yet exist. That remains a separate observability follow-up; it must not become the authority source for the host-owned permission snapshot.
Reverification
13,010/13,010 passed, 102 skipped, 0 observed retries/failures. One legacy restricted-token delete test is explicitly excluded after reproducing the same failure on the unmodified pinned base; strict delegation rejects that backend. A preceding no-retry sweep had two unrelated full-load timing flakes, both of which then passed three isolated zero-retry rounds (6/6) before the clean final sweep.13,667/13,667 passed, 23 skipped, 0 retries/failures. The trap restoredkernel.apparmor_restrict_unprivileged_usernsfrom 1 to 1, and/tmp/.git,/tmp/.agents, and/tmp/.codexwere absent afterward.codex-core --lib: Windows2,120/2,120(3 skipped), Ubuntu2,215/2,215(0 skipped).codex-coreClippy passed without diagnostics on both platforms before the final mechanicaljust fix/just fmt.Updated reference commit: ayanamislover@eab87d1
The reviewer packet now includes the reply-driven threat-model delta, exact cross-platform evidence, and the deliberate unsupported-provider boundary: https://github.com/ayanamislover/codex/tree/eab87d1f0f035c4bc4f13878a0ab4d90ae8ee16c/research/agent-permission-preflight
Roppon-64 commentedon Sep 30, 2026
Could this RFC expose a required External Spawn Authorizer integration at its native delegation boundary?
The RFC already addresses mandatory preflight, one-use receipts, and atomic enforcement before child creation. I would like to build on those guarantees rather than propose a parallel admission mechanism. The additional requirement is that an operator-configured external policy provider participates as a mandatory authorization authority.
In an opt-in required mode, the contract would provide:
decision: allow | deny, additional constraints, and an authorization receipt for an allow. Constraints may only narrow the host-validated authority. Unsupported or unenforceable constraints must cause refusal.A possible integration with this RFC is:
The host would retain responsibility for proving monotonicity, enforcing the compiled ceiling, and atomically validating the external receipt together with the strict-delegation grant. The external provider would not create children, expand permissions, or replace the native runtime. If required authorization fails, neither a child nor its initial model execution should start.
This is machine authorization, with its own receipt type and audit semantics. An external allow must not be interpreted as human approval, workflow approval, or completion evidence. Conversely, those records cannot substitute for the required external receipt.
Would this fit as an optional policy-provider extension to the RFC’s existing preparation and consume boundary? The transport and exact API names are open; the essential addition is a trusted external decision that is technically mandatory before native delegation can take effect.
virajchoudhary commentedon Oct 13, 2026
Current-source confirmation from a Windows verifier-isolation investigation, 2026-10-13.
At upstream
90f00bd89ed3263b4d5993849cccb795f4330449, both spawn versions route throughprepare_agent_spawn_configincodex-rs/core/src/agent/child_config.rs. It applies the role and then callsapply_spawn_agent_runtime_overrides, which copies the live parent's permission-profile snapshot and approval policy. The typedAgentRoleOverridesincodex-rs/core/src/agent/role.rsalso omits permission settings. Neither V1 nor V2 spawn arguments expose an independently enforced child permission profile.The current official Subagents documentation explicitly describes reapplying live parent permission overrides over role defaults. Accordingly, a role file containing
sandbox_mode = "read-only"must not be treated as a hard write boundary under a Full Access parent. Our retained audit distinguishes a working read-only-parent delegation from the unresolved Full Access-parent case; those are different acceptance claims.This supports the RFC's sticky host-ceiling requirement. Changing role precedence or adding a birth-time sandbox field alone would not close permission expansion, nested delegation, tool-source authority, or reload paths. We did not apply such a partial patch or activate the research branch. Its published test totals were not independently rerun here.
This is sanitized source analysis and workflow evidence, not a new exploit report, installed fix, or unsolicited code contribution. No credentials, conversation content, personal paths, or runtime configuration changes are included.
virajchoudhary commentedon Oct 13, 2026
Independent reference admission checks and the remaining Windows OS gate
I checked the published reference commit
eab87d1f0f035c4bc4f13878a0ab4d90ae8ee16con native Windows x64 with Rust 1.99.0 throughjust test. This was a separate local checkout, not an installed Codex or Desktop change.I added one focused local test for the Full Access-parent admission case:
PermissionProfile::Disabledcan prepare a managed read-only child snapshot, while an unsandboxed child request is rejected withUnsupportedPolicy. The final admission selection passed 22 tests. Five distinct session update, runtime-grant, and receipt-free/control-overlay tests also passed. Both selections used zero retries. No full workspace sweep was run.These are admission and in-memory enforcement checks. They do not prove child-side create, overwrite, delete, policy-file mutation, escalation, or nested-spawn denial through the Windows OS backend. The published real Windows filesystem tests construct a managed writable parent, rather than the exact Full Access-parent case. That case still needs a real-process canary.
One practical test-isolation boundary matters when reproducing this on a working Codex host. The Elevated setup uses fixed
CodexSandboxOfflineandCodexSandboxOnlineaccounts and stable firewall rule names. Itsprovision_sandbox_usersgenerates fresh passwords, andensure_local_userfalls back toNetUserSetInfofor an existing account. A separateCODEX_HOMEchanges the secrets-file location, not those machine-global identities or rules. It is therefore not proof that setup preserves an active host runtime.I did not run Elevated setup or a real restricted-child OS canary on the recovered host. The safe remaining gate is a disposable Windows test host, with effective child permissions, file/policy mutation attempts, escalation, nested delegation, and a parent write-control check. Known sandbox-account metadata and user PATH remained unchanged during the selected library tests.
Independent source review accepts the narrow admission test but reports FAIL for the requested runtime-isolation acceptance. No installed repair or overall security PASS is claimed. This adds bounded reference evidence, not a proposal to productize the broad branch or submit an external PR. No credentials, private logs, account identifiers, personal paths, or conversations are included.