Central tool execution (CONTEXT.md): Pixir — not the model — runs Tools. The Executor
resolves the tool, validates args against its schema, then dispatches to execute/2
(or dry_run/2 when context.dry_run).
run/2 is the Turn-loop entry point: it records the canonical tool_call Event
before running and the tool_result Event after (both via Pixir.Session, so they
get a seq, hit the Log, and publish). execute_call/2 is the side-effect-free core
(no Events) used directly in unit tests.
Bounded-write denials: feedback once, fatal on the second strike
A bounded-write policy denial is not unconditionally terminal (#446). The
Executor decides and records the denial; Pixir.Turn decides what it costs:
- Strike 1 is recoverable. The
tool_resultrecorded here for the denied call — carrying the denied tool, the requested and normalized path, the matched rule, the policy identity, andnext_actions— reaches the model as that call's tool output on the next provider round-trip, so it can adapt instead of losing the Turn. The rest of the response's tool calls still run. - Strike 2 is turn-fatal. N = 2 is fixed by owner decision. The strike is
keyed on the
:write_policy_deniedkind, so every raising site counts: allowlist miss, deny match, protected path, workspace-root target, child broadening, an outside-workspace bash token, and denials surfaced throughapply_virtual_diff. Each keeps its ownmatched_rule; only the kind is shared, so the strike counter never has to enumerate rules or tool names. - Always logged. Both strikes record a
permission_decisionEvent withdecision: "deny"andgate: "write_policy". Recoverability never reduces the audit record. - Always confessed. Both appear in the
write_denialsfield of delegate envelopes and workflow checkpoints, which is present even when empty. The confession classifies exactly the events the strike counter classifies.
:bash_disabled is a distinct denial kind: never terminal, never a strike,
never confessed. The shell being off is a property of the bounded-write mode,
not a boundary the worker probed.
Summary
Functions
Resolve, validate, and run (or dry-run) a tool call without emitting Events.
Run a tool call within a Session: record tool_call, protect the Session's own
evidence (.pixir), apply the permission policy (ADR 0006), execute (or refuse),
record tool_result. Returns the raw {:ok, result} | {:error, structured} from
the tool.
Types
Functions
@spec execute_call(%{name: String.t(), args: map()}, Pixir.Tool.context()) :: Pixir.Tool.result()
Resolve, validate, and run (or dry-run) a tool call without emitting Events.
@spec run(call(), Pixir.Tool.context()) :: Pixir.Tool.result()
Run a tool call within a Session: record tool_call, protect the Session's own
evidence (.pixir), apply the permission policy (ADR 0006), execute (or refuse),
record tool_result. Returns the raw {:ok, result} | {:error, structured} from
the tool.
The permission policy is read from context.permission (%{mode, asker}); it
defaults to :auto (allow everything) so the common path has zero overhead.