Skip to content

Named tools and ToolRuns

This page names project-declared tools and the ToolRun records sase tool run writes for each execution. A ToolRun is an execution record, not an LLM Calls row.

Try it

sase tool                                   # list the catalog with LAST and TYPICAL
sase tool run check                         # run `just check`, record a ToolRun
sase tool run test -- tests/tool            # extra args, only where the tool allows them
sase tool run -- sh -c 'echo hi; exit 3'    # ad-hoc argv; the `--` is mandatory
sase tool runs -n 5                         # newest recorded runs for this project
sase tool show RUN                          # one run: stages, evidence, samples
sase tool show RUN -l                       # replay the retained full stdout/stderr
sase tool show RUN -j                       # the complete versioned JSON record

Catalog provenance

Named tools live under tools: in the project's own sase/sase.yml and are read as complete entries. User, machine, plugin, and builtin config cannot supply or alter argv; a tools: key in those layers is diagnosed and ignored. Malformed entries are errors (exit 2) that name the entry and field, and nothing is spawned. Argv is never expanded for shell syntax or environment variables; write [sh, -c, ...] when a shell is wanted. Named tools run at the project root, ad-hoc commands at the invocation directory. See the field reference.

tool_runs: (retention and log caps) is separate operational policy and follows ordinary config precedence.

Duration classes

Each catalog entry may declare duration_class: short | long | unbounded, the floor below which the tool essentially never finishes:

Class Floor Meaning
short (default when unset) 0 s Can finish in minutes; always fits inline.
long 600 s (10 m) Essentially never finishes in under 10 minutes.
unbounded none (no upper bound) Waits on something external; never fits under any ceiling.

