Concepts

How a tool call is decided

Every check a model's call of a tenant's tool meets, in the order the code runs them, with what each one decides, what it can answer and the code that does it.

For: cluster operators and plugin authors.

A model's call to a tenant MCP function passes checks in three places: the job workflow on its home node, the edge that sees the scheduling command, and the home node's activity that makes the call. The reference below follows the code's execution order, including the checks after a call.

Each check says what it decides and what it can answer:

AnswerWhat happens
RefuseThe call is not made, and the agent reads why.
AskA person decides before the call goes on.
ChangeThe call goes on with other arguments: a hook's modify, or a person who edited the call before approving it.
StubA shadow run answers from a stub instead of making the call.
WithholdThe call was made, and its result does not reach the model.

The tool policy is one of these checks, not all of them. Hooks, grants, approvals, the secret scan and the kill switches are others. A call one check lets through still meets every check after it, so a person can approve a call that a later check refuses: the secret scan and the kill switch run on the node when the call is made, after the approval and after the edge.

The Code column names the file and the function that does each check, as they are in this repository: oz0_runtime/ is the node runtime's package, under node/runtime/src/, and the Go paths (edge/, runtimeapi/) are under server/internal/. The docs' own check fails when one of them no longer exists, so the list cannot drift from the code without someone noticing.

Before the gate

A model's call first meets what the job knows of the agent's run so far. A flow's tool: step and a tool called by hand have no model and no turn: they start at the gate. A flow step that names a platform built-in (oz.report_outcome, oz.evidence or oz.propose_update, in a step) meets the same checks as an agent's call of it: the routine's tool list, the tenant's refuse and approval rules for its name, a shadow run's stub, and the edge's own checks of the built-in (oz0_runtime/agents/flowbuiltins.py call).

#CheckWhat it decidesCan answerCode
1Tools withdrawnOnce the job tree reached its budget's soft wall, a person denied a forecast's early request, or results in a row added nothing, the agent has only its answer left: every tool is withdrawn for the rest of the run, and a call the model still makes is not made.Refuseoz0_runtime/agents/catalog.py _tool_caller; oz0_runtime/spend.py withdrawn
2The budget argumentA function that takes a budget gets one when the model left it out, before anything decides the call.Changeoz0_runtime/agents/catalog.py call_tool; oz0_runtime/fitbudget.py fill
3The turnCalls join their turn's cohort before awaiting anything if they may need approval: requires_approval, policy Ask, a matching pre_tool_call hook, a conditional rule, or a completed loop whose on_loop asks or stops the job. Every other non-read-only call joins too, allowing the turn's calls to wait for these decisions.Nothing yetoz0_runtime/agents/catalog.py call_tool, loop_decides
4Loop detectionA call that completes a loop the agent's loop detectors find is handled as the agent's on_loop says: reported, sent to a person, or the job stops. It is handled once the call joined its turn, so the turn's approvals hold it and its turn's other calls wait for its decision.Ask (kind loop), or the job stopsoz0_runtime/agents/catalog.py looping
5Shadow rulesWhat the tenant's shadow rules would have decided is recorded, from the call as the model proposed it.Nothing: it recordsoz0_runtime/wouldhave.py reports

The gate

The gate is everything that decides a call in the job's workflow before it is made. It runs in this order:

