Skip to main content

Sub-agents and the sandbox

An agent that can only do one thing at a time is a chatbot with a job title. The interesting work, scanning an estate, reviewing a codebase, decomposing a mission, is fan-out: one agent spawning many workers and answering for what they do.

That is also where an agent platform becomes dangerous, so this is the most heavily gated path in the product.

A sub-agent is addressed by lineage, not registered as an entity​

The first design decision is the one that makes the rest tractable.

A sub-agent is deliberately not written into the directory. Ten thousand ephemeral workers registered as entities would replicate around the fabric and leave an estate permanently full of the dead.

Instead a sub-agent is addressed by its lineage, a path from the root agent through each spawn: agent:…/sa-…/sa-…. A node's parent is that path minus its last segment, so the whole tree is derivable with no edge store at all.

Both kinds of team, durable directory teams and ephemeral lineage teams, run behind one membership interface, so the orchestration code does not fork.

Spawning: the model supplies intent, never authority​

When a model asks to spawn workers, the request it can express carries no identity fields whatsoever. Not an issuer, not a lineage, not a root entity, not a cell. Those are resolved server-side from the parent's sealed grant.

There is a load-bearing check in the middle of that resolution: the custody key loaded for the parent must match the parent's directory trust root byte for byte. A mismatch is a hard refusal, not a warning.

The grant a child receives is minted as requested intersected with parent, and the structural invariants are checked rather than trusted:

InvariantMeaning
Depth strictly decreasesA spawn chain terminates by construction
Every authority axis is a subsetA child cannot hold a permission its parent lacks
Clearance never risesClassification cannot be laundered through a spawn
Permission mode no more permissiveA restricted parent cannot mint an unrestricted child
Fan-out remaining decreasesBreadth is bounded as well as depth

Escalation is not prevented by a check that could be forgotten. It is impossible to express.

The gate that decides whether to spawn at all​

Before any of that, one question: is this fan-out worth making?

The net-potential-gain gate is consulted with the actor, those affected, the fan-out width, the objective and the estimated cost. Two details matter:

  • It is evaluated once per fan-out decision, not once per worker. A thousand-worker fan-out asks once. The decision being made is "should this happen", not "should this one more worker happen", and asking per worker would answer a different and easier question a thousand times.
  • It is fail-closed on both negative and unknown. A net-negative verdict refuses, and so does insufficient evidence. A neutral verdict allows and logs.

Admission, and why deep trees do not deadlock​

A spawn also has to fit. Workers are admitted against per-resource-class caps: processing, egress, inference, and coordination.

The coordination class is the interesting one. A coordinator waiting on its children releases its processing slot and occupies a coordination slot instead. Without that, a deep tree deadlocks: every processing slot is held by a parent waiting for a child that cannot be admitted because its parent holds the slot. Over-cap requests get backpressure, never oversubscription.

The cap itself is adaptive rather than static. It derives from available cores minus a reserve that preserves failover headroom, throttled by observed host load, and reduced by live peers already running on the cell. A cell already saturated by other work refuses a new spawn instead of degrading everything on it.

Above the cell, a fabric-wide population ceiling and a spawn-depth ceiling apply. Those fail open on a registry read error, deliberately: a metrics hiccup should not wedge the whole fabric.

Budget is leased, not granted​

A child's allocation is a time-limited lease split out of the parent's remaining budget, across tokens, wall-clock, processing, memory and egress.

Three consequences:

  • The subtree can never exceed the root, structurally, because every child's allocation is subtracted from what its parent still holds.
  • A crashed or killed worker returns its budget when its lease expires. There is no leak requiring a sweeper to notice.
  • An agent with no registered budget ledger cannot spawn at all. The resolver returns nothing and the coordinator refuses to mint. Fail-closed, so an unbudgeted agent is not an unbounded one.

The sandbox a worker runs in​

Where a worker gets a shell, it runs in a sandbox whose specification is compiled purely: a budget goes in, an exact isolation specification comes out, and the compiler itself runs nothing.

The isolation is structural rather than configured:

  • Every namespace unshared, including the network, and the environment cleared.
  • Remapped to an unprivileged identity.
  • The root filesystem read-only; one writable workspace directory; a size-bounded temporary filesystem.
  • Dies with its parent, in a new session.
  • Process-count, memory and processing caps derived from the budget. The process cap is explicitly the fork-bomb guard.

There is no host home directory, no host configuration, and no credential path representable in the specification. The only route outward is an egress broker bound as a file, never as a network route.

Two properties are worth calling out because they are where sandboxes usually leak:

Keys live outside the jail. A worker's private key is held by a broker in the parent process; the sandbox receives only an opaque handle. The broker signs on the worker's behalf with domain-separated tags and zeroizes on release. A process that can read its own memory never has a key in it.

Reset is by discard, not by cleanup. Sandboxes are leased from a warm pool and released by discarding the overlay, so no state survives into the next worker. An anomalous sandbox is quarantined, destroyed and never returned to the pool.

The pool size is not just a performance setting: it is the admission control. Pool capacity and resource admission are deliberately one mechanism rather than two that can disagree.

Fail-closed, always

Where the host cannot provide the isolation, the runtime refuses to spawn. It never degrades to running a worker unsandboxed. A degraded host loses the capability rather than quietly losing the boundary.

Supervision while it runs​

A spawned worker is watched, not just launched. A sensor samples load trend and behavioural drift into a verdict, and the signal is pushed into a shared registry so peers see it too.

Verdicts split by severity: throttle and pause are advisory; kill and quarantine tear the worker down and journal an intervention.

Alongside that runs behavioural detection, the equivalent of user-behaviour analytics pointed at agents: per-task-class baselines over tool-call pattern, egress attempts, token rhythm and file-access shape, plus honeytoken canaries. Its actuator is quarantine plus a cascade revoke. The detector is built; driving the live actuator from it is on the roadmap.

Every mint is journaled, strictly​

A spawn that cannot be recorded does not happen. The mint is written to the durable journal before the grant is handed out, and a failure to journal refuses the spawn.

This ordering is deliberate. The alternative, hand out the grant and record it afterwards, produces exactly the artifact nobody can explain later: a worker that acted with authority nobody can account for.

Because ephemeral cells are destroyed with their job, mint records also land in the durable ledger that outlives the process, rather than only in the memory of the least durable thing in the deployment.