[RFC] Strict delegation: host-enforced monotonic authority ceilings for subagents

Open 💬 3 comments Opened Jul 31, 2026 by ayanamislover

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:

  1. resolve and seal the current parent's effective filesystem, network, tool, environment, and

source authority;

  1. reject lexical non-subsets before any path existence/canonicalization oracle;
  2. compile the requested child policy and prove it is no broader than the immediate parent;
  3. acquire backend-owned filesystem object leases where path-scoped authority requires them;
  4. 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;

  1. revalidate and atomically claim the complete snapshot below all public spawn handlers, before

thread creation; and

  1. 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-Completed versus 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_filesystem
  • child_network <= current_parent_network
  • child_tools and 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 by notifications/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

  1. Does the mandatory prepare -> one-use receipt -> lowest-layer atomic claim shape fit Codex's

scheduler and protocol ownership model?

  1. Is the Linux descriptor-lease handoff (openat2/O_PATH through bubblewrap FD binds) the right

backend contract to standardize?

  1. Where should the cross-platform capability object and durable strict provenance live so a partial

landing cannot expose a bypassable mode?

  1. Should live-child expansion require a replacement receipt/guardian, or should a strict child be

permanently non-expandable?

  1. 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.

View original on GitHub ↗

3 Comments

sattyamjjain · 23 days ago

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 tools holds at prepare time, but an MCP server can change what sits behind a name after the receipt is sealed. notifications/tools/list_changed exists 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_changed inside 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 · 21 days ago

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:

  • admission preregistration before launch;
  • launch, follow-up, terminal, and bounded-wait transitions;
  • immutable parent/task binding and single-use event IDs;
  • terminal-instance reuse rejection;
  • deterministic deny reasons and replay with zero declared model posts;
  • effective read-only capability checks for verifier roles.

That work is useful as a contract and fixture, but it exposed three places where a downstream harness cannot close the boundary:

  1. A local receipt does not prove that admission ran before the host created a child thread or made a model request.
  2. A role TOML or prompt cannot prove the effective child sandbox when live parent/runtime authority can override or bypass it.
  3. A caller-supplied "observed" wait event is not evidence of an event-driven host wait; it can hide model-mediated polling.

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:

lifecycle_decision = {
  task_id,
  parent_thread_id,
  child_thread_id?,
  instance_id?,
  transition,              // preregister | launch | followup | terminal | wait
  requested_role,
  effective_role,
  requested_model,
  effective_model,
  effective_sandbox,
  allowed,
  deny_reasons,
  created_instances_delta,
  actual_model_requests_delta,
  authority_receipt_id,
  transition_receipt_id
}

The host should be able to prove these invariants:

  • A denied or incomplete prerequisite path produces both zero created child instances and zero model requests.
  • A verifier admitted as read-only receives an effective platform sandbox that blocks mutation tools and filesystem writes; an attempted mutation produces an authoritative deny receipt.
  • Once an instance is terminal, every follow-up is rejected below all public continuation handlers and cannot cause another model request.
  • A bounded wait names one expected terminal/mailbox event and wakes from host activity or its single deadline, without periodic model re-entry.
  • The receipt records requested and effective role/model/sandbox values so fallback or inheritance cannot be mistaken for successful routing.

A minimal regression gate would therefore assert:

  1. missing prerequisite -> no child and no model POST;
  2. terminal integrator -> reuse rejected;
  3. verifier write attempt -> platform denial;
  4. real heterogeneous worker launches -> authoritative effective-model receipts;
  5. quiet wait -> zero fixed-interval polling/model turns;
  6. target worktree unchanged for a read-only gate.

This seems complementary to:

  • #33314 for full-profile application, follow-up continuity, and deterministic finalisation;
  • #32504 for authoritative effective child metadata;
  • #35259 and #15723 for event-driven wait/wake behavior;
  • #32684 for the concrete risk of treating a read-only assignment as a prompt convention;
  • #33097 for lifecycle-hook dispatch gaps.

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 · 19 days ago

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 login field, 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:

  • re-reads the parent's definition table at consume time;
  • rejects receipt creation when any delegatable tool lacks an exact strict-child projection;
  • filters model-visible and dispatchable tools against the sealed definitions;
  • gives the raw dispatchable runtime definition precedence over core-owned display projections; and
  • recomputes the digest immediately before telemetry, hooks, or the handler. A missing table or same-name drift fails closed with zero handler calls.

MCP remains unsupported in strict children. The documented future contract requires a session-pinned normalized catalog and enforceable revision, treats notifications/tools/list_changed as 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:

  • wait blocks on host status/mailbox watch channels plus one deadline, not model-mediated polling;
  • Completed is resumable and retains the same sealed ceiling;
  • close/removal is the terminal addressing boundary; and
  • receipt tombstones are grant-lifecycle state, not agent-terminal state.

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

  • Windows full workspace: 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.
  • Ubuntu 24.04.3 VMware full workspace: 13,667/13,667 passed, 23 skipped, 0 retries/failures. The trap restored kernel.apparmor_restrict_unprivileged_userns from 1 to 1, and /tmp/.git, /tmp/.agents, and /tmp/.codex were absent afterward.
  • Complete codex-core --lib: Windows 2,120/2,120 (3 skipped), Ubuntu 2,215/2,215 (0 skipped).
  • Scoped codex-core Clippy passed without diagnostics on both platforms before the final mechanical just fix / just fmt.

Updated reference commit: https://github.com/ayanamislover/codex/commit/eab87d1f0f035c4bc4f13878a0ab4d90ae8ee16c

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