A class is a floor, not a forecast: check stays short on purpose (it fails fast, and its over-ceiling tail is reactive escalation's problem), while check-full is long (a superset of the full test suite). The Rust core owns the class enum, the floor table, and validation; an unknown value is a catalog error (exit 2) naming the entry. Like receipt:, the field is excluded from the definition digest and serialized only when declared, so declaring or changing a class never moves TYPICAL history, triage identity, or receipt coverage, and undeclared entries stay byte-identical.

sase tool list shows the effective class in a CLASS column and calibrates each declaration against the corpus: each JSON tool gains duration_class and duration_calibration (object or null; the envelope schema_version stays 1), and each calibration summary prints as one stderr diagnostic line in human mode. Calibration stays silent below 10 samples and otherwise suggests the class the TYPICAL numbers support; it never gates a run. Ad-hoc runs have no class and are always short.

Mixed installed cores fail closed: a core older than the duration-class wire rejects duration_class as an unknown catalog field, so the pin bump and the first declaration land together and just install-venv heals a stale wheel (same rollout as receipt:).

Project identity

Every run is recorded under the project identity of the repository that owns its catalog, and an ad-hoc run under the identity of its working directory's repository. That one identity fills runs.project, fingerprint_before/after.project_identity, and fingerprint.repos[0].identity, and it is what sase tool runs (without -a) scopes to. So sase tool run check from a linked repo checkout such as sase/repos/linked/sase-core is recorded as that repo's check, not as the host project's, and the two never share history, LAST, or flake evidence.

A repo that is a registered project keeps its registry key (sase-core, gh_sase-org__sase). Any other repo gets a stable key derived from it: gh_<owner>__<repo> from a GitHub origin, otherwise its directory name with a _<N> workspace suffix stripped, so every numbered checkout of one repo shares an identity. A repo nested inside another checkout is never claimed by it. SASE_PROJECT names the agent's own project and is not consulted.

A stored row keeps the identity it was recorded with. See Automation migration notes.

Stage recording

A stage recorded by tools/run_silent is one row of the enclosing run. The stage command runs without SASE_TOOL_RUN_EVENTS and SASE_MONITOR_DIAGNOSTICS_DIR, so a nested tools/run_silent (or a test that invokes one) records nothing into the enclosing run's stage timeline or monitor diagnostics. SASE_TOOL_RUN_ID stays, so a nested sase tool run keeps its parent link. The test suite scrubs those variables and SASE_TOOL_RUN_ID at session start, which covers stages: none tools such as test; a test that needs them sets them explicitly. SASE_TOOL_RUN_DEMAND deliberately passes through: a stage that leases pytest workers reports its grant to the enclosing run's demand record (see below).

Demand recording

Every recorded run also captures the demand evidence a future admission design needs: provider and ceiling context, process-tree CPU and memory, and pytest worker grants with token-wait time. sase tool show RUN renders the record, and show -j carries the raw run.demand object. Three writers contribute, each fail-open — a recording failure warns at most once per process and never changes the child's result:

  • The agent-side starter records the provider (SASE_AGENT_LLM_PROVIDER and friends, with the SASE_TOOL_RUN_PROVIDER overlay as fallback) and the synchronous kill ceilings. A foreground run and a starter-scoped (detached) reservation record ceilings; a plain hand-off reservation records the provider only. The adopt worker never writes context, so it cannot overwrite the starter's facts.
  • The executing wrapper reaps the child with wait4 for CPU seconds and max RSS, and samples live tree RSS on its ~10 s tick. CPU covers the child plus every descendant that exited and was reaped inside the tree; max RSS is the largest single process, not a sum.
  • The child reports pytest worker grants over SASE_TOOL_RUN_DEMAND=<run log dir>/demand.jsonl, one JSON line per grant, so any project's test runner can report grants. tools/run_pytest pops the variable before pytest starts, and the test suite scrubs it at session start, so nested runs never append to the parent's record.

A lost run (wrapper killed) records no usage and no grants; that gap is not reconstructed.

A grant line is {"schema_version": 1, "kind": "worker_grant", "run_id": "<id>", "grant": {...}} with the grant carrying grant_id, source, observed_ts_ms, lane (scoped, fast, cov, ...), path (serial, gear, lease, or bypass), requested_floor, requested_ceiling, granted (0 for a refused or timed-out lease), budget, wait_ms, selected_files, and escalated_from.

The reader is bounded: at most 64 KiB per file, lines over 4 KiB skipped, at most 64 grants, exact-int types only, and an out-of-range value drops that grant with an ignored out-of-range demand grant <id> diagnostic instead of failing the whole request. Usage carries typed availability reasons instead of zeros: rusage unavailable when the child settled without wait4 figures, and tree RSS unavailable on this host when the host has no process tree to sample.

Run output versus retained output

A human at a terminal gets the child's stdout and stderr passed through unchanged; the wrapper's own metadata goes to stderr. Every run also retains full output privately, capped per run, and it is only shown by sase tool show RUN -l; truncation is stated explicitly. Default JSON never contains child output.

See the generated sase tool run page for -q, -T, and -v. When retained output overflows tool_runs.run_log_max_bytes, the child is still drained and streamed in full. The run footer, sase tool show RUN -j (output_truncation), and sase tool show RUN -l each state how many bytes were dropped per stream, so a capped log is never mistaken for a complete one.

Precedence: an explicit -q or -v wins; otherwise a direct agent invocation (SASE_AGENT_NAME) defaults to compact and everything else streams. When the run is already owned by a running monitor or proc that captures the output, the tool writes no duplicate log and records the owner id and parent run instead. An inherited monitor or proc id whose owner has already settled (agents launched by a monitored epic launch inherit that id) is stale: it owns nothing and is not recorded. A SASE agent is always a new ownership root: agent launches scrub every inherited SASE_TOOL_*, SASE_MONITOR_*, and SASE_PROC_* variable (including SASE_TOOL_BYPASS), and a run started from a SASE agent process never attributes itself to an ancestor's monitor, proc, or parent run.

Stream fidelity

The child gets a single merged pipe (stdout and stderr into one stream, one pump) whenever a single target receives both streams: when an enclosing owner holds the output (monitor logs, 2>&1 tails), or when the wrapper's own fds 1 and 2 name the same device and inode. Only the merged mode preserves the child's stdout/stderr interleaving; two pipes pumped on two threads regroup output by stream. Inline compact mode (-q, or the agent default) retains separate stdout and stderr logs and therefore keeps two pipes even when the wrapper's fds share a target.

Merged runs retain the interleaved bytes in the stdout log; the stderr log stays empty. sase tool show RUN -l replays each retained file to its own stream and claims no total order between two retained streams: order across stdout and stderr is meaningful only for merged runs.

History and retention

sase tool runs filters by -t tool, -s state, -A agent, -a all projects, and pages with -n/-c. sase tool list reports LAST (the newest native result) and TYPICAL (the median of up to 30 normally exited runs in 30 days with the same definition, no appended args). No samples render as an em dash, never as an ETA. sase disk list shows the ToolRun owner, and sase disk reap previews its retention.

Retention keeps run summaries for summary_days, detailed stage and sample rows for detail_days, and retained output and event files for log_days after settlement. log_max_bytes is an aggregate target: once age has been applied, the oldest settled runs' files are selected until the remainder fits. Unsettled runs and their files are never selected; if they alone exceed the target, the preview and apply report the over_target_bytes instead of hiding it. Reclaimed bytes count only files actually unlinked (no-follow, confined to the ToolRun root), never SQLite row deletion.

Incomplete evidence

Missing observations are recorded as absent with a typed reason: an unsupported host field, a probe that timed out or exited non-zero, an input that does not exist, an exhausted observation budget. They are never replaced with zero, and a run is never called mutated_input unless both fingerprints are complete. A run whose wrapper was SIGKILLed is later reported lost, without a guessed duration or exit code.

Failure semantics

The child's exit code is returned, or 128+signal; catalog and usage errors return 2. If recording fails (unwritable store, lock contention), the command still runs exactly once with the same argv and exit code, a single warning is printed, and no durable id is claimed. SIGTERM and SIGINT are forwarded and produce 143/signaled and 130/interrupted.

Under a live monitor or proc owner the child shares the wrapper's process group, so the owner's stop reaches the whole tree and the wrapper does not escalate on its own. An inline run puts the child in its own session and escalates a forwarded SIGTERM/SIGINT to SIGKILL on the child's process group after 5 seconds; on Linux the child is also killed if the wrapper dies. When a later sase tool run finds a run whose wrapper is gone (the run is settled lost), it reaps that run's leftover process group with SIGTERM, then SIGKILL after 2 seconds, but only while the group leader still matches the recorded child's start identity; a reused PID is never signaled. sase tool runs and sase tool show reconcile lost runs without signaling anything.

Failure triage

Every settled named-tool failure is extracted into durable failure items. An item is classified as NEW, KNOWN, FLAKY, or UNKNOWN: KNOWN needs an independent witness; FLAKY needs the reproducible-flake baseline; and missing or insufficient evidence is UNKNOWN, not an assertion that the failure belongs elsewhere. A verdict summarizes the stored items (new_failures, no_new_failures, or unavailable triage) without changing the command outcome. sase tool show RUN and sase tool show RUN -j retain the items, evidence, possible owners, stage decisions, and verdict after logs are reaped.

For stages: run_silent tools, agent-attributed runs use known continuation: a failed stage continues only when it has one or more items and every item is KNOWN or FLAKY. -x restores fail-fast and -k continues every failed stage. All three modes preserve the first continued failure's exit code when tools/run_silent --finish closes the recipe. If a continued failure reaches a child exit of zero without that finish record, the wrapper reports a failed run and exits 1 rather than manufacturing success. A plain recipe line after a continued failure is not a run_silent stage and keeps that line's own exit code, just as just does.

sase tool failures groups current signatures by class, tool and stage, and reports runs, agents, first/last seen, and possible owners. Its default scope is the catalog repository; linked-repository runs are a separate identity and never witness this repository's items. Rows created before catalog-owned identity was fixed remain history. Renaming a stage description starts a new stage key and therefore breaks continuity. Thin ledgers deliberately produce mostly UNKNOWN items until independent evidence has accumulated. Classification uses the stored rule version and evidence, with the min_witnesses and touched_requires_clean_witness knobs recorded alongside it.

Inline-then-escalate

Every agent sase tool run starts inline from the agent's point of view, and a run still going near the caller's synchronous ceiling moves into a monitor under the same ToolRun id without being cancelled or rerun. A short tool such as check settles inline inside the budget. A run that outlasts the budget prints one shared escalation block and exits 124 while the run keeps going; the agent runs the printed sase monitor start -J ... join next and ends the turn. The explicit legs behind that block — sase tool run -d, the ceiling-bounded sase tool wait / sase tool show -F, and sase monitor start -J — are documented under "Hand-off and lifecycle control" below and share the run's id end to end.

sase tool run refuses an inline run before starting anything (exit 2) when all four conditions hold: the caller is a SASE agent (SASE_AGENT is set), a valid provider synchronous ceiling is present (SASE_PROVIDER_SYNC_CEILING_SECONDS), the run is a named catalog tool, and the tool's duration class floor meets that ceiling (long under a 600 s ceiling; unbounded under any ceiling). The refusal prints why (tool, class, floor, ceiling), states that nothing was started, and prints two runnable forms: a sase monitor start -p verify hand-off with a --next placeholder, and the prepared-completion variant (-f <ref>) with the verification.command argv to seal first. No ToolRun row is written and no child starts.

The class is a floor, not a forecast, so on a correctly declared catalog the refusal can only fire where the ceiling would have killed the run anyway. There is no bypass flag: running inline past the ceiling is killed anyway, and a monitor always works. A misdeclared class is fixed in the project-owned catalog, and with no long or unbounded declaration nothing is ever refused (rollback is the catalog itself). Humans, CI, monitors, and procs never evaluate the check, and ad-hoc runs are never refused.

Hand-off and lifecycle control

A handed-off run has a durable identity before its caller lets go. The launcher reserves a created run that already names its owner (a proc or a monitor), then starts the owner, whose command is a hidden worker that atomically claims that exact run and executes the frozen invocation. The printed handle is durable from the reservation on: the run stays discoverable, followable, waitable, and stoppable through its id, and it settles to either an authoritative outcome or an explicit, typed uncertainty. One ToolRun links to exactly one proc; each -H invocation is a new request, and a run id executes at most once. The owner proc is attributed to a TUI session only when the process that submitted it is itself a live registered session. An agent escalation, -d, and a human shell -H leave it unattributed even when another TUI is open; there is no fallback to the newest TUI session. Unattributed procs remain visible in the Procs tab's this session scope. See Durable Procs.

sase tool run -H check     # returns at once with a run id; the shell may close
sase tool show RUN -F      # stream the run live until it settles, then summarize
sase tool wait RUN         # block until it settles; exit with its exit code
sase tool stop RUN         # ask the owner to stop it; reports requested vs stopped

Agents hand off through sase monitor start (which reserves the run and prints its id before the turn ends) or detach with -d (see "Detached runs" below); sase tool run -H refuses inside an agent or a live owner with the exact monitor form to use instead (exit 2). An agent's inline run of a catalog tool declared long or unbounded is likewise refused before starting when its class floor meets the provider's synchronous ceiling (see "Inline-then-escalate" above).

Detached runs

sase tool run -d/--detach (agents only) starts a normal hand-off run that also carries a starter record naming the agent runner that started it (SASE_AGENT_NAME plus the runner PID from $SASE_ARTIFACTS_DIR/agent_meta.json, paired with boot/start-time identity so a reused PID never counts as proof). Detached output mirrors -H and adds the starter lifetime plus the join hint; -q prints only the run id:

sase tool run -d check     # agent-only; stopped when the turn ends unless joined

An unjoined detached run never outlives its starter. Eagerly, the end of the provider invocation that started it stops every unjoined run scoped to that runner (a monitor handoff records its join first, so an actively joined run is never touched). As a backstop, the adopting worker polls the starter about every five seconds once claimed: while the starter identity is live the run is left alone, and after the starter dies the run survives only while its join names an active monitor — otherwise it is stopped with requested_by: sase and a reason naming the case (starter agent X ended without joining, joining monitor Y ended). sase tool show renders the scope (detached by agent X ...) and the join (joined by monitor Y at T), and detached runs never raise the settlement notification: either the starter reads the result inline or the joined monitor's follow-up does.

Recording is fail-closed like -H: if the starter cannot be resolved, the reservation cannot be committed, or the proc cannot start, nothing starts (exit 1). Usage and refusals exit 2: a human caller (use -H instead), a live owner or parent run, -H together with -d, and -v/-T. -k/-x are allowed and travel as the envelope continuation mode, and long tools are accepted detached since a join can finish them.

Inline-then-escalate

(Agents only.) A plain sase tool run from an agent with a sync budget and no live owner or parent run starts the run detached (same reservation, starter, and continuation envelope as -d) and follows it inline: compact mode follows with inline-identical stage lines and footer, -v streams the proc log. The run's exit is returned when it settles inside the budget. At the budget the follower prints the shared escalation block and exits 124 without stopping the run; SIGTERM, SIGINT, and SIGHUP do the same and exit 143, 130, and 129. If detach cannot start, one sase: inline escalation unavailable (<why>); running inline warning is printed and the command runs inline under a new id. The automatic path does not inherit stdin; the inline path (including that fallback) does.

Ceiling-bounded wait

(Agents only.) An agent's sase tool wait and sase tool show -F never block past the caller's synchronous ceiling: the wait budget is computed by the Rust core from the hard ceiling (SASE_PROVIDER_SYNC_CEILING_SECONDS) and the soft ceiling (SASE_PROVIDER_SYNC_SOFT_CEILING_SECONDS) as the smaller of the budgets that are present — the hard budget is the ceiling minus a margin of 15% clamped to 90–300 s (but never below half the ceiling), the soft budget is the soft ceiling itself, and a tie reports hard. With neither ceiling there is no budget. wait uses the smaller of -t and the budget (a clamped -t prints one notice); show -F stops following at the budget. At the bound both print one shared escalation block on stderr and exit 124 without stopping the run: tool and run id, elapsed time, the budget source, the sase monitor start -J ... join form, the bounded sase tool wait ... -T 200 re-wait form, and the turn-end stop rule. A run that is not joinable (not detached, another agent's run, or joined elsewhere) prints only the still-running line plus the re-wait form — never a join command that would be refused. wait -j adds an escalation object (budget_seconds, source, joinable, join_command) at the deadline; schema_version stays 1. Humans are never bounded.

Monitor joins

(Agents only.) sase monitor start -J/--join RUN hands an existing detached run to a monitor under the same ToolRun id — the join form the escalation block prints. The joining caller must be the agent that started the run, and -J refuses a command remainder, -c, -f (use -n for a follow-up), and -a with exit 2, as do a caller outside an agent, a run with no starter scope, and another agent's run. Settled, stop-requested, and joined-elsewhere races exit 1 with the state and a sase tool show pointer.

The join is recorded atomically after the monitor member exists and before its proc submits (a refusal tears the member down; a submit failure releases the join), and no wrapper resolution or new ToolRun reservation happens for joins. The monitor proc runs the hidden sase tool _join RUN worker: it reasserts the join as an idempotent replay, streams the run's output and stage lines into the monitor log without a deadline, then renders a compact summary with the existing triage footer and mirrors the run's mapped exit code (recorded code, 128+signal, 143 for a requested stop with no code, else 1). On SIGTERM/SIGINT it requests a stop through the proc owner with a reason naming the joining monitor (naming the timeout when the monitor's termination intent says timeout), waits at most 15 seconds, and exits 143/130. The joined run is never settled as monitor-owned: its executing proc stays the owner, and sase monitor show renders the scope and join as before. sase tool stop on a run joined by an active monitor stops that monitor — suppressing its follow-up while the join worker stops the run; with a terminal joining monitor the existing proc-owner path applies. Monitor stop, timeout, and a lost joiner converge with the detached-run watchdog, preserving the stop_requested settlement reason.

Recording failures behave differently by leg. Explicit -H is fail-closed: if the reservation cannot be committed, nothing starts (exit 1, "nothing was started"). Foreground sase tool run stays fail-open as above. A monitor start's reservation is fail-open: it falls back to wrapped execution with one reason line in the monitor log, since the monitor id itself is already durable.

Every settlement records a typed terminal_cause: exited, signal, interrupt, stop_requested, timeout, launch_failed, owner_lost, or wrapper_lost. A stop settles signaled/stop_requested, including before the command started ("command was not run"). Killing any process leaves either a recoverable run or a run that says, in a typed field, why its outcome is unknown — nothing is replayed and no success is fabricated. A reboot yields truthful reconciliation (lost), never a rerun. One ToolRun is not an exactly-once guarantee for external side effects: a lost acknowledgement retried by the caller creates a second run, and each run executes once.

Platform note: on Linux a terminal-launched proc escapes into its own transient systemd-run --user --scope (with OOMPolicy=continue) whenever the user's systemd manager is reachable, so closing the terminal does not reach the run; where no user manager is reachable the proc stays in the terminal's cgroup unless its detach_scope escapes, and closing the terminal then still reaches the run. show on a run whose owner row or logs were pruned reports the owner and the log as not retained, by name — the summary survives.

In the TUI

The Agents tab surfaces each node's runs without ever touching the ledger: a live-only ⚒ chip on the owning row (the monitor row for monitor-owned runs, never the starter), a header chip answering whether the latest check added NEW failures, and the Tools deck's ⚒ Runs card with a stage waterfall, triage items, and a bounded log tail. LLM Calls rows, the slow-tool list, and monitor/proc Context cards link to the run instead of copying it. Admin Center → Tools (tab 7) covers project-wide runs, failure signatures, and the tool catalog. Stopping a run and starting a catalog tool are explicit, confirmed flows; when a hand-off settles, an OpenToolRun notification jumps back to the run. In the Procs tab, Enter on the owner proc or on a monitor that joined the run opens Tools focused on that run, unless the row also has a Command Line tag, which opens its transcript block first. See Opening a row, Agents Tab Tool Runs, and the Tools tab. The TUI never reconciles: a silent run is shown in red and left for its owner (or a backend reconcile) to settle.

Rerunnable harness

just smoke-tool-runs                                    # core smoke + fixture harness
tools/smoke_sase_tool_runs --sase .venv/bin/sase -j     # per-case JSON report and DoD map
tools/smoke_sase_tool_runs --live                       # also real monitor/proc owners and overhead
tools/smoke_sase_tool_runs --keep                       # keep the temp fixtures (path printed)

The harness drives the executable named by --sase (default: sase on PATH) against an isolated SASE_HOME, a private HOME so user config and plugin overlays cannot leak in, a temporary git project, and a known fixture catalog. It inspects the real store through versioned queries; it never runs the expensive SASE catalog, and its only fault injection is at the filesystem and process boundary. The report records which Python and sase_core_rs module the executable actually loaded. Default mode runs every hermetic case and labels the live cases not-run; --live adds a real monitor, a real proc, the hand-off fault matrix (acceptance, stop races, viewer detach, worker crash, delivery, monitor hand-off), and the cold-start overhead measurement. tests/test_sase_tool_runs_smoke.py is the pytest twin and shares the same cases.

A --keep directory holds one world-* subdirectory per case group. Inspect one with SASE_HOME=<kept>/world-NAME/sase-home HOME=<kept>/world-NAME/user-home sase tool runs -a.

Guarded recipes

check and check-full are guarded: run as a SASE agent, just check refuses unless it runs inside sase tool run check for that project root, or with an explicit bypass. test and install-venv are never guarded. The guard is a guardrail against habit, not a security boundary.

sase tool run check                                # the wrapped form agents use
SASE_TOOL_BYPASS='<why>' just check                # run raw on purpose; the value is the reason

The environment contract

Three variables decide everything, and the decision is makeable without starting a sase process — the override exists precisely for when sase is broken. Any repo can implement this table from scratch (linked-repo catalogs do exactly that):

Variable Set by Meaning
SASE_AGENT the agent runner this process tree is a SASE agent process
SASE_TOOL_NAME sase tool run, always the tree is inside sase tool run <name>, or ad-hoc
SASE_TOOL_PROJECT_ROOT sase tool run, always the resolved project root the named run executes in; empty for an ad-hoc run
SASE_TOOL_BYPASS an agent or human run raw on purpose; any non-empty value bypasses, and the value is the reason
SASE_PROVIDER_SYNC_CEILING_SECONDS the agent runner the caller's provider-harness kill ceiling in seconds for one synchronous command; set around each provider invocation, scrubbed at agent, monitor, and proc boundaries, unset when the provider declares none
SASE_PROVIDER_SYNC_SOFT_CEILING_SECONDS the agent runner the most time an agent should block on one sase tool run before escalating to a monitor in seconds; it kills nothing; set around each provider invocation from tool_runs.soft_ceiling, scrubbed at agent, monitor, and proc boundaries, unset when none is configured

SASE_TOOL_NAME is exported whether or not recording succeeded: recording stays fail-open, so a guard keyed on the run id would refuse the child of a fail-open sase tool run check and tell the agent to run the command it is already running.

SASE_TOOL_PROJECT_ROOT exists so a wrapper marker from one checkout cannot satisfy another checkout's guard: the guard requires the name and the root to match. Ad-hoc runs (SASE_TOOL_NAME=ad-hoc, empty root) never match a guarded recipe's name.

The guard rule

An agent may run a guarded recipe only from inside sase tool run <that tool> for that project root, or with an explicit bypass. The match is strict: sase tool run -- sh -c 'just install-venv && just check' does not satisfy the check guard, because the named identity is what run history, fingerprints, and receipts key on.

The guard is tools/require_tool_run, a dependency-free POSIX sh script wired as the first just dependency of each guarded recipe, ahead of _setup. It refuses — it does not redirect — printing the wrapped form and the bypass form and exiting 2:

  • SASE_AGENT unset (humans, CI, finalizers, monitors, procs): allowed, silently.
  • SASE_TOOL_NAME equals the tool and SASE_TOOL_PROJECT_ROOT is the recipe's own root: allowed, silently.
  • SASE_TOOL_BYPASS non-empty: allowed, with one stderr line naming the reason.
  • sase not on PATH at all: allowed with one stderr line — fail open, since there is no wrapped form to print.
  • Otherwise: refused with the wrapped and bypass forms on stderr, exit 2.

Monitor wrapping

The guard cannot see detached runs: monitor supervisors scrub agent identity on purpose. verify-profile monitors therefore wrap what they run — an exact catalog match upgrades to a named sase tool run <name>, anything else wraps ad-hoc — in the proc argv only, leaving the recorded command and -f completion bindings untouched. See Monitors (§"Tool-run wrapping") for the policy table and the monitor.tool_wrap config field.

sase tool

Named-tool catalog and foreground ToolRun commands. With no subcommand, sase tool defaults to sase tool list.

Moved to sase tool.

Moved to Named tools and ToolRuns.

Command behavior

Humans default to exact stdout/stderr passthrough; wrapper metadata goes to stderr. Direct agent execution (SASE_AGENT_NAME) defaults to compact output. -q forces compact and -v forces streaming. Recording failures warn once and still execute the child exactly once without inventing a durable id. Catalog, usage, and missing-run errors exit 2. sase tool run returns the child's exit code, or 128+signal.

Each recorded run captures a bounded pre/post fingerprint of configured repositories, input globs, allow-listed environment names, and toolchain probes. It also samples host load at start, roughly every ten seconds, and finish. sase tool show reports evidence completeness, whether complete fingerprints prove an input mutation, dirty-path counts, toolchain summaries, and samples. A missing repository/input, timed-out probe, unsupported host field, or observation budget exhaustion remains explicit incomplete evidence; SASE never substitutes zero or claims mutated_input from incomplete fingerprints.

For definitions with stages: run_silent, nested tools/run_silent calls append stage events to the same run. Compact completion and sase tool show render that timeline, including repeated or overlapping stages and the interval-union time not attributed to any stage. -j exposes the complete stage, fingerprint, and sample records. -l is a different mode: it replays retained stdout and stderr without claiming a total ordering between those streams.

LAST is the newest native result for this project and definition. TYPICAL is the observed median duration of at most 30 normally exited native runs within 30 days. Missing samples render as an em dash, never as zero or an ETA.

_[project]: A sase project is a named unit of work registered with SASE. _[workspace]: A sase workspace is a numbered clone of a project's primary repo, managed by the workspace store and tracked in that project's registry.json. _[agent]: A sase agent is an agent session or a single agent that does not belong to a session. _[prompt]: A prompt is the text SASE hands to a provider to start an agent turn. _[macro]: A macro is a reusable prompt triggered with #name in agent prompts. _[Patch]: A Patch is SASE's local unit of change. _[bead]: A bead is one tracked work item in SASE's git-native issue tracker. _[plan]: A plan is a persisted, approved artifact recording the intent behind agent work. _[TUI]: The TUI is SASE's terminal interface for running agents and reviewing results. _[epic]: An epic is an executable multi-phase plan run with sase bead work. _[gate]: A sase gate is a durable, command-backed request for one user decision, created when work must pause for the user. _[monitor]: A sase monitor is a monitor turn that runs one long command under a detached supervisor, so the command outlives the agent that started it. *[stitch]: A stitch is the lightweight ordered change record inside a Patch's STITCHES: section.