---
title: "Set clearance, egress and content walls"
description: "Set clearance, egress and content walls for tenant admins and governance reviewers."
canonical_url: "https://docs.oz0.ai/govern/taint-and-labels/label-policies"
---
# Set clearance, egress and content walls

> Set clearance, egress and content walls for tenant admins and governance reviewers.

For: tenant admins and governance reviewers.

## Label policies

Rules on labels are conditions you write. **Label policies** are settings the platform enforces where the data moves, outside the model: a clearance per agent, egress by level, models by level, and walls between compartments. They are set with `oz0 tenant labels clearance|walls|people|egress` and `oz0 model rules --level-rule`, shown by `oz0 tenant labels <tenant>` and on the **Privacy** page's **Labels** tab, and governed: a change takes a reason, and a loosening waits for another admin where four eyes is on.

### Clearance per agent

An agent's **clearance** is the highest level, and the compartments, of the tool results its model may read:

```yaml [agents/triage.yaml]
name: triage
clearance: {level: internal, compartments: [support]}   # '*' clears every compartment
```

```bash [Terminal]
oz0 tenant labels clearance set triage --level internal --compartments support --reason "triage reads support data only"
```

A result above the clearance (a higher level, or a compartment the clearance does not hold) is **withheld**:

- the model reads a fixed notice that names the tool and says a result was withheld; never its content, its level or its compartments (labels never go to a model provider);
- the job's labels do not take the withheld result's labels, since the model never read it;
- no `post_tool_call` hook sees it;
- the call is recorded: the trace marks the step and the audit trail has `run.result.withheld` with the labels and why, never the text, for every withheld result; the job's page lists the newest 20 under **Withheld**.

The plugin still read the data. The tool ran on the node and returned its result there; withholding keeps it from the model, not from the tool. Give an agent only tools you are content to have read the data.

The tenant's clearance for an agent replaces the one its definition declares. Without either, the agent has no limit, so nothing changes until a clearance is set. A level your list does not hold counts as the lowest, so a wrong name withholds more, never less.

An agent a hand-off reaches runs in the job of the agent that handed off and calls tools under its own name: each of its calls is held to the stricter of its own clearance and the job's agent's. The conversation it takes over is the job's, so what the job's agent read before the hand-off is in it; give the agents of one team the same clearance where that matters.

A result that carries no labels at all (from a node runtime older than labels) is withheld under a clearance, and under a wall while the job holds a walled compartment, since its level and compartments are unknown. Raising, widening or removing a clearance loosens, and so does setting one for an agent the tenant had none for: it replaces the definition's, which may be lower.

### Egress by level

Two halves, for two kinds of call:

- Calls through the tool gate (every tool call the job's workflow decides): a rule of the `label-egress` preset refuses, or asks before, calls of functions that can send data out once the job read something at a level or above. It is a floor like the taint-default rule, so it never loosens a stricter rule, and it starts in shadow mode, so you see what it would decide before it acts ([tool policy presets](https://docs.oz0.ai/govern/tool-policy/review-changes#label-presets)).
- What never passes the tool gate: a harness session's or remote session's own tools (a shell, a web fetch, the terminals an ACP agent opens, the executor of a remote session that runs its tools apart) and a tool server started in a job's own sandbox.<br />

When the tenant lists egress entries for a level at or below the job's, the egress proxy in front of the process refuses every host outside the list of the highest such level, as a denial of kind `label`. The list narrows the other egress lists; it never adds a host. An empty list allows no host.

```bash [Terminal]
oz0 tenant labels egress set confidential --allow api.internal.example.com --reason "confidential data stays inside"
oz0 tenant labels egress set restricted --reason "restricted jobs reach no host"
```

A session's list is the one for the job's labels at the session's start; a session reads through its own tools what the platform never sees, so it is not narrowed further mid-session. Shared plugin servers keep their proxy as it is: their calls pass the tool gate.

A job's tool server that would run on the host's network or in a container or microVM of its own is refused when a list applies, since the list cannot hold there yet; so is a session or sandbox on a node whose binary is older than this feature. Neither is started without the list.

Adding an entry to a level's list, dropping an entry that starts with `!`, adding a list for a higher level that holds a host the list below it did not, and removing a list all loosen.

### Models by level

Data rules can hold for jobs at a level or above: the regions their providers must be in, no retention, and the providers allowed.

```bash [Terminal]
oz0 model rules --level-rule "confidential:region=eu" --level-rule "restricted:region=eu,no-retention,provider=mistral"   --reason "policy 4.2: restricted data stays with one EU provider"
```

The gateway refuses a call that breaks a rule for the job's level with `403 oz0_data_rule`, naming the rule, and records it in the cost log as any refused call.

The job's labels at the gateway are what the edge knows joined with what the node says: the labels the job started with (from the envelope the edge minted, which the call binding names: for a job the platform started from another job's content, such as a job run again, a conversation's next turn, a comparison's or shadow run or a judge job, the compartments and level of that job), the labels the runtime declared at the job's tool calls the edge decided, and the job's labels the runtime sends with each model call.

The node's word can only raise them.

While the edge cannot read the labels the job started with, a model call of a tenant with rules by level waits (`503 oz0_envelope_unavailable`, which the call retries) rather than be decided without them.

The labels never go to the provider.

A harness session's model calls carry no labels of their own and are decided on what the edge knows.

A rule for a level your list does not hold applies to every job.

