Taint and labels

Read outside text in a quarantine child

Read outside text in a quarantine child for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

The quarantine child

A grow-only join over-blocks: once a job read one web page, every later outbound call asks. oz.quarantine is the way back. An agent that lists it can hand a reading task to a quarantine child: the same agent, run as a child job (<job>/<agent>-q<hex>) that

  • starts with the parent's labels and the data it was given;
  • may call only functions of class read that cannot send data out (no web fetch, no write, no connection, no colleague, no delegation, no board write); the runtime does not offer the rest and refuses them, and the edge refuses any other call the child, or any job under it, schedules;
  • runs alone: an agent's best_of and outcome checks do not run in it, and the edge refuses any child job it starts;
  • answers only with a value of a bounded contract the parent gives (output_schema): booleans, enums of at most 50 identifiers, integers or numbers with both bounds, identifiers matching ^[A-Za-z0-9_.:-]{1,64}$, and objects (at most 20 properties) and arrays (at most 50 items) of these, at most three levels deep and at most 4 KiB as JSON. No free text, though an identifier can still spell a few words (Ignore_the_task); the size bound keeps that short.

Its calls of those read functions never reach their plugin's shared server: each runs in a sandbox of its own, with no network and none of the plugin's secrets, so arguments the data shaped cannot leave through a tool.

agents/screener.yaml
name: screener
runtime: pydantic-ai
model: {primary: default}
tools: [oz.quarantine, mail.read]
delegation: {max_depth: 2}

The parent's workflow checks the answer against the contract. A fitting answer comes back as JSON and joins the child's confidentiality labels into the parent, not its trust: the parent stays trusted. An answer that does not fit fails the call with where and which rule, never the text; the edge checks the shape again when the child completes. The plugin templates quarantine-map-reduce and quarantine-dual-model (oz0 plugin new --template) show the two patterns built on it.

Where the child's calls run

Each tool call of a quarantine child (and of any job under one) starts the tool's server for that call alone, in sandbox tier 1, and stops it when the call ends:

  • No egress. The sandbox's egress proxy allows no host, not the tenant's list nor the plugin's; every request is refused and shows as an egress denial of the child's job. No port of the node is forwarded in: not the LLM proxy, not the host API.
  • No secrets. The server gets none of its plugin's secrets, no connection's token, and no credential or argument binding. Its settings and the manifest's own environment stay: they are not secrets.
  • Nothing kept. The server's folder is removed with it, so nothing one call's arguments shaped is there for the next.

A tool from a container image or a microVM starts with no network and no secrets the same way. Where that cannot hold, the call is not made and the child reads why: a node without sandbox tier 1 (Windows, Linux without bubblewrap), a remote tool (its server is on the network), a tool in a remote sandbox, and a sandbox: own tool.

The child's hooks are the tenant's and run as for any job. A read function that needs the network (an internal search API) cannot serve a quarantine child; give a quarantine agent tools that read what is on the node or what the call carries.

What is fixed and what is not

Fixed checks, outside the model: the default rule (a tainted job's outbound call waits for a person), the quarantine contract (only bounded values cross back), labels joined in the workflow from recorded results, the verifier's check of the tool calls against each other, the start, recorded joins and children, an agent's clearance (a result above it never reaches the model), walls for a job's tool results and for people, the egress proxy's list by level for sessions, their terminals and job sandboxes, and the gateway's data rules by level.

These cases are not covered:

  • A model misuses data it read before any taint, or text misleads the person who reads it.
  • A plugin labels its results wrongly. The default level and the finding are the available controls. The plugin itself reads what it returns, whether or not the result is withheld.
  • A child's or colleague's answer, a board note, a file or an event is above an agent's clearance or across a wall. Only tool results are withheld, so consider this when choosing whom an agent may delegate to, ask or share a board with.
  • The default taint rule does not gate a harness session's own shell or web fetch, although the session starts tainted. Its egress list is set at the start.
  • A runtime drops joins before it records them.

No attack-success figures are published for taint yet; when they are, they will come from adaptive attacks, not one static benchmark.

Runtime and evaluation limits

The edge rechecks a quarantine child's answer only when it handed out the completing workflow task itself. If another edge handed out that task, the parent's runtime supplies that check.

A quality gate runs eval cases unattended. If the taint-default rule asks for approval, the case can wait until its timeout. Vouch for the relevant function, narrow the rule, or evaluate the rule in shadow mode. plugin test disables it in its throwaway tenant.

The routine-output tool asks before a tainted fire's outputs on Pydantic AI agents. Harness sessions and OpenAI Agents loops use the routine's own output approval instead.

Files and board notes stored before labels were available read as untrusted. The console shows labels on the job and trace, but not in the job list.

Copyright © 2026