One Agent
One Agent policy and review
How a one-agent tool call becomes allow, ask or deny, and the safety layers around a run (posture, review, the denial breaker, secret redaction, untrusted-content screening).
Every tool call in a loop is classified, then decided. One function makes the decision, so every door (Studio, REST, MCP, the CLI, the scheduler) gets the same answer, and the rung that decided is written to the run's audit row.
Classes and presets
A call has one of five classes: read, write, send, delete or pay (how each tool gets its class is on Tools). The permissions map on a task, and the stored presets on a saved bot, give a class or a tool name one preset:
| Preset | Effect |
|---|---|
allow | The call runs. |
ask | The run pauses for approval. |
deny | The call does not run; the model gets an error that names the reason. |
hand_off | A deny that does not retry. The call never runs, and the run's result carries a [handed off to you — not run: …] line so the owner knows it is theirs to do. A hand-off does not count toward the denial breaker. |
A preset this build does not recognise is treated as ask, never allow. hand_off is understood only by One Agent; for any other agent the engine rewrites it to deny.
The decision order
The first rung that applies decides:
- Posture
read_only:write,send,deleteandpayare denied. So aredelegate,invoke_workflowand every MCP tool, and any tool with an unknown class (fail closed). - Explicit
deny: on the tool's name or on its class. Posture adds a classdenyon every mutating class. hand_off.- The money floor: an operation in the always-gated set (
buy,purchase,pay,transfer,send,send_payment,withdraw,fetch_paid,shop) is alwaysask. Noallowlowers it, and it pauses the run even when unattended. - The preset: the tool's own entry, then its class entry. Anything other than
allowasks. - An admin lock: a locked class lifts
allowtoask. It never lifts adeny. - Otherwise
allow.
The presets on the task and the presets stored on the saved bot are merged before the run starts: the task's own entries win per class, the bot's locks replace what the YAML carried, and the lower per-run budget wins. See Permissions and budget.
Posture
posture: read_only makes a run safe to point at the open web. A bot's own permissions cannot lift it, and an admin lock never turns a deny into an allow. The browser tool still reads pages under it. Any value other than read_only is forced to read_only: a typo never widens a run.
Review
review: auto adds one model call that reviews each write, send, pay or delete call the permissions allowed. It answers allow, ask the owner (the normal approval pause) or deny (a tool error that names the reason). A reviewer error asks and never allows. Three denials in a row stop the run with stop_reason: review. It costs one model call per gated action. Any other value is forced to auto.
The denial breaker
Three denied calls in a row, with no executed call between them, stop an unattended run with stop_reason: denied. An interactive run pauses and asks the person how to proceed. Any executed call resets the count.
Secrets stay out of results
Tool results, spilled files, the outcome of an approval and the saved result pass through a redactor first. JWTs, bearer tokens and sk- or ozk_ keys become [REDACTED:<type>]. Card numbers, IBANs and email addresses pass, because they are the owner's own data.
Untrusted content
Text someone else wrote is data, not instructions. The results of web_search, web_fetch, browser, message_bot, every mcp__* tool and call_agent calls to search, MCP or scraping agents are scanned for instruction patterns. A result that matches is wrapped in an <untrusted-content> marker listing the patterns it carries; a clean result is returned as it is. Either way the run is marked tainted. Today the taint is recorded in the audit trail only: a later write or send call writes a row saying it would have been asked, and nothing is blocked because of it. code results are not treated as untrusted.
web_fetch refuses private and link-local addresses.
Other run limits
| Limit | Key | Stops with |
|---|---|---|
| Spend per run | budget_usd_run | stop_reason: budget |
| Wall-clock time | max_runtime_s | stop_reason: timeout |
| Turns | max_iterations | the turn cap |
Cancelling a run also cancels the child runs it started with delegate or message_bot wait: true, and the OceanCode sessions it started with code.