#CheckWhat it decidesCan answerCode
6Flow toolsA flow tool's call that would start more child jobs than the agent's max_children, deeper than its max_depth, or inside its own flow, is refused before anyone is asked.Refuseoz0_runtime/agents/catalog.py flow_tool_refusal
7QuarantineA quarantine child calls only functions that read and cannot send data out.Refuseoz0_runtime/agents/catalog.py quarantine_refusal
8GrantsDefault-deny: the agent declares the function in its tools:; no two plugins provide its tool ID; another plugin's function needs the uses: grant, read again as it is now; the tool is installed on the job's home node.Refuseoz0_runtime/reach.py refresh; oz0_runtime/allow.py refusal
9A routine's toolsA routine's fire calls only the tools the routine lists.Refuseoz0_runtime/routines.py allows
10The policy's plain refusalA function the tenant's policy refuses for every call (a rule without a condition, or a type's default), when no rule with a condition names it, is not called, and no hook is asked.Refuseoz0_runtime/agents/catalog.py _gate
11Shadow stubsIn a shadow run, a call that does not only read, or one a hook the run cannot ask would see, answers from a stub.Stuboz0_runtime/agents/catalog.py shadow_stub, unasked_guard
12pre_tool_call hooksThe hooks that match the call, in order. A hook may allow, deny, modify the arguments or escalate to a person (an approval of kind hook); an escalate-only hook, such as the guardrails plugin's command guard and intent check, may only allow or escalate. A hook's own function is under the tenant's policy too: refused, the hook counts as failed; on Ask, a person decides before the hook is asked.Refuse, Ask, Changeoz0_runtime/hooks.py run, _tenant_allows
13The policy's decisionFor a function a rule with a condition names, those rules decide the call on the arguments the hooks left; for any other function, what the policy decided when the job started (Allow or Ask) stands. The function's requires_approval asks unless the policy refuses.Refuse, Askoz0_runtime/agents/catalog.py _gate, decide_call
14Taint and labelsThe floors: the taint-default rule and label presets raise the decision by what the job read. They never lower it.Refuse, Askoz0_runtime/taintflow.py floor
15Where arguments came fromThe origin check of role-marked arguments, and the job's sequence rules. They only raise the decision.Refuse, Askoz0_runtime/origingate.py decide
16A changes approvalA call a changes approval decided before it came here goes on with that approval's binding, unless the agent's changes policy, not a person, made that decision and the tenant's policy sends the call to a person.Nothing newoz0_runtime/agents/catalog.py _gate, tenant_approves
17The personWhen anything above asks, a person decides, as an approval. A per-job allowance (Allow the same call in this job) answers yes without asking. When the person decides, the edge checks that they may (the deciders, four eyes) and re-checks a long wait.Ask, Change (an edit)oz0_runtime/agents/catalog.py approve; oz0_runtime/approvals.py Approvals.request, Approvals._allowed; runtimeapi/identity.go Service.checkDecision, Service.recheck
18The approval's bindingThe approval is bound to the function's snapshot and the policy's version as the home node has them when the person is asked. When either changed while the person decided, the person is asked again.Ask againoz0_runtime/agents/catalog.py current_binding; oz0_runtime/worker.py ToolActivities.binding
19The long-wait re-checkA yes that came after a wait longer than the tenant's threshold is checked again on the home node: the grant read fresh, the function still installed, and the node's current policy, conditions included. It only refuses.Refuseoz0_runtime/agents/catalog.py _recheck; oz0_runtime/recheck.py check; oz0_runtime/worker.py ToolActivities.recheck
20After approval and waitsConditional rules, taint floors, origin requirements and sequence rules are checked again using the approved arguments and the job's current state, immediately before dispatch. A new refusal or a different required approval stops the call.Refuseoz0_runtime/agents/catalog.py _gate, final_policy_check

Origin receipts are bounded. If one call has too many protected input fields to attest, split it into smaller calls. The runtime refuses an oversized attestation, and the edge refuses missing evidence for a protected argument.

The turn

A model often asks for several calls in one turn. A call of the turn that does not only read waits until every call of the turn that waits for a person is decided, and is not made when one of them was denied (one turn waits as a whole).

#CheckWhat it decidesCan answerCode
21The turn's barrierThe call waits for its turn's decisions, and is refused when one was denied.Refuseoz0_runtime/barrier.py TurnBarrier.hold

Making the call

#CheckWhat it decidesCan answerCode
22The approved argumentsA call is made only with the arguments a person approved: their digest must equal the approval's.Refuseoz0_runtime/agents/catalog.py _dispatch
23A flow tool's childA flow tool's call starts its flow as a child job instead of a call on the node. The home node checks the binding, the snapshot, the kill switches and the secret scan first; the starter's quota admits the child; then the edge checks the child's start against the tree's limits and budget.Refuseoz0_runtime/agents/catalog.py _flow_tool; oz0_runtime/flowtools.py check_activity; oz0_runtime/childquota.py check; edge/uses_admission.go UsesCheck.childAdmission
24The origin receiptThe call carries a receipt of its origin check and sequence verdicts, decided again on the arguments it is made with, which the edge reads.Nothing: it recordsoz0_runtime/origingate.py receipt

At the edge

The job's workflow sends the command that schedules the call through the edge, which checks it before Temporal takes it. A refusal here fails the job, as a refusal of the policy does (ToolPolicyRefused for the policy's own). The edge checks only refusals: whether a person approved a call is the job's own history.

