zot/docs
patriceckhart 83ae236571 feat(extensions): phase 3 — event subscriptions + tool-call interception
Two new capabilities, both ride on the existing subprocess
protocol with a couple of new frame types.

Event subscriptions (one-way notifications):

  ext  -> host: subscribe {events: [...], intercept: [...]}
  host -> ext:  event {event, ...payload}

  Recognised events: session_start, turn_start, turn_end,
  tool_call, assistant_message. Subscribers get fire-and-forget
  notifications on each. Useful for telemetry, audit logs, custom
  state widgets that follow live agent activity.

Tool-call interception (round-trip, can refuse):

  host -> ext:  event_intercept {id, event:"tool_call", tool_name, tool_args}
  ext  -> host: event_intercept_response {id, block?, reason?}

  When at least one extension subscribed to "tool_call" intercept,
  zot asks each one in turn before running every tool call. First
  blocker wins; reason becomes the tool-result error text the model
  sees. Per-extension 5s timeout treats unresponsive interceptors
  as "allow" so a wedged extension never stalls the agent.

Wire format additions (internal/extproto):
  ext -> host: SubscribeFromExt, EventInterceptResponseFromExt
  host -> ext: EventFromHost, EventInterceptFromHost

Manager (internal/agent/extensions):
  - per-extension eventSubs / interceptSubs sets, populated by the
    subscribe frame
  - EmitEvent fans out to every subscribed extension on its own
    goroutine (won't block the agent on slow stdin writes)
  - InterceptToolCall walks subscribers serially, returning the
    first refusal; 5s timeout per subscriber (allow on timeout)
  - readLoop handles event_intercept_response correlations the
    same way it handles command/tool responses

Core (internal/core/agent.go):
  - Agent.BeforeToolExecute hook called from runOneTool right
    before tool.Execute. Returning (allowed=false, reason)
    short-circuits with an IsError tool result containing reason.
  - Agent.OnEvent observer fires for every emitted AgentEvent;
    composed transparently with the per-Prompt sink via wrapSink
    so neither the existing TUI nor the rpc loop need changes.

Wiring (internal/agent/cli.go, rpc.go):
  - wireAgentExt sets BeforeToolExecute -> InterceptToolCall and
    OnEvent -> fanoutAgentEvent for every freshly-built agent
    (initial, login rebuild, model swap)
  - fanoutAgentEvent translates core AgentEvent kinds into
    extproto.EventFromHost. Internal-only events (text_delta,
    tool_progress) are dropped to keep the per-extension stream
    sane.
  - session_start emitted once after extensions come up

SDK (pkg/zotext):
  - On(name, EventHandler) registers per-event observers
  - InterceptToolCall(InterceptHandler) registers a single
    intercept callback
  - Run() now also sends a subscribe frame before the ready
    sentinel, with the union of subscribed events + intercept
  - Frame loop handles "event" and "event_intercept" frames,
    runs the handlers (intercepts on a goroutine to avoid
    head-of-line blocking)
  - Capabilities advertised: commands + tools + events

Example (examples/extensions/guard):
  - subscribes to session_start / turn_start / tool_call / turn_end
    and writes one-line audit entries
  - intercepts every bash call; refuses commands matching
    rm -rf, sudo, dd of=/, mkfs, the fork bomb, chmod -R 777
  - end-to-end verified live: agent -> bash("rm -rf /tmp/foo")
    -> guard refuses -> model sees the refusal text and surfaces
    it in its reply ("the guard blocked it, as expected — the
    pattern \brm\s+-rf\b matched")

Docs/extensions.md updated with all five new frame types and the
guard example.
2026-04-19 14:57:03 +02:00
..
extensions.md feat(extensions): phase 3 — event subscriptions + tool-call interception 2026-04-19 14:57:03 +02:00
rpc.md feat: zotcore SDK + zot rpc subprocess protocol 2026-04-19 12:26:48 +02:00
skills.md feat: skills — reusable instructions discovered from SKILL.md files 2026-04-19 14:32:30 +02:00