---
title: "Taint and labels"
description: "How a job's labels track what it read: outside text taints the job tree, a tainted job asks before tools that can send data out, a quarantine child reads what its parent should not, and label policies withhold, narrow egress, pick models and wall off compartments by label."
canonical_url: "https://docs.oz0.ai/govern/taint-and-labels"
---
# Taint and labels

> How a job's labels track what it read: outside text taints the job tree, a tainted job asks before tools that can send data out, a quarantine child reads what its parent should not, and label policies withhold, narrow egress, pick models and wall off compartments by label.

For: tenant admins and governance reviewers.

A job carries the trust and data labels of what it reads. Those labels follow results and child jobs so later calls can be checked against the tenant's rules.

Every job carries a **label set**: what it has read, as far as the platform can tell. The label set has two parts:

- Trust: `trusted`, or `untrusted` once the job read text from outside, with up to 20 reasons (where it came from, which function and which call; never what the text said).
- Confidentiality: a level from your tenant's ordered list (lowest first) and a set of compartments, which tool results declare.

The labels are joined outside the model, in the job's workflow, from what the job's history recorded. They only grow: untrusted wins, the higher level wins, compartments unite. Tool-policy rules read them, and the default rule asks a person before a tainted job calls a tool that can send data out.

<callout color="warning" icon="i-ph-warning">

Taint **limits** prompt injection; it does not stop it. It makes the platform ask before text a job read can carry data out, and it never lets text from outside hide in a sub-agent. It does not stop a model from misusing what it read before any taint, a tainted summary from misleading the person who reads it, or a plugin from labelling its results wrongly.

</callout>

## What taints a job

A job is untrusted from its start when an outside event started it:

<table>
<thead>
  <tr>
    <th>
      Start
    </th>
    
    <th>
      Reason (<code>
        source
      </code>
      
      )
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A webhook's or an event's delivery, a routine fire whose trigger carried a payload
    </td>
    
    <td>
      <code>
        event
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A chat message
    </td>
    
    <td>
      <code>
        channel
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      An A2A call
    </td>
    
    <td>
      <code>
        a2a
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A call through the edge's MCP server
    </td>
    
    <td>
      <code>
        mcp_caller
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A turn an app started through the <a href="https://docs.oz0.ai/build/apps/ag-ui">
        AG-UI route
      </a>
    </td>
    
    <td>
      <code>
        agui
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Any other job an outside event started (the sandboxes' <code>
        outside
      </code>
      
      )
    </td>
    
    <td>
      <code>
        outside_start
      </code>
    </td>
  </tr>
</tbody>
</table>

Whoever starts it, a job whose agent may run on a model the tenant has not [vetted](https://docs.oz0.ai/spend/models-and-keys/data-rules#provenance-and-vetting) (a local model, or one behind an endpoint of the tenant's own) is untrusted from its start too, with the reason `unvetted_model` and the model as its `tool`: an unvetted model's answers are treated as text from outside. Such a job also runs on sandbox tier 1 only.

A job a person or an app starts with `StartJob`, and a scheduled routine fire without a payload, start trusted. So do the flows and routine fires of the built-in [`inventory` source](https://docs.oz0.ai/build/parts/event-sources/platform-events#the-inventorys-changes): the platform writes those events about its own records, and they carry no text from outside. Such a job still runs where outside-started jobs run, and what its tools read taints it as always: a directory's answer to a registration plugin's call is an open-world result like any other, so the next outbound call after it asks.

During a run, these join untrusted labels into the job:

<table>
<thead>
  <tr>
    <th>
      What the job read
    </th>
    
    <th>
      Reason
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      The result of an <strong>
        open-world
      </strong>
      
       function: one whose annotations do not say <code>
        openWorldHint: false
      </code>
      
      , and every remote server's, whatever its read-only hint
    </td>
    
    <td>
      <code>
        open_world
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A result that declares itself untrusted, or whose tool's manifest says <code>
        labels: {untrusted: true}
      </code>
    </td>
    
    <td>
      <code>
        plugin
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      An event <code>
        oz.wait_for_event
      </code>
      
       returned
    </td>
    
    <td>
      <code>
        wait_for_event
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      What a routine's target answered to an output applied now
    </td>
    
    <td>
      <code>
        routine_answer
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A harness session, or a remote agent's session at its provider (its own tools, such as a shell or a web fetch, run inside it and their results never reach the workflow, so the session taints its job from its start)
    </td>
    
    <td>
      <code>
        harness
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A <a href="https://docs.oz0.ai/build/parts/skills">
        skill
      </a>
      
       the agent read with <code>
        oz.skill
      </code>
      
       from an imported plugin or from another plugin than its own, a <a href="https://docs.oz0.ai/build/parts/skills#a-tools-manual">
        tool's manual
      </a>
      
       of another plugin included, unless that plugin is a verified connector
    </td>
    
    <td>
      <code>
        skill
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A tainted child's result, a tainted colleague's answer, a board note or a file a tainted job wrote
    </td>
    
    <td>
      <code>
        child
      </code>
      
      , <code>
        colleague
      </code>
      
      , <code>
        board
      </code>
      
      , <code>
        file
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      A failed flow tool's or Ask delegation's reason, which the model reads and which can hold the child's own words
    </td>
    
    <td>
      <code>
        child
      </code>
    </td>
  </tr>
</tbody>
</table>

A tenant may vouch for an open-world function whose results it trusts (its own documentation search, say) with `oz0 tenant labels --trusted`. A plugin cannot make its own open-world results trusted.

## Levels and compartments

A tool result declares its labels in its `_meta` under `oz0/labels` ([building tools](https://docs.oz0.ai/build/parts/tools/policy#labels)). A level is a name from your tenant's list; compartments are names such as matters or projects. An unlabelled result of a tool whose manifest declares a label source (`labels: {source: drive}`) gets the tenant's **default level**, never the lowest. A result that declares a level your list does not hold, or malformed compartments, is withheld: the model reads an error, never the content.

```bash [Terminal]
oz0 tenant labels default
oz0 tenant labels default --levels public,internal,confidential,restricted --default-level internal \
  --reason "data classes of policy 4.2"
```

Without a list of its own a tenant has the built-in one: `public`, `internal`, `confidential`, `restricted`, default `internal`. Emptying the list, dropping or moving down a level, lowering the default level or vouching for a function loosens what labels protect: it needs a reason and, where [four eyes](https://docs.oz0.ai/govern/reviewed-changes#four-eyes-for-governance-changes) is on, another admin. The console has the same form under **Govern › Privacy › Labels**, beside the label policies.

Labels and compartments are engineering controls. Rules on them decide what a job may do; they are not a legal or professional-conduct barrier.

## How labels travel

<table>
<thead>
  <tr>
    <th>
      Path
    </th>
    
    <th>
      What happens
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      A child job (<code>
        oz.delegate
      </code>
      
      , a flow tool's flow, best-of candidates and reviewers, Ask's delegations)
    </td>
    
    <td>
      <a href="#labels-on-child-jobs">
        Details
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      An MCP or A2A call that names the job it came from
    </td>
    
    <td>
      its job holds the origin's compartments and level from its start: a call of the same person or key continues the origin's envelope, and one of another caller gets an envelope of its own that still carries the origin's labels
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        oz.delegate
      </code>
      
      's <code>
        data
      </code>
    </td>
    
    <td>
      values passed beside the task, shown to the child's model in a <code>
        &lt;delegated_data>
      </code>
      
       block marked with their trust only (no level or compartment reaches a model provider), never mixed into the task; the child's job carries the labels; every best-of candidate and a harness or remote agent's session get it too
    </td>
  </tr>
  
  <tr>
    <td>
      A colleague's question and answer
    </td>
    
    <td>
      the question carries the asker's labels into the colleague; the answer carries the colleague's back
    </td>
  </tr>
  
  <tr>
    <td>
      The board
    </td>
    
    <td>
      a note keeps its writer's labels; reading joins them
    </td>
  </tr>
  
  <tr>
    <td>
      Files between tools
    </td>
    
    <td>
      a file keeps every writer's labels; a call given the file joins them
    </td>
  </tr>
  
  <tr>
    <td>
      An outcome check's judge that reads the job's files (<code>
        outcome.files: read
      </code>
      
      )
    </td>
    
    <td>
      its model call carries the job's labels and the files'; the labels of the files it was shown join the job's before its reasons reach the agent
    </td>
  </tr>
  
  <tr>
    <td>
      A conversation's next turn, Ask's hand-over
    </td>
    
    <td>
      starts with the labels of the turn it continues: those its runtime kept and those it was started holding, so a turn that ended before a node ran it passes on its conversation's compartments; a turn whose labels cannot be read is not continued; a fork from before the taint starts clean
    </td>
  </tr>
  
  <tr>
    <td>
      Continue-as-new
    </td>
    
    <td>
      the next run keeps the labels, and holds what the job was started holding: the edge copies it from the run's start into the next run's, whatever the runtime's memo says, and an edge that has not seen the run's start holds the continue-as-new (its task is offered again) until one that has
    </td>
  </tr>
  
  <tr>
    <td>
      A job run again (<code>
        oz0 job retry
      </code>
      
      )
    </td>
    
    <td>
      starts with the old job's compartments and level; a job whose labels do not read does not run again
    </td>
  </tr>
  
  <tr>
    <td>
      A comparison's run of a recent job, a rollout's shadow run
    </td>
    
    <td>
      holds its job's compartments and level from its start; what its calls read joins its labels as in a real run, so its floors ask where the real run's would (nobody is asked in a shadow run, so such a call is not made)
    </td>
  </tr>
  
  <tr>
    <td>
      A sampled job's judge job
    </td>
    
    <td>
      holds the judged job's compartments and level from its start, as a comparison's run does; its envelope carries them, so the rules by level hold the judge's model call as they held the job's; a job whose labels do not read is not judged
    </td>
  </tr>
  
  <tr>
    <td>
      A governance replay's continuation
    </td>
    
    <td>
      made only from a job that holds no compartment and the lowest level: a job that does is not continued
    </td>
  </tr>
  
  <tr>
    <td>
      A kept eval case, a draft flow saved from a job
    </td>
    
    <td>
      made only from a job that holds no compartment and the lowest level: they keep what the job said where no wall reaches
    </td>
  </tr>
</tbody>
</table>

### Labels on child jobs

starts with its parent's labels; its result's labels join the parent's. It holds at least the compartments and level its parent was started holding from its own start, before a node runs it: the edge copies them from the parent's start into the child's, and an edge that has not seen the parent's start holds the child's start (its parent's task is offered again) until one that has.

Only the edge writes what a job was started holding: a value a node's runtime puts into a child's start, a continue-as-new or its job's memo is dropped.

The child's authority envelope carries the join of its parent's labels and its own, so the rules by level at the gateway hold for every descendant of a job that started holding a level, whatever the child's start says; the offline verifier reports a hop whose labels hold less than its parent's

The job's page and `oz0 job get` show the labels; the trace marks the step whose result tainted the job.

Labels that cannot be read are read as strictly as they can be. Where the server and the edges read labels that do not validate, or none where a call, a job's start, a file, a record or a result should carry them (the edge's checks, file and record labels, traces and replays), they count as untrusted, at the highest level and holding every compartment, never as the lowest.

On a node, a child's or a colleague's answer whose labels do not validate counts as untrusted, and one without the stamp every runtime with labels writes counts as untrusted at no less than the level and compartments it names.

The offline verifier holds a call recorded with no labels to the lowest set, so any label proven before it counts as a difference.

A trust word that is neither trusted nor untrusted is untrusted; a default level your list does not hold is its highest level.

## Related tasks and reference

- [Set rules on taint and labels](https://docs.oz0.ai/govern/taint-and-labels/rules)
- [Set clearance, egress and content walls](https://docs.oz0.ai/govern/taint-and-labels/label-policies)
- [Read outside text in a quarantine child](https://docs.oz0.ai/govern/taint-and-labels/quarantine)

<legacy-sections>
<span id="clearance-per-agent">

clearance per agent

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#clearance-per-agent).

<span id="egress-by-level">

egress by level

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#egress-by-level).

<span id="label-policies">

label policies

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#label-policies).

<span id="label-sources-that-do-not-label">

label sources that do not label

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#label-sources-that-do-not-label).

<span id="models-by-level">

models by level

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#models-by-level).

<span id="rules-on-labels">

rules on labels

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/rules#rules-on-labels).

<span id="the-quarantine-child">

the quarantine child

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/quarantine#the-quarantine-child).

<span id="the-taint-default-rule">

the taint default rule

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/rules#the-taint-default-rule).

<span id="walls">

walls

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/label-policies#walls).

<span id="what-is-fixed-and-what-is-not">

what is fixed and what is not

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/quarantine#what-is-fixed-and-what-is-not).

<span id="where-the-childs-calls-run">

where the childs calls run

</span>

: [Read the section](https://docs.oz0.ai/govern/taint-and-labels/quarantine#where-the-childs-calls-run).

</legacy-sections>


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