#CheckWhat it decidesCan answerCode
25The call's shapeThe call names its plugin, and that plugin provides the tool; a call that runs in the job's own sandbox has that sandbox's shape.Refuseedge/uses.go UsesCheck.toolCall; edge/uses_ownsandbox.go ownSandboxShape
26A hook's callThe edge checks the hook, event, caller and execution location. Policy conditions and auditing apply; grants, duties, sequences and origins do not. See hook-call details.Refuseedge/uses_hooks.go UsesCheck.hookCall
27Own sandbox and quarantineA call in the job's own sandbox is one the job may make there; a quarantine child calls only closed-world reads.Refuseedge/uses_ownsandbox.go UsesCheck.ownSandbox; edge/quarantine.go quarantineTool
28The policy's conditions and refusing floorsFor functions named by conditional rules or refusing floors, the edge rechecks arguments against its current tenant policy. It uses the node's exact input keys and refuses keys differing only in case. Function matching follows the node's Python fnmatchcase behavior. This check can only refuse.Refuseedge/uses_conditions.go UsesCheck.conditions, mayCondition; edge/uses_input.go toolCallFields
29Duty rulesA call that would complete a pair of duties the tenant refuses for this agent.Refuseedge/duties_p378.go UsesCheck.duties
30The grant and the envelopeAnother plugin's function needs the job's plugin's grant, and must be within the job's authority envelope.Refuseedge/uses.go UsesCheck.toolCall; edge/uses_scopes.go UsesCheck.envelopeScope
31Tree sequences and originsThe tree's within: tree sequence rules fired in another job, then the origin rules that refuse, from the call's receipt.Refuseedge/uses_sequence.go UsesCheck.treeSequences; edge/uses_origins.go UsesCheck.originRefusals
32No audit entry, no callThe edge writes run.tool_intent to the audit trail; the call is not scheduled until it can.Holds the calledge/uses_intent.go UsesCheck.intent
33Revoked authorityA job a live revocation reaches (a kill switch, a narrowed grant, a removed plugin) is refused, or held while a park revocation lasts.Refuse, or holdedge/uses_fence.go UsesCheck.fenceCommand

Hook-call checks at the edge

The hook must belong to the named plugin and be called by the job's own agent or another agent of its plugin. It must run where the job's hooks run.

Its input must describe an event the platform asks hooks about, belonging to that job and matching the hook's kind, agent and tool filters. The header must name the same event.

No grant is needed. Policy conditions and the audit entry still apply; grant, duty, sequence and origin checks do not.

A look at a long call that runs as an MCP task is checked as the call it follows (edge/uses_tasks_p380.go UsesCheck.toolTask).

On the node

The home node's activity makes the call. Before it does:

#CheckWhat it decidesCan answerCode
34The bindingAn edited call must fit the function's input schema; an approved call is not made when the function's snapshot or the node's current policy version differs from the one the person approved under. The agent reads that it may call again, which asks again.Refuseoz0_runtime/worker.py ToolActivities._binding_refusal, ToolActivities._edit_refusal
35A drainA node that is stopping holds the call until its workers stop; the call is made on the next runtime.Holdoz0_runtime/worker.py _held
36The toolThe tool is not withheld (two plugins provide its ID), not a flow tool, its server is up, and the function is one its server listed.Refuseoz0_runtime/plugins.py PluginHost.call
37The approved snapshotThe function's listing is the one the tenant approved.Refuseoz0_runtime/plugins.py PluginHost.check_approved
38Kill switchesA kill switch that stops the function, its tool, the agent or the tenant, or a tool retired in the inventory.Refuseoz0_runtime/plugins.py PluginHost.check_killed
39The secret scanArguments that carry the value of one of the tenant's secrets (calls that carry a secret).Refuseoz0_runtime/secretscan.py scan_arguments_async
40Quarantine, secrets and grantsA quarantine child's call runs in a sandbox of its own with no egress, or is not made; a secret placeholder goes only where an argument binding puts it; the connection's token is added; a plugin that reads the inventory or the setup needs its grant.Refuseoz0_runtime/plugins.py quarantine_refusal, remote_placeholder; oz0_runtime/inventory.py fetch; oz0_runtime/setup.py snapshot
41Kill switches againChecked once more under the tool server's lock, right before the call goes to the server.Refuseoz0_runtime/plugins.py PluginHost.call
42While it runsA kill switch that now stops the call, and the tool's time limit, cancel it at the tool.Stops the calloz0_runtime/worker.py _beating

After the call

