mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-26 21:36:31 +02:00
Clears every deferred extension todo in one push:
1) Interception expands to three events: tool_call (already shipped),
turn_start (gate the turn before the model call, e.g. rate-limit /
business-hour), and assistant_message (suppress or rewrite the
user-visible text while keeping the model's original output in
the transcript).
2) Tool-call args can now be rewritten mid-flight. An interceptor
returning modified_args replaces the JSON the tool actually
receives, without the model seeing the rewrite. Chains: each
subscriber sees the previous one's output, letting guards
successively redact / patch / augment. Invalid JSON is dropped
safely.
3) /reload-ext hot-reloads every extension without restarting zot.
The manager gracefully shuts down all running subprocesses,
re-reads extension.json from disk, respawns (including --ext
paths remembered from startup), and the host rebuilds the agent's
tool registry in-place so freshly-registered tools are callable
immediately.
Wire-format changes (extproto):
- EventInterceptResponseFromExt gains modified_args and replace_text
fields (both optional, ignored when block=true).
- EventInterceptFromHost gains Step (for turn_start) and Text (for
assistant_message) alongside the existing tool_call payload.
Core agent changes:
- BeforeToolExecute signature now returns (allowed, reason,
modifiedArgs json.RawMessage). Non-nil+valid JSON args replace
tc.Arguments before Tool.Execute runs.
- New BeforeTurn hook, invoked in runLoop before oneTurn. Blocking
cancels the turn with an EvTurnEnd{StopError} carrying the reason.
- New BeforeAssistantMessage hook, invoked after finalMsg is
assembled but before the EvAssistantMessage emit. Supports
suppress (block=true) and text rewrite (replace_text). Transcript
always gets the original; UI gets the rewritten text.
- New SetTools(reg) so /reload-ext can swap the registry on the
live agent under the agent mutex.
Manager changes:
- InterceptToolCall now returns InterceptResult (Block, Reason,
ModifiedArgs, ReplaceText), with a chain that folds rewrites.
- New InterceptTurnStart and InterceptAssistantMessage.
- New Reload(ctx, grace) tears down and respawns everything,
returning ReloadStats{Stopped, Loaded, Ready, Errors}.
- New SetOnReload(fn) callback the host uses to rebuild the agent
tool registry after a reload.
- LoadExplicit remembers --ext paths so Reload respawns them.
- subscribe accepts "tool_call", "turn_start", "assistant_message"
under "intercept".
SDK (pkg/zotext):
- New handler types: ToolCallHandler, TurnStartHandler,
AssistantMessageHandler, and their decision structs
(ToolCallDecision with ModifiedArgs, AssistantMessageDecision
with ReplaceText).
- New registration methods: InterceptToolCallX (rich variant of
the existing InterceptToolCall), InterceptTurnStart,
InterceptAssistantMessage.
- dispatchIntercept routes per-event with panic recovery and
always emits exactly one event_intercept_response.
TUI:
- /reload-ext slash command registered in slashCatalog and
runSlash. Added to slashCancelsTurn so it waits for idle like
/compact does.
- runReloadExt shows a "reloading extensions..." status, runs the
Manager.Reload on a goroutine, and reports the resulting stats.
Tests:
- internal/core/intercept_test.go: verifies args are actually
rewritten on the way to Tool.Execute, malformed JSON is ignored,
and block surfaces the reason as an error ToolResult.
- internal/agent/extensions/intercept_test.go: end-to-end with a
bash extension subprocess that blocks rm -rf, rewrites other bash
args to "echo GUARDED:", passes through read calls, allows
turn_start, and redacts SECRET in assistant messages. Second test
verifies Reload respawns the subprocess, re-registers its command,
and fires the onReload callback.
Docs:
- docs/extensions.md: rewrote the intercept section to cover all
three events, added a table of event_intercept_response fields,
documented the /reload-ext hot-reload command, expanded the SDK
section with examples of every handler, moved the old "future"
items into a shipped Phase 4.
- README.md: extensions summary mentions intercept beyond tool_call,
/reload-ext added to the slash-commands table and to the
turn-cancel list in "Queued messages".
541 lines
18 KiB
Markdown
541 lines
18 KiB
Markdown
# zot extensions
|
|
|
|
zot can be extended with custom slash commands by running an external
|
|
program as a subprocess and exchanging newline-delimited JSON over
|
|
its stdin/stdout. Extensions can be written in **any language** that
|
|
can read and write JSON lines from stdio — Go, TypeScript, Python,
|
|
Rust, shell with `jq`, anything.
|
|
|
|
Three phases shipped so far:
|
|
|
|
- **Phase 1**: slash commands + chat notifications.
|
|
- **Phase 2**: tools the LLM can call.
|
|
- **Phase 3**: lifecycle event subscriptions + tool-call interception
|
|
for guardrail extensions.
|
|
|
|
## Quick start
|
|
|
|
The simplest extension is a script that prints a hello frame, reads
|
|
commands, and prints responses. Here's the whole thing in **Python**,
|
|
no SDK required:
|
|
|
|
```python
|
|
#!/usr/bin/env python3
|
|
# $ZOT_HOME/extensions/hello-py/hello.py
|
|
import json, sys, threading
|
|
|
|
def emit(obj):
|
|
sys.stdout.write(json.dumps(obj) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
emit({"type":"hello","name":"hello-py","version":"1.0.0","capabilities":["commands"]})
|
|
emit({"type":"register_command","name":"hellopy","description":"say hi (python)"})
|
|
|
|
for line in sys.stdin:
|
|
msg = json.loads(line)
|
|
if msg["type"] == "command_invoked":
|
|
emit({"type":"command_response","id":msg["id"],"action":"prompt",
|
|
"prompt": "Greet me very briefly. Add one emoji."})
|
|
elif msg["type"] == "shutdown":
|
|
emit({"type":"shutdown_ack"})
|
|
break
|
|
```
|
|
|
|
Drop it in a directory with this `extension.json`:
|
|
|
|
```json
|
|
{
|
|
"name": "hello-py",
|
|
"version": "1.0.0",
|
|
"exec": "./hello.py",
|
|
"language": "python",
|
|
"enabled": true
|
|
}
|
|
```
|
|
|
|
`chmod +x hello.py`, install:
|
|
|
|
```bash
|
|
zot ext install ./hello-py
|
|
```
|
|
|
|
Restart `zot`, type `/hellopy`, the agent greets you. Done.
|
|
|
|
## Built-in extensions
|
|
|
|
**zot ships with no extensions installed by default.** A fresh `zot install` (or `go install`) gives you a clean agent. Extensions are entirely opt-in: you install (or `--ext` for one run) only the ones you want.
|
|
|
|
The `examples/extensions/` directory in the repo is reference code, not a default install set. To use any of those:
|
|
|
|
```bash
|
|
# go-based examples need a build first
|
|
cd path/to/zot/examples/extensions/hello && go build -o hello .
|
|
|
|
# install (copies to $ZOT_HOME/extensions/hello/)
|
|
zot ext install path/to/zot/examples/extensions/hello
|
|
|
|
# or load straight from the repo for one zot session
|
|
zot --ext path/to/zot/examples/extensions/hello
|
|
```
|
|
|
|
Nothing is auto-installed and nothing reaches out to the network without your explicit action.
|
|
|
|
## Layout & discovery
|
|
|
|
zot scans two directories on startup, in this order:
|
|
|
|
1. **Project-local**: `./.zot/extensions/<name>/extension.json`
|
|
2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json`
|
|
|
|
A project-local extension with the same name wins over a global one.
|
|
On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`;
|
|
on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`.
|
|
|
|
Each extension owns its own subdirectory. The `extension.json`
|
|
manifest tells zot how to launch it:
|
|
|
|
```json
|
|
{
|
|
"name": "weather",
|
|
"version": "1.0.0",
|
|
"exec": "./weather",
|
|
"args": ["--mode", "daemon"],
|
|
"language": "go",
|
|
"description": "current weather for any city",
|
|
"enabled": true
|
|
}
|
|
```
|
|
|
|
| field | meaning |
|
|
|---|---|
|
|
| `name` | required. how zot identifies the extension; must match what's sent in the `hello` frame. |
|
|
| `version` | optional. shown in `zot ext list`. |
|
|
| `exec` | required. path to the executable (relative to the manifest). |
|
|
| `args` | optional. extra argv passed to `exec`. |
|
|
| `language` | optional. informational only (`go`, `python`, `typescript`, ...). |
|
|
| `description` | optional. shown in `zot ext list`. |
|
|
| `enabled` | optional, defaults to `true`. set to `false` to disable without removing. |
|
|
|
|
## Lifecycle
|
|
|
|
1. **Discovery**: zot reads every `extension.json` in the search dirs.
|
|
2. **Spawn**: enabled extensions are launched as subprocesses. stderr
|
|
redirects to `$ZOT_HOME/logs/ext-<name>.log` (one file per
|
|
extension, append-mode).
|
|
3. **Hello handshake**: the extension sends a `hello` frame; zot
|
|
replies with `hello_ack` containing the protocol version and the
|
|
active provider/model/cwd.
|
|
4. **Registration**: the extension sends `register_command` frames.
|
|
First-come-first-served: a name already taken by a built-in or by
|
|
a previously-loaded extension is silently shadowed (logged in the
|
|
extension's own log file).
|
|
5. **Runtime**: zot dispatches `command_invoked` frames when the
|
|
user runs a registered command; the extension responds with
|
|
`command_response`. Extensions can also push `notify` frames at
|
|
any time.
|
|
6. **Shutdown**: when zot exits, it sends `shutdown` and waits up to
|
|
2s for the extension to send `shutdown_ack`. Holdouts are
|
|
SIGTERM'd, then SIGKILL'd.
|
|
|
|
A crashing extension does not bring down zot. The slash command it
|
|
owned simply stops working until the extension is fixed and zot is
|
|
restarted.
|
|
|
|
## Wire format
|
|
|
|
All frames are one JSON object per line. Top-level `type` is the
|
|
discriminator. Optional `id` correlates request frames with their
|
|
responses.
|
|
|
|
### Extension → host
|
|
|
|
#### `hello` (required, first frame)
|
|
|
|
```json
|
|
{"type":"hello","name":"weather","version":"1.0.0",
|
|
"capabilities":["commands","tools"]}
|
|
```
|
|
|
|
#### `register_command`
|
|
|
|
```json
|
|
{"type":"register_command","name":"weather",
|
|
"description":"current weather for a city"}
|
|
```
|
|
|
|
#### `register_tool`
|
|
|
|
Registers a tool the LLM can call. `schema` is a JSON Schema object
|
|
describing the tool's args (the same shape Anthropic and OpenAI accept).
|
|
|
|
```json
|
|
{"type":"register_tool","name":"weather",
|
|
"description":"Get the current weather for a city.",
|
|
"schema":{
|
|
"type":"object",
|
|
"properties":{"city":{"type":"string"}},
|
|
"required":["city"]
|
|
}}
|
|
```
|
|
|
|
Tool names live in the same namespace as built-in tools (`read`,
|
|
`write`, `edit`, `bash`, `skill`). Conflicts are silently shadowed by
|
|
the built-in.
|
|
|
|
#### `ready`
|
|
|
|
Sentinel telling zot "all initial registrations are flushed". Send it
|
|
right after your last `register_*` frame so the host can build the
|
|
agent's tool registry without racing the registration window.
|
|
|
|
```json
|
|
{"type":"ready"}
|
|
```
|
|
|
|
#### `tool_result`
|
|
|
|
Reply to a `tool_call` from the host. `content[]` is a list of
|
|
message blocks; each block is `{"type":"text","text":"..."}` or
|
|
`{"type":"image","mime_type":"image/png","data":"<base64>"}`. Set
|
|
`is_error: true` to mark the call as failed.
|
|
|
|
```json
|
|
{"type":"tool_result","id":"...",
|
|
"content":[{"type":"text","text":"Berlin: 16°C, fog"}]}
|
|
```
|
|
|
|
#### `subscribe`
|
|
|
|
Declares which lifecycle events the extension wants to observe and
|
|
which it wants to intercept. Send once after `hello`, before `ready`.
|
|
|
|
```json
|
|
{"type":"subscribe",
|
|
"events":["session_start","turn_start","tool_call","turn_end","assistant_message"],
|
|
"intercept":["tool_call","turn_start","assistant_message"]}
|
|
```
|
|
|
|
Recognised event names: `session_start`, `turn_start`, `turn_end`,
|
|
`tool_call`, `assistant_message`.
|
|
|
|
Interceptable events:
|
|
|
|
- `tool_call`: block the call (model sees `reason` as the tool
|
|
error) or rewrite args via `modified_args`.
|
|
- `turn_start`: block the turn before the model is called. Useful
|
|
for rate-limiting and business-hour gates. `reason` is shown to
|
|
the user as a status line. No rewrite supported.
|
|
- `assistant_message`: suppress the message via `block`, or rewrite
|
|
the user-visible text via `replace_text`. The model's original
|
|
text stays in the transcript so the model sees what it actually
|
|
said on subsequent turns.
|
|
|
|
#### `event_intercept_response`
|
|
|
|
Reply to an `event_intercept` from the host. All fields default to
|
|
"allow, pass through unmodified".
|
|
|
|
| field | meaning |
|
|
|---|---|
|
|
| `block` | `true` refuses the action. For `tool_call`, `reason` is shown to the model; for `turn_start` / `assistant_message`, `reason` is shown to the user. |
|
|
| `reason` | refusal text (on block) or pass-through note. |
|
|
| `modified_args` | for `tool_call`: rewritten JSON args the tool will actually see. Must be a valid JSON object. Ignored when `block` is true. |
|
|
| `replace_text` | for `assistant_message`: replaces the user-visible text. The model's original output still lives in the transcript. Ignored when `block` is true. |
|
|
|
|
Missing the response within 5s is treated as "allow" (i.e. an
|
|
unresponsive extension never stalls the agent). When multiple
|
|
extensions subscribe to the same event, they're consulted serially;
|
|
the first `block` wins and rewrites (args / text) chain: each
|
|
subsequent interceptor sees the previous one's output.
|
|
|
|
```json
|
|
{"type":"event_intercept_response","id":"...",
|
|
"block":true,"reason":"refused: matches danger pattern \"rm -rf\""}
|
|
|
|
{"type":"event_intercept_response","id":"...",
|
|
"modified_args":{"command":"echo GUARDED: ls"}}
|
|
|
|
{"type":"event_intercept_response","id":"...",
|
|
"replace_text":"[redacted]"}
|
|
```
|
|
|
|
#### `command_response` (reply to `command_invoked`)
|
|
|
|
```json
|
|
{"type":"command_response","id":"...","action":"prompt",
|
|
"prompt":"Show today's weather for Berlin in one line."}
|
|
```
|
|
|
|
`action` is one of:
|
|
|
|
- `"prompt"` — submits `prompt` as a fresh user message; the agent
|
|
runs a turn against it.
|
|
- `"insert"` — inserts `insert` into the editor at the cursor without
|
|
submitting.
|
|
- `"display"` — appends `display` to the chat as a one-shot styled
|
|
note. No model call, nothing written to the transcript.
|
|
- `"noop"` — the extension handled it itself (e.g. it pushed
|
|
`notify` frames or kicked off background work). zot doesn't change
|
|
the UI in response.
|
|
|
|
If `error` is non-empty, zot renders it as a red status line
|
|
regardless of `action`.
|
|
|
|
#### `notify` (one-way, any time)
|
|
|
|
```json
|
|
{"type":"notify","level":"info",
|
|
"message":"refreshed cache (12 entries)"}
|
|
```
|
|
|
|
`level` is one of `info`, `success`, `warn`, `error`. The note shows
|
|
up below the transcript with the extension's name in brackets.
|
|
|
|
#### `shutdown_ack`
|
|
|
|
Sent in response to `shutdown`. Extension should exit promptly after.
|
|
|
|
### Host → extension
|
|
|
|
#### `hello_ack`
|
|
|
|
```json
|
|
{"type":"hello_ack","protocol_version":1,
|
|
"zot_version":"0.0.7","provider":"anthropic",
|
|
"model":"claude-opus-4-7","cwd":"/Users/pat/Developer/zot"}
|
|
```
|
|
|
|
Sent immediately after `hello`. The extension can use these fields to
|
|
decide which commands to register (e.g. only register a Python tool
|
|
on macOS, only register a model-specific shortcut for opus, etc.).
|
|
|
|
#### `command_invoked`
|
|
|
|
```json
|
|
{"type":"command_invoked","id":"...",
|
|
"name":"weather","args":"berlin"}
|
|
```
|
|
|
|
`args` is everything the user typed after the command name, trimmed.
|
|
|
|
#### `tool_call`
|
|
|
|
Sent when the LLM invokes a tool the extension registered. `args` is
|
|
the parsed JSON object the model produced; the extension is
|
|
responsible for validating/coercing it.
|
|
|
|
```json
|
|
{"type":"tool_call","id":"...","name":"weather",
|
|
"args":{"city":"Berlin"}}
|
|
```
|
|
|
|
Reply with `tool_result` within the host's tool timeout (default 60s).
|
|
Missing the timeout surfaces an error to the model and the call is
|
|
marked as failed.
|
|
|
|
#### `event`
|
|
|
|
Lifecycle notification for events the extension subscribed to via
|
|
`subscribe`. One-way — no response expected.
|
|
|
|
```json
|
|
{"type":"event","event":"turn_start","step":1}
|
|
{"type":"event","event":"tool_call",
|
|
"tool_id":"...","tool_name":"read","tool_args":{"path":"foo.go"}}
|
|
{"type":"event","event":"turn_end","stop":"end_turn"}
|
|
```
|
|
|
|
#### `event_intercept`
|
|
|
|
Sent when zot wants to give the extension a chance to block, modify,
|
|
or annotate a lifecycle event before it happens. Reply with
|
|
`event_intercept_response` within 5s; missing the deadline is
|
|
treated as "allow".
|
|
|
|
Payload fields depend on the event:
|
|
|
|
```json
|
|
// tool_call: includes the tool id, name, and parsed args
|
|
{"type":"event_intercept","id":"...","event":"tool_call",
|
|
"tool_id":"...","tool_name":"bash",
|
|
"tool_args":{"command":"rm -rf /tmp/foo"}}
|
|
|
|
// turn_start: includes the step number
|
|
{"type":"event_intercept","id":"...","event":"turn_start",
|
|
"step":3}
|
|
|
|
// assistant_message: includes the assembled text
|
|
{"type":"event_intercept","id":"...","event":"assistant_message",
|
|
"text":"here is your api key: sk-ant-..."}
|
|
```
|
|
|
|
#### `shutdown`
|
|
|
|
Sent during graceful zot exit (or `/reload-ext` once that lands).
|
|
Reply with `shutdown_ack` and then exit.
|
|
|
|
## Managing extensions from the CLI
|
|
|
|
```
|
|
zot ext list list installed extensions and their state
|
|
zot ext install <path|git-url> copy / clone into $ZOT_HOME/extensions/
|
|
zot ext remove <name> delete an extension directory
|
|
zot ext enable <name> re-enable a disabled extension
|
|
zot ext disable <name> disable without removing
|
|
zot ext logs <name> [-f] cat / tail the extension's stderr
|
|
```
|
|
|
|
`zot ext install <path>` does a recursive copy; `<git-url>` does a
|
|
shallow clone. Both validate that the destination contains an
|
|
`extension.json` and roll back if not.
|
|
|
|
## Loading an extension for one run
|
|
|
|
For iteration on a working copy, skip the install + reload cycle
|
|
and load straight from disk for one zot session:
|
|
|
|
```
|
|
zot --ext ./my-extension # short form: -e ./my-extension
|
|
zot --ext ./a -e ./b # repeatable
|
|
```
|
|
|
|
`--ext` paths take precedence over installed extensions of the same
|
|
name, so you can shadow an installed copy with a work-in-progress
|
|
version without uninstalling first. Nothing is copied or persisted;
|
|
the extension dies with zot like any other subprocess.
|
|
|
|
## SDKs
|
|
|
|
Writing the wire protocol by hand is fine for one-off scripts, but
|
|
for anything bigger the SDKs handle the boilerplate.
|
|
|
|
### Go — `pkg/zotext`
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"github.com/patriceckhart/zot/pkg/zotext"
|
|
)
|
|
|
|
func main() {
|
|
ext := zotext.New("hello", "1.0.0")
|
|
|
|
// Slash command
|
|
ext.Command("hello", "say hi", func(args string) zotext.Response {
|
|
return zotext.Prompt("Greet me in one short sentence.")
|
|
})
|
|
|
|
// LLM-callable tool
|
|
ext.Tool("weather", "Current weather for a city.",
|
|
json.RawMessage(`{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}`),
|
|
func(args json.RawMessage) zotext.ToolResult {
|
|
var in struct{ City string `json:"city"` }
|
|
json.Unmarshal(args, &in)
|
|
return zotext.TextResult(in.City + ": sunny")
|
|
})
|
|
|
|
ext.Run()
|
|
}
|
|
```
|
|
|
|
Build with `go build -o hello .`, drop the binary + an `extension.json`
|
|
into `$ZOT_HOME/extensions/hello/`.
|
|
|
|
The SDK has four interceptor hooks, all optional:
|
|
|
|
```go
|
|
// Refuse calls or rewrite args before they run.
|
|
ext.InterceptToolCall(func(tool string, args json.RawMessage) (bool, string) {
|
|
if tool == "bash" { /* inspect args, return false, reason */ }
|
|
return true, ""
|
|
})
|
|
|
|
// Richer variant: returns ToolCallDecision so you can also rewrite
|
|
// args via ModifiedArgs.
|
|
ext.InterceptToolCallX(func(tool string, args json.RawMessage) zotext.ToolCallDecision {
|
|
return zotext.ToolCallDecision{
|
|
ModifiedArgs: json.RawMessage(`{"command":"echo GUARDED"}`),
|
|
}
|
|
})
|
|
|
|
// Block the next turn before the model is called.
|
|
ext.InterceptTurnStart(func(step int) zotext.TurnStartDecision {
|
|
if time.Now().Hour() < 9 { return zotext.TurnStartDecision{Block: true, Reason: "outside business hours"} }
|
|
return zotext.TurnStartDecision{}
|
|
})
|
|
|
|
// Scrub or rewrite the assistant's final text before the user sees it.
|
|
ext.InterceptAssistantMessage(func(text string) zotext.AssistantMessageDecision {
|
|
return zotext.AssistantMessageDecision{
|
|
ReplaceText: strings.ReplaceAll(text, "SECRET", "[redacted]"),
|
|
}
|
|
})
|
|
```
|
|
|
|
See:
|
|
- `examples/extensions/hello/` — slash commands
|
|
- `examples/extensions/clock/` — slash commands in plain Node, no SDK
|
|
- `examples/extensions/weather/` — LLM-callable tool
|
|
- `examples/extensions/guard/` — event subscriptions + tool-call
|
|
interception (refuses dangerous bash patterns)
|
|
|
|
### Hot reload
|
|
|
|
Type `/reload-ext` in the TUI to tear down every running extension
|
|
subprocess, re-read the manifests from disk, and respawn the set.
|
|
The agent's tool registry is rebuilt automatically, so freshly-
|
|
registered extension tools become callable without restarting zot.
|
|
Useful while developing an extension: edit, save, `/reload-ext`,
|
|
done. Explicit `--ext` paths are remembered and reloaded alongside
|
|
discovered extensions.
|
|
|
|
### TypeScript / Python
|
|
|
|
These SDKs aren't in the main repo yet; the wire format is small
|
|
enough that a `~30 line` raw script gets you started in either
|
|
language. See the [Quick start](#quick-start) Python example for the
|
|
shape. SDK packages will land in follow-up commits.
|
|
|
|
## Security
|
|
|
|
Extensions run with **the user's full filesystem and network
|
|
permissions**. Treat installing an extension the same as installing
|
|
any other binary on your machine.
|
|
|
|
`zot ext install <git-url>` clones from any URL you give it. There's
|
|
no sandbox in v1; if you need isolation, install only extensions you
|
|
trust or run zot under your platform's sandboxing tool (`bwrap` /
|
|
`sandbox-exec` / AppContainer).
|
|
|
|
## Roadmap
|
|
|
|
Phase 1 (shipped):
|
|
- [x] subprocess lifecycle + hello handshake
|
|
- [x] `register_command` + `command_invoked`
|
|
- [x] `notify`
|
|
- [x] `zot ext` CLI
|
|
|
|
Phase 2 (shipped):
|
|
- [x] `register_tool` + `tool_call` + `tool_result`
|
|
- [x] `ready` sentinel for safe agent-registry build timing
|
|
- [x] tool result attribution surfaces extension name in details
|
|
|
|
Phase 3 (shipped):
|
|
- [x] event subscriptions (`session_start`, `turn_start`, `turn_end`,
|
|
`tool_call`, `assistant_message`)
|
|
- [x] tool-call interception (block before execution)
|
|
|
|
Phase 4 (shipped):
|
|
- [x] interception for `turn_start` and `assistant_message` (in
|
|
addition to `tool_call`)
|
|
- [x] modify tool args mid-flight via `modified_args`
|
|
- [x] rewrite user-visible assistant text via `replace_text`
|
|
- [x] `/reload-ext` slash command (hot-reload without restarting zot)
|
|
|
|
Future (no firm timeline):
|
|
- [ ] TypeScript and Python SDK packages (currently the wire format
|
|
is stable enough to hand-roll, see the Python quick-start)
|
|
- [ ] HTTP / WebSocket transport variants (today: subprocess stdio)
|
|
- [ ] per-extension permission scopes (today: full user privileges)
|