# `Pixir.Tools.Executor`
[🔗](https://github.com/Ranvier-Technologies/pixir/blob/main/lib/pixir/tools/executor.ex#L1)

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.

# `call`

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

# `execute_call`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
