Skip to content

Guarding tools

const monitor = createMonitor({ rules: [SEC_01] })
const tools = guardTools(monitor, { queryMetrics, restartService })

guardTools returns the same tools with their execute wrapped. The originals are untouched, and each wrapper keeps its tool’s class, id, description and schemas, so anything holding one cannot tell the difference — including the model, which sees the same tool definitions it saw before.

Four things happen without interruption:

  1. The candidate event is drafted — at the current sequence position, without advancing it.
  2. The monitor is asked what that event would mean. This is peek: it runs the transition and throws the state away.
  3. The policy decides: observe, deny, suspend or abort.
  4. Only if the answer is observe is the event committed, and only then does the sequence counter advance.

That sequence is serialised per monitor. Two concurrent tool calls would otherwise each judge state the other had already changed, and a budget of one would let two through. The tool bodies themselves run outside the critical section, so real concurrency survives where it is safe.

The load-bearing invariant underneath is that stepping after peeking equals stepping. It is checked by a property test over generated traces rather than by inspection, because enforcement, replay, timeline scrubbing and batch checking are all the same fold, and they stay the same fold only because it holds.

guardTools(monitor, tools, {
source,
policy,
onDecision: (decision) => report(decision),
onEvent: (event) => recorder.record(event),
})
source The trace source numbering this run’s events. Share one between the tool guard, the workflow guard and the observer so they agree on runId and on the sequence.
policy Overrides the mode per call. See enforcement modes.
onDecision Fires once per interception, including for calls that were refused and therefore never happened. This is the telemetry surface.
onEvent Fires per committed event. This is where a recorder attaches.

The monitor partitions its state by runId, so events belonging to the same run must carry the same one or they will be judged against separate histories. Sharing a single trace source between every guard on a run is how they agree, and it is also what keeps the sequence numbering dense across all of them.

One monitor can therefore stand in front of interleaved runs without an approval in one satisfying a prerequisite in another. release(runId) drops a finished run’s state, which is what keeps a long-lived guard bounded.