#CheckWhat it decidesCan answerCode
43An unknown outcomeA write whose outcome is unknown (its node was lost, or a task ran past its limit) waits for a person or fails the job, as the agent's on_unknown says.Askoz0_runtime/unknown.py made
44Labels and clearanceThe result's labels join the job's (and may taint it); a result above the agent's clearance, or across a wall, is withheld from the model.Withholdoz0_runtime/labelpolicy.py withheld_why; oz0_runtime/taintflow.py join
45The origin indexThe result's values join the job's origin index, and a tree sequence rule it fired reaches the edge at once.Nothing: it recordsoz0_runtime/origingate.py joined, announce
46post_tool_call hooksThe hooks that match may redact the result, or withhold it.Change, Withholdoz0_runtime/hooks.py run
47OffloadA result over the job's offload threshold reaches the model as a stub, after the hooks saw the whole.Changeoz0_runtime/offload.py stub_text

Harness tools

A harness session's own tools (Claude Code's Bash or WebFetch, an ACP agent's execute) are not MCP functions of the tenant, and take a shorter path. The session asks the node's runtime whether it may run a call; only names on the job's ask list go on to the job's workflow, and every other call runs at once.

#CheckWhat it decidesCan answerCode
H1The runtime's own requestsA session may not call the runtime's own tools this way.Refuseoz0_runtime/harness.py _Answers.decide
H1aMCP resourcesClaude's tools that list or read MCP servers' resources (ListMcpResourcesTool, ReadMcpResourceTool and their other names), whatever the agent declares: a session is offered a server's functions only.Refuseoz0_runtime/sessiontools.py is_resource_tool
H2Default-denyA tool the agent does not declare in harness.tools (or its harness's default tools).Refuseoz0_runtime/allow.py harness_refusal
H2aThe MCP nameA Claude session's mcp__<tool>__<function> must name exactly one function the session was offered of its own servers, matched by the harness's spelling of the function's name. Any other is refused.Refuseoz0_runtime/sessiontools.py function_of
H2bThe node's check of an MCP functionThe node's plugin host must list the function, its listing must be the approved snapshot's, and no kill switch may stop it.Refuseoz0_runtime/harness.py _node_refusal
H3The secret scanThe call's input carries the value of one of the tenant's secrets. It runs here, before any hook.Refuseoz0_runtime/secretscan.py scan_arguments_async
H4The ask listOnly names a pre_tool_call hook matches (every name, for a hook that names no tools), the agent's harness.requires_approval, the harness's shell tools (for the command rules) and, in a routine's fire or a shadow run, every name, go to the job's workflow. A shell command the harness runs without asking reaches no rule.Nothing yetoz0_runtime/agents/workflow.py AgentWorkflow._run_harness; oz0_runtime/agents/catalog.py harness_approvals; oz0_runtime/commandcheck.py patterns
H5Tools withdrawn and loopsAs for a model's call, in the job's workflow.Refuse, Askoz0_runtime/agents/workflow.py AgentWorkflow._session_loop
H6Routine and shadowA routine's fire calls only the tools it lists; a shadow run stubs what does not only read.Refuse, Stuboz0_runtime/agents/workflow.py AgentWorkflow._permission
H7pre_tool_call hooksAs for a model's call. The guardrails plugin's command guard, where a tenant installed that plugin, leaves a shell command the runtime decides itself to the command rules below.Refuse, Ask, Changeoz0_runtime/hooks.py run
H7aCommand rulesThe home node parses the post-hook shell command (Bash, bash, ACP execute) against command rules and platform defaults, enabled without installation. A Refuse matching any part wins; otherwise the first matching rule decides. Unreadable commands ask. Approval after a long wait is rechecked against the node's current rules.Refuse, Askoz0_runtime/commandcheck.py check; oz0_runtime/commandcheck_node.py decide_activity; oz0_runtime/commandrules/rules.py Decider.decide
H8The personA name in the agent's harness.requires_approval waits for a person.Askoz0_runtime/agents/catalog.py approve

A harness's own tools meet no function rule, no floor, no edge check and no per-call kill switch check, and an approval of one has no long-wait re-check (it needs a home node); its shell commands meet the command rules (H7a), whose own re-check runs on the job's home node. The sandbox bounds what such a session reaches. A session's calls of the tenant's MCP tools, through the platform's own MCP server, take the whole path above (oz0_runtime/agents/workflow.py AgentWorkflow._session_tool).

A Claude session of an imported agent whose install opted in to harness extras runs the plugin's MCP servers itself. Its call mcp__<tool>__<function> is the plugin's function <tool>.<function>. The harness writes every character of the function's name outside letters, digits, _ and - as _, so issues.delete is mcp__jira__issues_delete, and the name is matched by that spelling.

After H2 and before H3, a name that is no function the session was offered is refused (H2a), and so is a function the node may not run (H2b).

Every other call goes to the job's workflow, whatever the ask list says. There the name is resolved again against the job's own functions, and the call meets the function's path above without being made: the routine, default-deny, the tenant's refusals, conditions, floors and origin checks, its hooks (once, with those that name only the session's spelling first) and a person, then the agent's harness.requires_approval (AgentWorkflow._session_extra, catalog.decide_session_call).

The server's result goes to the session, never to the workflow, so a clearance or a wall that withholds unlabelled results refuses the call instead.

The session is offered, of each server, the functions the node's plugin host lists with their approved snapshot, among those the job's policy knows. Two functions the session would spell alike are both left out.

Each server the session runs itself sits behind a pin that lists exactly those functions and refuses a call of any other (oz0_runtime/mcppin.py). The pin also refuses the server's resources, prompts and completions, and of the server's own requests passes only pings and the session's roots, so no resource text, prompt or sampling request reaches the session.

Of the server's answer to initialize the session hears only the protocol version, the tools and logging capabilities and the tool's name: no instructions or other text of the server's.

The pin gives every request it passes on an id of its own, and passes an answer back only by exactly that id, once, rewritten as the answer to the request it recorded; any other answer is dropped and logged.

So what the session's model reads of the server is the approved listing and the results of its calls, whatever the server writes. A relayed server offers the same functions and nothing else (oz0_runtime/guarded.py).

Which policy applies

Each check reads the tenant's tool policy at its own time:

  • The job's runtime decides with the rules the job started with. A job reads the policy once, when it starts on its home node, and keeps what it decided in its history, so a job that runs for days keeps that policy, and a replay decides the same (oz0_runtime/worker.py job_start_activity; oz0_runtime/agents/catalog.py job_policy). Grants are the exception: they are read again before each call of another plugin's function.
  • An approval binds to the policy the home node has when the person is asked, and to the function's snapshot. A job without a home node, such as a harness's own tools, binds to the policy it started with. When the version changes while the person decides, the person is asked again; when it changed after the yes, the node does not make the call, and the agent may call again. Neither applies the new rules: the call asked again is decided, as before, with the rules the job started with (oz0_runtime/agents/catalog.py approve; oz0_runtime/worker.py ToolActivities._binding_refusal).
  • The long-wait re-check uses the node's current policy, and only refuses.
  • Command rules are decided with the rules the job read at its start, like the rest of the job's policy: a change reaches only jobs that start after it, and a rule's end still stops it inside a running job; a yes to a command after a long wait is decided again with the node's current command rules (oz0_runtime/commandrules/rules.py Decider.decide; oz0_runtime/commandcheck.py check).
  • The edge uses its current copy of the tenant's policy, but only for functions that a rule with a condition or a refusing floor names, and for origin and tree sequence rules (edge/registry_toolpolicy.go ToolPolicies.For).

So a change of the policy reaches a running job like this:

ChangeReaches a running job
A new AskNeither at the edge nor in the runtime: only jobs that start after it ask.
A new plain Refuse (no condition)Only at a long-wait recheck using current node policy, or at the edge when the function also has a conditional rule or refusing floor. A changed approval binding triggers another approval or call, but still uses the job's starting rules, so approval lets it through. Otherwise the new refusal affects only new jobs.
A new conditional Refuse, a refusing floor, an origin or tree sequence ruleAt the edge, at the job's next call of the function.
A command rule, of any actionOnly in jobs that start after it, and through the long-wait re-check of a person's yes to a command.
A narrowed grant, a kill switch, a removed pluginAt the next call: in the runtime (the grant), on the node (the kill switch), and at the edge.

To stop a running job's calls of a function now, use a kill switch, not a rule.

Budgets and tool calls

No budget refuses a single tool call. A job tree's budget and the tenant's quotas meter model calls:

  • At the tree's soft wall, or after a person denied a forecast's early request, every tool is withdrawn and the agent can only answer (row 1).
  • A flow tool's child start meets the starter's quota and the tree's budget at the edge (row 23).
  • A tool's own model calls (MCP sampling, or a call through the node's gateway path) are budgeted at the gateway as the job's, like the agent's own.

See Budgets and quotas.

Copyright © 2026