Skip to content

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

Open

Description

@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;
  2. reject lexical non-subsets before any path existence/canonicalization oracle;
  3. compile the requested child policy and prove it is no broader than the immediate parent;
  4. acquire backend-owned filesystem object leases where path-scoped authority requires them;
  5. 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;
  6. revalidate and atomically claim the complete snapshot below all public spawn handlers, before
    thread creation; and
  7. 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?
  2. Is the Linux descriptor-lease handoff (openat2/O_PATH through bubblewrap FD binds) the right
    backend contract to standardize?
  3. Where should the cross-platform capability object and durable strict provenance live so a partial
    landing cannot expose a bypassable mode?
  4. Should live-child expansion require a replacement receipt/guardian, or should a strict child be
    permanently non-expandable?
  5. 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

  1. added
    enhancementNew feature or request
    subagentIssues involving subagents or multi-agent features
    sandboxIssues related to permissions or sandboxing
    on Jul 31, 2026
  2. changed the title [-][RFC] Require a monotonic per-delegation permission preflight before spawning subagents[/-] [+][RFC] Strict delegation: host-enforced monotonic authority ceilings for subagents[/+] on Aug 1, 2026
  3. sattyamjjain commented on Aug 4, 2026

    @sattyamjjain

    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.

  4. jaebit commented on Aug 6, 2026

    @jaebit

    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:

    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.

  5. ayanamislover commented on Aug 8, 2026

    @ayanamislover
    Author

    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: 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

  6. Roppon-64 commented on Sep 30, 2026

    @Roppon-64

    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:

    • Mandatory pre-spawn invocation. Every admitted native delegation must involve a fresh call to the configured, authenticated authorizer before child creation. A model instruction to consult policy, or notification after creation, would not satisfy this requirement.
    • Typed decision and constraints. The authorizer can return 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.
    • Receipt-gated creation. A valid external receipt is a prerequisite for the host’s existing strict-delegation admission/consume path. Missing receipts cannot fall back to ordinary spawning.
    • Exact binding. The receipt binds the canonicalized spawn request (or its cryptographic commitment), unique call identity, task identity or commitment, parent, resolved role, authority snapshot, and returned constraints, with a bounded expiry. Constraint application must produce a sealed effective authority covered by that binding; it must not silently change what was authorized.
    • Fail-closed validation. Replayed, expired, stale, or mismatched receipts—and unavailable authorizers, timeouts, malformed responses, unsupported versions, or identity/configuration drift—must prevent child creation.

    A possible integration with this RFC is:

    Existing strict preflight and authority snapshot
      -> required external authorization
      -> enforce and bind any narrowing constraints
      -> existing atomic receipt validation/consumption
      -> native child creation
    

    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.

  7. virajchoudhary commented on Oct 13, 2026

    @virajchoudhary

    Current-source confirmation from a Windows verifier-isolation investigation, 2026-10-13.

    At upstream 90f00bd89ed3263b4d5993849cccb795f4330449, both spawn versions route through prepare_agent_spawn_config in codex-rs/core/src/agent/child_config.rs. It applies the role and then calls apply_spawn_agent_runtime_overrides, which copies the live parent's permission-profile snapshot and approval policy. The typed AgentRoleOverrides in codex-rs/core/src/agent/role.rs also 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.

  8. virajchoudhary commented on Oct 13, 2026

    @virajchoudhary

    Independent reference admission checks and the remaining Windows OS gate

    I checked the published reference commit eab87d1f0f035c4bc4f13878a0ab4d90ae8ee16c on native Windows x64 with Rust 1.99.0 through just 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::Disabled can prepare a managed read-only child snapshot, while an unsandboxed child request is rejected with UnsupportedPolicy. 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 CodexSandboxOffline and CodexSandboxOnline accounts and stable firewall rule names. Its provision_sandbox_users generates fresh passwords, and ensure_local_user falls back to NetUserSetInfo for an existing account. A separate CODEX_HOME changes 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsandboxIssues related to permissions or sandboxingsubagentIssues involving subagents or multi-agent features

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions