---
title: "Rules for taint, origins and sequences"
description: "Rules for taint, origins and sequences for tenant admins and governance reviewers."
canonical_url: "https://docs.oz0.ai/govern/tool-policy/origins-and-sequences"
---
# Rules for taint, origins and sequences

> Rules for taint, origins and sequences for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

## The taint-default rule

Every policy has a built-in rule, `rule-taint-default` with preset `taint-default`: `*` Ask, when `job.tainted && tool.outbound`. Once a job read untrusted input (an outside start, an open-world result, an event), each call of a function that can send data out waits for a person, and the card says the taint asked.

It is a floor: it takes no part in the first-match order, and only ever raises what the rules and classes decided, so it never loosens a refusal and an allow rule before it does not get past it.

Narrow its condition (`--taint-default-when`, below), set it to shadow mode or turn it off (`--taint-default off`) like any rule, with a reason; [Taint and labels](https://docs.oz0.ai/govern/taint-and-labels) says what taints a job and how to recover with a quarantine child.

A tenant that trusts a service with what its jobs read leaves that service's calls out of the rule's condition, and every other outbound call of a tainted job still asks:

```bash [Terminal]
oz0 tenant tool-policy set --reason "Jev is our triage service: reading tickets is its job" \
  --taint-default-when 'job.tainted && tool.outbound && !(mcp.tool.target == "jev" && mcp.tool.name == "decide")'
```

## Where arguments came from

A function's input schema can mark a sensitive argument with a role (`x-oz0-role`: `recipient`, `amount`, `host`, `path`, `payee` or `repository`) and a floor (`x-oz0-origin`: `request`, `identity`, `record` or `any`): how trusted the place its value came from must be ([Tools](https://docs.oz0.ai/build/parts/tools/policy#where-arguments-came-from) shows the keywords). Each job keeps an index of the values it may legitimately use, built from what its history recorded, never from what the model says:

<table>
<thead>
  <tr>
    <th>
      Origin
    </th>
    
    <th>
      What it covers
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        request
      </code>
    </td>
    
    <td>
      the input of a job a person or an app started (not a webhook's, a chat's or another outside event's)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        identity
      </code>
    </td>
    
    <td>
      the starter's own account name and the addresses their identity provider verified
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        record
      </code>
    </td>
    
    <td>
      results of functions whose schema says <code>
        x-oz0-origin-source: record
      </code>
      
       and that the tenant vouches for (<code>
        --origin-record
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        any
      </code>
    </td>
    
    <td>
      anything else the job read: every other tool result, a child's answer, an outside start's input
    </td>
  </tr>
</tbody>
</table>

### Origin floors and value comparison

A floor accepts its origin and every more trusted one.

A word the checks do not know is read as strictly as it can be, never as nothing to check: a floor word that is no floor (a misspelt `requets`, or one a newer version wrote) is `request`, and an `x-oz0-role` that is no role still marks its argument, which is then checked as an exact string.

A URL counts by the host a browser would connect to: one whose host part holds a backslash, a space or other control character, or a letter outside ASCII is no host at all, so it is checked as an exact string too.

Values are compared as units of one kind, normalised the same way everywhere: an email address (`Ann@Example.com` is `ann@example.com`), a host (a URL counts by its host), a repository (`owner/name`, from a URL too), a path, an IBAN, a phone number, an amount (`1,200.50` is `1200.5`), or else the exact string.

A value that is merely part of a longer one does not match: `ann@example.com` is not in `joann@example.community`.

A child job starts with what its parent could vouch for (its request, identity and records, at most 512 values, the most trusted first), so a recipient a person gave the parent passes in the child.

A result is read for values up to 256 KiB of its text.

After the conditions and the taint-default rule, and before an approval, every role-marked argument with a floor is checked. A value below its floor is `origin.unknown`: the built-in rule `rule-origin-default` (preset `origin-default`) asks a person, and the card says which argument, its role, its floor and where its value came from, never the value.

The trace and the [audit trail](https://docs.oz0.ai/govern/audit-trail) record the same.

Like the taint-default rule it is a floor: it never loosens a decision.

Turn it off with `--origin-default off` (a loosening, with a reason), or keep a rule of its preset in shadow mode to see what it would ask; a rule of the preset in shadow mode, with a weaker action or with an end counts as a loosening too.

An agent's [changes policy](https://docs.oz0.ai/agents/approvals) never stands for a person's approval the origin check or a sequence rule asks for.

A tenant can set floors for functions it does not own, and make them refuse instead of ask, with an origin rule:

```bash [Terminal]
# a payment's payee must be one the person named, or a CRM record the tenant vouches for
oz0 tenant tool-policy set --reason "the CRM's customers are records" --origin-record crm.lookup_customer
oz0 policy refuse 'payments.*' --origin role:payee=record --origin amount=request --reason "payees from the request or the CRM"
```

`--origin` names arguments, or every argument of a role as `role:<role>`. Vouching for a record function (`--origin-record`) loosens what passes, so it takes a reason too, and a second admin where four eyes are on.

The edge decides a refusing origin rule again, and the origin-default rule when you keep it refusing (with the floors the tool's schema sets): a call it names that carries no record of the check (a node runtime from before origin checks) or whose own check came out below the floor is refused there too.

It reads which arguments a tool marks from the plugin's approved version it already holds, so it decides the same while it cannot reach management.

A call by hand and a channel's post are no model's calls: origin rules do not apply to them.

## Sequence rules

A sequence rule decides a call by what came before it in the job, or anywhere in its job tree:

```bash [Terminal]
oz0 policy ask '*' --after-level confidential --then-outbound yes --within tree \
  --reason "confidential reads, then nothing leaves without a person"
```

### Sequence matching and enforcement

The first call and the later one are each matched by `--after-function` and `--then-function` (a pattern), `--after-class` and `--then-class` ([action classes](https://docs.oz0.ai/govern/tool-policy/conditions#action-classes), repeatable), `--after-plugin` and `--then-plugin`, and `--after-outbound` and `--then-outbound` (`yes` or `no`); the first call can also match the [labels](https://docs.oz0.ai/govern/taint-and-labels) of its result (`--after-trust`, `--after-level`, a level at least as high, and `--after-compartment`, any of them).

`--within` is `job` (the default) or `tree`.

`oz0 tenant tool-policy set --gate-rule` takes the same rule as JSON (`{"match": "*", "action": "approve", "sequence": {"after": {...}, "then": {...}, "within": "tree"}}`).

A rule fires once the result of a matching call has come back (a result without labels counts as untrusted), and an `after` that names only labels also fires on the labels the job started with: a job a webhook or an event started is untrusted from its start.

Each job's runtime decides the rules for its own calls, and a child starts with the tree rules its parent fired and hands its own back when it ends.

The edge, which sees every tool call of every job of the tree, decides `within: tree` rules again for the tree's other jobs.

A job's runtime tells the edge which tree rules fired as soon as the result that fired them comes back, not only with the job's next tool call, so a sibling meets the rule from its next call: when a rule fired in one job and a sibling's later call matches its `then`, a refusing rule refuses that call, and an asking rule refuses it unless a person approved it (its runtime could not know, so the edge fails it rather than let it pass).

A job's own calls are its runtime's to decide.

What fired counts from the tree's start: a job ID used again starts a new tree.

Sequence rules are floors too, and take a mode, an end and a reason like any rule.

Two shipped presets are off until a tenant adds them:

```bash [Terminal]
oz0 tenant tool-policy set --keep-rules --reason "after untrusted input, a person decides" --preset untrusted-then-consequential
oz0 tenant tool-policy set --keep-rules --reason "confidential, then egress asks" --preset confidential-then-egress
```

- `untrusted-then-consequential`: after a result that is untrusted, a call of class `write`, `destructive`, `egress` or `host.*` asks, within the job.
- `confidential-then-egress`: after a result labelled at least `confidential`, a call that can send data out asks, within the tree.


## Sitemap

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