Skip to content

Commit 25c31af

Browse files
committed
docs: update book, READMEs, and specs for v0.18.2
1 parent 539b6ed commit 25c31af

File tree

10 files changed

+362
-1
lines changed

10 files changed

+362
-1
lines changed

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- [LSP Context Injection](concepts/lsp-context-injection.md)
2323
- [Code Intelligence](concepts/code-intelligence.md)
2424
- [Task Orchestration](concepts/task-orchestration.md)
25+
- [Reactive Hooks](concepts/hooks.md)
2526
- [Logging](concepts/logging.md)
2627
- [Experiments](concepts/experiments.md)
2728

book/src/advanced/a2a.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,50 @@ pub enum ProcessorEvent {
5454

5555
The processor sends events through an `mpsc::Sender<ProcessorEvent>`, enabling per-token SSE streaming to connected clients. In daemon mode, `AgentTaskProcessor` bridges A2A requests to the full agent loop (LLM, tools, memory, MCP) via `LoopbackChannel`, providing complete agent capabilities over the A2A protocol.
5656

57+
## Invocation-Bound Capability Tokens (IBCT)
58+
59+
IBCT are per-call security tokens that bind each A2A request to a specific task and endpoint. They prevent replayed or forwarded A2A requests from being accepted by other tasks or endpoints.
60+
61+
### Enabling IBCT
62+
63+
Gated on the `ibct` feature flag (enabled in the `full` feature set):
64+
65+
```toml
66+
[a2a]
67+
ibct_ttl_secs = 300 # Token validity window (default: 300 s)
68+
69+
# Option A: inline key (dev/test only — prefer vault ref in production)
70+
[[a2a.ibct_keys]]
71+
key_id = "k1"
72+
key_bytes_hex = "73757065722d73656372657400000000000000000000000000000000000000"
73+
74+
# Option B: vault reference (recommended for production)
75+
ibct_signing_key_vault_ref = "ZEPH_A2A_IBCT_KEY"
76+
```
77+
78+
When `ibct_keys` or `ibct_signing_key_vault_ref` is set, outgoing A2A client calls include an `X-Zeph-IBCT` header containing a base64-encoded JSON token.
79+
80+
### Token Structure
81+
82+
Each token is HMAC-SHA256 signed and contains:
83+
84+
| Field | Description |
85+
|-------|-------------|
86+
| `key_id` | Key identifier (for rotation without downtime) |
87+
| `task_id` | A2A task the token is scoped to |
88+
| `endpoint` | Target endpoint URL |
89+
| `issued_at` | Unix timestamp of issuance |
90+
| `expires_at` | Expiry timestamp (`issued_at + ibct_ttl_secs`) |
91+
| `signature` | HMAC-SHA256 over key_id + task_id + endpoint + timestamps |
92+
93+
### Key Rotation
94+
95+
Multiple keys can be listed in `[[a2a.ibct_keys]]`. The first key is used for signing; all keys are tried during verification. To rotate:
96+
97+
1. Add the new key as the first entry (it will be used for new tokens).
98+
2. Keep the old key in the list temporarily (it will still verify existing tokens).
99+
3. After `ibct_ttl_secs` has elapsed, remove the old key.
100+
57101
## A2A Client
58102

59103
Zeph can also connect to other A2A agents as a client:

book/src/advanced/tools.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,42 @@ action = "ask"
252252

253253
When `[tools.permissions]` is absent, legacy `blocked_commands` and `confirm_patterns` from `[tools.shell]` are automatically converted to equivalent permission rules (`deny` and `ask` respectively).
254254

255+
## Structured Shell Output Envelope
256+
257+
When `execute_bash` completes, stdout and stderr are captured as separate streams using a tagged channel. The result is stored as a `ShellOutputEnvelope` in `ToolOutput.raw_response`:
258+
259+
```json
260+
{
261+
"stdout": "...",
262+
"stderr": "...",
263+
"exit_code": 0,
264+
"truncated": false
265+
}
266+
```
267+
268+
The LLM context continues to receive the interleaved combined output (in `summary`) — behavior for the agent is unchanged. ACP and audit consumers, however, can access the envelope directly via `raw_response` to distinguish stdout from stderr and inspect the exact exit code.
269+
270+
`AuditEntry` gains two optional fields populated from the envelope:
271+
272+
| Field | Description |
273+
|-------|-------------|
274+
| `exit_code` | Process exit code (`null` when the process was killed by a signal) |
275+
| `truncated` | `true` when output was cut to the overflow threshold |
276+
277+
## File Read Sandbox
278+
279+
`FileExecutor` supports a per-path read sandbox via `[tools.file]`:
280+
281+
```toml
282+
[tools.file]
283+
deny_read = ["/etc/shadow", "/root/*", "/home/*/.ssh/*"]
284+
allow_read = ["/etc/hostname"]
285+
```
286+
287+
Evaluation order: deny-then-allow. Patterns are matched against canonicalized absolute paths, so symlinks pointing into a denied directory are still blocked after resolution.
288+
289+
See the [File Read Sandbox](../reference/security/file-sandbox.md) reference for the full configuration and glob syntax.
290+
255291
## Output Overflow
256292

257293
When tool output exceeds a configurable character threshold, the full response is stored in the SQLite memory database (table `tool_overflow`) and the LLM receives a truncated version (head + tail split) with an opaque reference (`overflow:<uuid>`). This prevents large outputs from consuming the entire context window while preserving access to the complete data.

book/src/concepts/hooks.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Reactive Hooks
2+
3+
Zeph can run shell commands automatically in response to environment changes. Two hook events are supported: working directory changes and file system changes.
4+
5+
## Hook Types
6+
7+
### `cwd_changed`
8+
9+
Fires when the agent's working directory changes — either via the `set_working_directory` tool or an explicit directory change detected after tool execution.
10+
11+
```toml
12+
[[hooks.cwd_changed]]
13+
command = "echo"
14+
args = ["Changed to $ZEPH_NEW_CWD"]
15+
16+
[[hooks.cwd_changed]]
17+
command = "git"
18+
args = ["status", "--short"]
19+
```
20+
21+
Environment variables available to the hook process:
22+
23+
| Variable | Description |
24+
|----------|-------------|
25+
| `ZEPH_OLD_CWD` | Previous working directory |
26+
| `ZEPH_NEW_CWD` | New working directory |
27+
28+
### `file_changed`
29+
30+
Fires when a file under `watch_paths` is modified. Changes are detected via `notify-debouncer-mini` with a 500 ms debounce window — rapid successive modifications produce a single event.
31+
32+
```toml
33+
[hooks.file_changed]
34+
watch_paths = ["src/", "config.toml"]
35+
36+
[[hooks.file_changed.handlers]]
37+
command = "cargo"
38+
args = ["check", "--quiet"]
39+
40+
[[hooks.file_changed.handlers]]
41+
command = "echo"
42+
args = ["File changed: $ZEPH_CHANGED_PATH"]
43+
```
44+
45+
Environment variable available to the hook process:
46+
47+
| Variable | Description |
48+
|----------|-------------|
49+
| `ZEPH_CHANGED_PATH` | Absolute path of the changed file |
50+
51+
## The `set_working_directory` Tool
52+
53+
The `set_working_directory` tool gives the LLM an explicit, persistent way to change the agent's working directory. Unlike `cd` in a `bash` tool call (which is ephemeral and scoped to one subprocess), `set_working_directory` updates the agent's global cwd and triggers any `cwd_changed` hooks.
54+
55+
```text
56+
Use set_working_directory to switch into /path/to/project
57+
```
58+
59+
After the tool executes, subsequent `bash` and file tool calls run relative to the new directory.
60+
61+
## TUI Indicator
62+
63+
When a hook fires, the TUI status bar shows a short spinner message:
64+
65+
- `cwd_changed``Working directory changed…`
66+
- `file_changed``File changed: <path>…`
67+
68+
The indicator disappears once all hook commands for that event have completed.
69+
70+
## Configuration Reference
71+
72+
```toml
73+
# cwd_changed hooks — run in order when the working directory changes
74+
[[hooks.cwd_changed]]
75+
command = "echo"
76+
args = ["cwd is now $ZEPH_NEW_CWD"]
77+
78+
# file_changed hooks — watch_paths + handler list
79+
[hooks.file_changed]
80+
watch_paths = ["src/", "tests/"] # relative or absolute paths to watch
81+
debounce_ms = 500 # debounce window in milliseconds (default: 500)
82+
83+
[[hooks.file_changed.handlers]]
84+
command = "cargo"
85+
args = ["check", "--quiet"]
86+
```
87+
88+
| Field | Type | Default | Description |
89+
|-------|------|---------|-------------|
90+
| `hooks.cwd_changed[].command` | `string` || Executable to run |
91+
| `hooks.cwd_changed[].args` | `Vec<String>` | `[]` | Arguments (env vars expanded) |
92+
| `hooks.file_changed.watch_paths` | `Vec<String>` | `[]` | Paths to monitor |
93+
| `hooks.file_changed.debounce_ms` | `u64` | `500` | Debounce window in milliseconds |
94+
| `hooks.file_changed.handlers[].command` | `string` || Executable to run |
95+
| `hooks.file_changed.handlers[].args` | `Vec<String>` | `[]` | Arguments (env vars expanded) |

book/src/guides/mcp.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,44 @@ min_tools_to_filter = 5 # Only apply filtering when the server exposes a
117117

118118
`min_tools_to_filter` prevents aggressive filtering on small servers. When a server exposes fewer tools than this value, all tools from that server are included unconditionally.
119119

120+
## MCP Elicitation
121+
122+
MCP servers can request structured user input mid-task via the `elicitation/create` protocol method. This allows a server to prompt for missing parameters, confirmations, or credentials without requiring a separate out-of-band channel.
123+
124+
### Enabling Elicitation
125+
126+
Elicitation is disabled by default. Enable it globally or per server:
127+
128+
```toml
129+
[mcp]
130+
elicitation_enabled = true # global default (default: false)
131+
elicitation_timeout = 120 # seconds to wait for user input (default: 120)
132+
elicitation_queue_capacity = 16 # max queued requests (default: 16)
133+
elicitation_warn_sensitive_fields = true # warn before sensitive field prompts
134+
135+
[[mcp.servers]]
136+
id = "my-server"
137+
command = "npx"
138+
args = ["-y", "@acme/mcp-server"]
139+
elicitation_enabled = true # per-server override (overrides global default)
140+
```
141+
142+
`Sandboxed` trust-level servers are never permitted to elicit regardless of config.
143+
144+
### How It Works
145+
146+
When a server sends `elicitation/create`:
147+
148+
- **CLI:** the user sees a phishing-prevention header showing the server name, followed by field prompts. Fields are typed (string, integer, number, boolean, enum).
149+
- **Non-interactive channels** (Telegram, ACP without a connected client): the request is automatically declined.
150+
- If the request queue is full (exceeds `elicitation_queue_capacity`), the request is auto-declined with a warning log instead of blocking or accumulating indefinitely.
151+
152+
### Security Notes
153+
154+
- Always review which servers have `elicitation_enabled = true`. A compromised server with elicitation access can prompt for arbitrary user input.
155+
- `elicitation_warn_sensitive_fields = true` (default) logs a warning when field names match secret patterns before prompting.
156+
- See [Elicitation Security](../reference/security/mcp.md#elicitation-security) for the full security model.
157+
120158
## How Matching Works
121159

122160
MCP tools are embedded in Qdrant (`zeph_mcp_tools` collection) with BLAKE3 content-hash delta sync. Unified matching injects both skills and MCP tools into the system prompt by relevance score — keeping prompt size O(K) instead of O(N) where N is total tools across all servers.

book/src/reference/security/mcp.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,77 @@ MCP server child processes inherit a sanitized environment. The following 21 env
109109

110110
This prevents accidental secret leakage to untrusted MCP servers.
111111

112+
## Tool Collision Detection
113+
114+
When two connected MCP servers expose tools whose `sanitized_id` (server-prefix + normalized name) collide, Zeph logs a warning and the first-registered server's tool wins dispatch. This prevents a later server from silently shadowing an established tool.
115+
116+
Collision warnings appear at connection time and when a dynamic server is added via `/mcp add`. Check the log for `[WARN] mcp: tool id collision` lines if you suspect shadowing.
117+
118+
## Tool-List Snapshot Locking
119+
120+
By default, Zeph accepts `notifications/tools/list_changed` from connected servers and fetches an updated tool list. This creates a window for mid-session tool injection: a compromised or misbehaving server could swap in tools after the operator has reviewed the initial list.
121+
122+
Enable snapshot locking to prevent this:
123+
124+
```toml
125+
[mcp]
126+
lock_tool_list = true
127+
```
128+
129+
When `lock_tool_list = true`, `tools/list_changed` notifications are rejected for all servers after the initial connection handshake. The tool set is frozen at connect time. The lock flag is applied atomically before the connection handshake to eliminate TOCTOU races.
130+
131+
## Per-Server Stdio Environment Isolation
132+
133+
By default, spawned MCP server processes inherit the full (already-sanitized) environment. For additional containment, enable per-server environment isolation:
134+
135+
```toml
136+
# Apply to all stdio servers by default
137+
[mcp]
138+
default_env_isolation = true
139+
140+
# Override per server
141+
[[mcp.servers]]
142+
id = "sensitive-tools"
143+
command = "npx"
144+
args = ["-y", "@acme/sensitive"]
145+
env_isolation = true
146+
env = { TOOL_API_KEY = "vault:tool_key" }
147+
```
148+
149+
With `env_isolation = true`, the child process receives only a minimal base environment (PATH, HOME, USER, TERM, TMPDIR, LANG, plus XDG dirs on Linux) plus the server-specific `env` map. All other inherited variables — including remaining secrets not caught by the blocklist — are stripped.
150+
151+
| Setting | Scope | Effect |
152+
|---------|-------|--------|
153+
| `default_env_isolation` | All stdio servers | Opt-in baseline for all servers |
154+
| `env_isolation` per server | Single server | Override (can enable or disable the default) |
155+
156+
## Intent-Anchor Nonce Boundaries
157+
158+
Every MCP tool response is wrapped with a per-invocation nonce boundary:
159+
160+
```
161+
[TOOL_OUTPUT::550e8400-e29b-41d4-a716-446655440000::BEGIN]
162+
<tool output>
163+
[TOOL_OUTPUT::550e8400-e29b-41d4-a716-446655440000::END]
164+
```
165+
166+
The UUID is unique per call and generated inside Zeph, not from the server response. If tool output itself contains the string `[TOOL_OUTPUT::`, that prefix is escaped before wrapping, preventing injection attempts that mimic the boundary marker. This gives the injection-detection layer a reliable delimiter to trust.
167+
168+
## Elicitation Security
169+
170+
When a connected server uses the `elicitation/create` method to request user input, Zeph applies two safeguards:
171+
172+
1. **Phishing-prevention header** — the CLI always displays the requesting server's ID before showing any fields, so the user knows which server is asking.
173+
174+
2. **Sensitive field warning** — field names matching common secret patterns (password, token, secret, key, credential, auth, private, passphrase, pin) trigger an additional warning before the user is prompted. Configure with:
175+
176+
```toml
177+
[mcp]
178+
elicitation_warn_sensitive_fields = true # default: true
179+
```
180+
181+
`Sandboxed` trust-level servers are never allowed to elicit regardless of `elicitation_enabled`. This is enforced unconditionally.
182+
112183
## Environment Variables
113184

114185
MCP servers inherit environment variables from their configuration. Never store secrets directly in `config.toml` — use the [Vault](../security.md#age-vault) integration instead:

0 commit comments

Comments
 (0)