### Walls

A **wall** says two compartments conflict. Which ones do is your decision:

```bash [Terminal]
oz0 tenant labels walls set matter-a matter-b --reason "opposing parties, case 12"
oz0 tenant labels people set ann@example.com --compartments matter-a --reason "works on matter A"
```

- In a job: a tool result whose compartments conflict with the job's is withheld, as above.

Only tool results are checked: a child's or a colleague's answer, a board note, a file and an event still join their compartments, so a job can come to hold both sides through them.

Keep agents that work on conflicting matters from delegating to, asking or sharing a board with each other.

### Content shown to people

A person cleared for one side of a wall cannot read the content of jobs on the other side. The check covers every path that shows what a job said or what its steps read and wrote:

- Job steps and chat turns. A walled turn shows facts only. A chat's title and preview appear only where the turn they came from is open to the reader.
- Board notes, checked per note against its writer's compartments.
- Feeds, including the console's chat drawer and activity, and `WatchJob` with its SSE and AG-UI streams.
- Tenant exports and data-subject bundles.
- The audit trail's private parts, checked against the job's compartments when the edge recorded each entry. An entry whose job labels could not be read then, or that kept none, is closed.
- A chat channel's thread running as the person's own account. The platform posts the answer, result card and approval arguments to the room only while that account may see them. Otherwise it posts facts and a link to OZ.
- Measured-savings comparisons using recent job inputs. Each run starts with the source job's compartments and level.
- Running a job again. A person closed out by the wall cannot restart it. A permitted new job holds the old job's compartments and level from its start, before a node runs it.
- Inbound events. Their text and attributes are what the jobs they started received, so those jobs' compartments decide access. For a chat message, the check also uses the chat containing its turn. Routing that names no readable job, or is not yet recorded, is closed to a person subject to walls. A chat message whose routing is not recorded is closed to everyone. A message refused inside a thread is checked against that thread's turn.
- Approvals. Permission to decide an approval reveals no extra content: no pending arguments, narrow "always allow" rule made from argument values, edit, typed confirmation, arguments digest or answer to a question while the wall closes the job to the decider.

### Failures, children and live views

A failure's text is content too, whatever the failure kind, because it can repeat what a step read. A closed job or turn shows only the failure kind and a fixed sentence.

A job that holds a compartment or level reports it however the job fails. A job that reads that failure, or ends with it, holds those labels too. Older runtimes did not preserve labels this way. For their jobs, a failed child closed to a reader also closes the failure text of the jobs above it, in their errors and each step's error. The rest of those jobs stays open, including what their steps read and answered.

Each job in a tree and each turn in a chat is checked against its own compartments. A child or turn on the other side shows facts only even when its root or chat is open. A live view or feed already open stops showing content within about ten seconds after its job joins the other side. The platform checks a live view's final answer again when it sends it.

### Content held by management

Management holds some content without the job's compartments: a routine output's arguments, a legal hold's history copy and the recent inputs used by a measured-savings comparison. It gives that content only to people the walls do not apply to, and explains the refusal to others. Those people can read the content where the edge checks the job's compartments, such as the job itself or the routine's fires.

An audited reason for reading someone else's private chat never opens a wall. A person with no entry is walled from nothing, and nobody can be cleared for both sides. When a job's compartments cannot be read for a person subject to walls, because its labels are unreadable or the job cannot be found, its content is closed to that person.

Only admins see who is cleared for which compartments. The audit trail and a change request's audit entry count the people listed.

- For memory: a memory plugin's records route asks the same walls: a record across a wall from what the reading job holds is not shown to it.

A record is stored with its writing job's compartments, level and trust as the edge reads them (those its runtime wrote and those it was started holding), joined with what the node sends, so a node cannot write a record that leaves its job's compartments out, and a reading job meets the walls with the same compartments; a job whose labels do not read is refused records, and a record whose stored labels do not read is not shown to a reader the walls apply to.

- When compartments overflow: a job keeps at most 64 compartments; past that, its label set holds `*` ("more than can be listed") in place of the rest.

The walls read `*` as holding every compartment, so a job or a result whose compartments overflowed conflicts with anything on either side of a wall, and is closed to every person the walls apply to.

Walls are engineering controls, decided by the tenant. They are not a legal or professional-conduct barrier, and the platform makes no claim that they satisfy one.

Removing a wall loosens.

So does listing a person for fewer compartments, or removing their entry: a person is walled off from the other side of each compartment they are listed for, so fewer entries refuse them less. Listing a person for more compartments tightens.

### Label sources that do not label

A tool that declares a label source (`labels: {source: ...}`) and returns a result without a level gets the tenant's default level. Each such result is recorded (`label.source.unlabelled`), the setup's health checks (`oz0 doctor`) list the functions that did so in the last day with how often, and the `label_source.unlabelled` notification goes to the plugin's owner and the tenant's admins once per function and day ([notifications](https://docs.oz0.ai/admin/notifications)). Counts are per function, never per person.

## Verification and saved examples

The verifier uses a recorded label join to bound a later call only when that call occurred more than two seconds after the join. It bounds a quarantine child through the parent's recorded joins, considering the first 50 in each run.

The workspace and eval store hold no labels. A job with a compartment or a level above the lowest cannot be saved as a flow or kept as an eval case.

A `label-egress` rule retains the level number it was created with when levels are reordered. Review those rules when changing the tenant's level order.


## Sitemap

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