Pixir.Tools.Executor (pixir v0.1.13)

Copy Markdown View Source

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_result recorded here for the denied call — carrying the denied tool, the requested and normalized path, the matched rule, the policy identity, and next_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_denied kind, so every raising site counts: allowlist miss, deny match, protected path, workspace-root target, child broadening, an outside-workspace bash token, and denials surfaced through apply_virtual_diff. Each keeps its own matched_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_decision Event with decision: "deny" and gate: "write_policy". Recoverability never reduces the audit record.
  • Always confessed. Both appear in the write_denials field 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

call()

@type call() :: %{call_id: String.t(), name: String.t(), args: map()}

Functions

execute_call(map, context)

@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.

run(map, context)

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.