From e5c7986e7f1a70418a4bfb49fffe1d53c8ddf79a Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Tue, 12 May 2026 21:31:27 +0200 Subject: [PATCH 1/3] release: prepare v0.21.1 --- CHANGELOG.md | 14 ++--- Cargo.lock | 60 +++++++++---------- Cargo.toml | 60 +++++++++---------- README.md | 2 +- ...idgets__splash__tests__splash_default.snap | 3 +- 5 files changed, 70 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c42b6735..18ca66fcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] -### Fixed - -- fix(channels): apply 30-second per-request timeout to `TelegramApiClient::new()` and - `TelegramApiClient::with_base_url()` to prevent indefinite stalls when `api.telegram.org` - is unreachable (issue #3777). The constant `REQUEST_TIMEOUT` is defined in - `telegram_api_ext.rs` and matches the project's general policy for external HTTP calls. +## [0.21.1] - 2026-05-12 ### Added @@ -34,6 +29,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- fix(channels): apply 30-second per-request timeout to `TelegramApiClient::new()` and + `TelegramApiClient::with_base_url()` to prevent indefinite stalls when `api.telegram.org` + is unreachable (issue #3777). The constant `REQUEST_TIMEOUT` is defined in + `telegram_api_ext.rs` and matches the project's general policy for external HTTP calls. - fix(core): pass live MCP dispatch to `on_turn_complete` hook dispatcher. The hook path previously hardcoded `no_mcp = None`, causing every `mcp_tool` hook configured under `on_turn_complete` to fail with `HookError::McpUnavailable`. Now calls `self.mcp_dispatch()` @@ -5841,7 +5840,8 @@ let agent = Agent::new(provider, channel, &skills_prompt, executor); - Agent::run() uses tokio::select! to race channel messages against shutdown signal [0.16.0]: https://github.com/bug-ops/zeph/compare/v0.15.3...v0.16.0 -[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.21.0...HEAD +[Unreleased]: https://github.com/bug-ops/zeph/compare/v0.21.1...HEAD +[0.21.1]: https://github.com/bug-ops/zeph/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/bug-ops/zeph/compare/v0.20.2...v0.21.0 [0.20.2]: https://github.com/bug-ops/zeph/compare/v0.20.1...v0.20.2 [0.20.1]: https://github.com/bug-ops/zeph/compare/v0.20.0...v0.20.1 diff --git a/Cargo.lock b/Cargo.lock index 64bf274cd..4635b0820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10280,7 +10280,7 @@ dependencies = [ [[package]] name = "zeph" -version = "0.21.0" +version = "0.21.1" dependencies = [ "anyhow", "async-trait", @@ -10349,7 +10349,7 @@ dependencies = [ [[package]] name = "zeph-a2a" -version = "0.21.0" +version = "0.21.1" dependencies = [ "axum 0.8.9", "base64 0.22.1", @@ -10379,7 +10379,7 @@ dependencies = [ [[package]] name = "zeph-acp" -version = "0.21.0" +version = "0.21.1" dependencies = [ "agent-client-protocol", "agent-client-protocol-tokio", @@ -10421,7 +10421,7 @@ dependencies = [ [[package]] name = "zeph-agent-context" -version = "0.21.0" +version = "0.21.1" dependencies = [ "chrono", "futures", @@ -10443,7 +10443,7 @@ dependencies = [ [[package]] name = "zeph-agent-feedback" -version = "0.21.0" +version = "0.21.1" dependencies = [ "regex", "schemars 1.2.1", @@ -10458,7 +10458,7 @@ dependencies = [ [[package]] name = "zeph-agent-persistence" -version = "0.21.0" +version = "0.21.1" dependencies = [ "serde", "serde_json", @@ -10474,7 +10474,7 @@ dependencies = [ [[package]] name = "zeph-agent-tools" -version = "0.21.0" +version = "0.21.1" dependencies = [ "futures", "serde", @@ -10496,7 +10496,7 @@ dependencies = [ [[package]] name = "zeph-bench" -version = "0.21.0" +version = "0.21.1" dependencies = [ "clap", "schemars 1.2.1", @@ -10517,7 +10517,7 @@ dependencies = [ [[package]] name = "zeph-channels" -version = "0.21.0" +version = "0.21.1" dependencies = [ "axum 0.8.9", "criterion", @@ -10546,7 +10546,7 @@ dependencies = [ [[package]] name = "zeph-commands" -version = "0.21.0" +version = "0.21.1" dependencies = [ "serde", "thiserror 2.0.18", @@ -10556,7 +10556,7 @@ dependencies = [ [[package]] name = "zeph-common" -version = "0.21.0" +version = "0.21.1" dependencies = [ "blake3", "cpu-time", @@ -10583,7 +10583,7 @@ dependencies = [ [[package]] name = "zeph-config" -version = "0.21.0" +version = "0.21.1" dependencies = [ "dirs", "insta", @@ -10603,7 +10603,7 @@ dependencies = [ [[package]] name = "zeph-context" -version = "0.21.0" +version = "0.21.1" dependencies = [ "blake3", "futures", @@ -10623,7 +10623,7 @@ dependencies = [ [[package]] name = "zeph-core" -version = "0.21.0" +version = "0.21.1" dependencies = [ "age", "base64 0.22.1", @@ -10690,7 +10690,7 @@ dependencies = [ [[package]] name = "zeph-db" -version = "0.21.0" +version = "0.21.1" dependencies = [ "regex", "sqlx", @@ -10705,7 +10705,7 @@ dependencies = [ [[package]] name = "zeph-experiments" -version = "0.21.0" +version = "0.21.1" dependencies = [ "futures", "ordered-float 5.3.0", @@ -10728,7 +10728,7 @@ dependencies = [ [[package]] name = "zeph-gateway" -version = "0.21.0" +version = "0.21.1" dependencies = [ "axum 0.8.9", "blake3", @@ -10747,7 +10747,7 @@ dependencies = [ [[package]] name = "zeph-index" -version = "0.21.0" +version = "0.21.1" dependencies = [ "futures", "ignore", @@ -10781,7 +10781,7 @@ dependencies = [ [[package]] name = "zeph-llm" -version = "0.21.0" +version = "0.21.1" dependencies = [ "async-stream", "audioadapter-buffers", @@ -10829,7 +10829,7 @@ dependencies = [ [[package]] name = "zeph-mcp" -version = "0.21.0" +version = "0.21.1" dependencies = [ "async-trait", "blake3", @@ -10862,7 +10862,7 @@ dependencies = [ [[package]] name = "zeph-memory" -version = "0.21.0" +version = "0.21.1" dependencies = [ "arc-swap", "blake3", @@ -10901,7 +10901,7 @@ dependencies = [ [[package]] name = "zeph-orchestration" -version = "0.21.0" +version = "0.21.1" dependencies = [ "blake3", "dirs", @@ -10928,7 +10928,7 @@ dependencies = [ [[package]] name = "zeph-plugins" -version = "0.21.0" +version = "0.21.1" dependencies = [ "anyhow", "dirs", @@ -10950,7 +10950,7 @@ dependencies = [ [[package]] name = "zeph-sanitizer" -version = "0.21.0" +version = "0.21.1" dependencies = [ "proptest", "regex", @@ -10970,7 +10970,7 @@ dependencies = [ [[package]] name = "zeph-scheduler" -version = "0.21.0" +version = "0.21.1" dependencies = [ "chrono", "cron", @@ -10990,7 +10990,7 @@ dependencies = [ [[package]] name = "zeph-skills" -version = "0.21.0" +version = "0.21.1" dependencies = [ "anyhow", "blake3", @@ -11024,7 +11024,7 @@ dependencies = [ [[package]] name = "zeph-subagent" -version = "0.21.0" +version = "0.21.1" dependencies = [ "dirs", "indoc", @@ -11051,7 +11051,7 @@ dependencies = [ [[package]] name = "zeph-tools" -version = "0.21.0" +version = "0.21.1" dependencies = [ "arc-swap", "dashmap", @@ -11098,7 +11098,7 @@ dependencies = [ [[package]] name = "zeph-tui" -version = "0.21.0" +version = "0.21.1" dependencies = [ "arboard", "base64 0.22.1", @@ -11138,7 +11138,7 @@ dependencies = [ [[package]] name = "zeph-vault" -version = "0.21.0" +version = "0.21.1" dependencies = [ "age", "proptest", diff --git a/Cargo.toml b/Cargo.toml index b58abe02a..47f44d24c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "3" [workspace.package] edition = "2024" rust-version = "1.95" -version = "0.21.0" +version = "0.21.1" authors = ["bug-ops"] license = "MIT" repository = "https://github.com/bug-ops/zeph" @@ -145,35 +145,35 @@ url = "2.5.8" uuid = "1.23.1" walkdir = "2.5" wiremock = "0.6.5" -zeph-a2a = { path = "crates/zeph-a2a", version = "0.21.0" } -zeph-acp = { path = "crates/zeph-acp", version = "0.21.0" } -zeph-agent-context = { path = "crates/zeph-agent-context", version = "0.21.0" } -zeph-agent-feedback = { path = "crates/zeph-agent-feedback", version = "0.21.0" } -zeph-agent-persistence = { path = "crates/zeph-agent-persistence", version = "0.21.0" } -zeph-agent-tools = { path = "crates/zeph-agent-tools", version = "0.21.0" } -zeph-bench = { path = "crates/zeph-bench", version = "0.21.0" } -zeph-channels = { path = "crates/zeph-channels", version = "0.21.0" } -zeph-commands = { path = "crates/zeph-commands", version = "0.21.0" } -zeph-common = { path = "crates/zeph-common", version = "0.21.0" } -zeph-config = { path = "crates/zeph-config", version = "0.21.0" } -zeph-context = { path = "crates/zeph-context", version = "0.21.0" } -zeph-core = { path = "crates/zeph-core", version = "0.21.0" } -zeph-db = { path = "crates/zeph-db", default-features = false, version = "0.21.0" } -zeph-experiments = { path = "crates/zeph-experiments", version = "0.21.0" } -zeph-gateway = { path = "crates/zeph-gateway", version = "0.21.0" } -zeph-index = { path = "crates/zeph-index", version = "0.21.0" } -zeph-llm = { path = "crates/zeph-llm", version = "0.21.0" } -zeph-mcp = { path = "crates/zeph-mcp", version = "0.21.0" } -zeph-memory = { path = "crates/zeph-memory", default-features = false, version = "0.21.0" } -zeph-orchestration = { path = "crates/zeph-orchestration", version = "0.21.0" } -zeph-plugins = { path = "crates/zeph-plugins", version = "0.21.0" } -zeph-sanitizer = { path = "crates/zeph-sanitizer", version = "0.21.0" } -zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.21.0" } -zeph-skills = { path = "crates/zeph-skills", version = "0.21.0" } -zeph-subagent = { path = "crates/zeph-subagent", version = "0.21.0" } -zeph-tools = { path = "crates/zeph-tools", version = "0.21.0" } -zeph-tui = { path = "crates/zeph-tui", version = "0.21.0" } -zeph-vault = { path = "crates/zeph-vault", version = "0.21.0" } +zeph-a2a = { path = "crates/zeph-a2a", version = "0.21.1" } +zeph-acp = { path = "crates/zeph-acp", version = "0.21.1" } +zeph-agent-context = { path = "crates/zeph-agent-context", version = "0.21.1" } +zeph-agent-feedback = { path = "crates/zeph-agent-feedback", version = "0.21.1" } +zeph-agent-persistence = { path = "crates/zeph-agent-persistence", version = "0.21.1" } +zeph-agent-tools = { path = "crates/zeph-agent-tools", version = "0.21.1" } +zeph-bench = { path = "crates/zeph-bench", version = "0.21.1" } +zeph-channels = { path = "crates/zeph-channels", version = "0.21.1" } +zeph-commands = { path = "crates/zeph-commands", version = "0.21.1" } +zeph-common = { path = "crates/zeph-common", version = "0.21.1" } +zeph-config = { path = "crates/zeph-config", version = "0.21.1" } +zeph-context = { path = "crates/zeph-context", version = "0.21.1" } +zeph-core = { path = "crates/zeph-core", version = "0.21.1" } +zeph-db = { path = "crates/zeph-db", default-features = false, version = "0.21.1" } +zeph-experiments = { path = "crates/zeph-experiments", version = "0.21.1" } +zeph-gateway = { path = "crates/zeph-gateway", version = "0.21.1" } +zeph-index = { path = "crates/zeph-index", version = "0.21.1" } +zeph-llm = { path = "crates/zeph-llm", version = "0.21.1" } +zeph-mcp = { path = "crates/zeph-mcp", version = "0.21.1" } +zeph-memory = { path = "crates/zeph-memory", default-features = false, version = "0.21.1" } +zeph-orchestration = { path = "crates/zeph-orchestration", version = "0.21.1" } +zeph-plugins = { path = "crates/zeph-plugins", version = "0.21.1" } +zeph-sanitizer = { path = "crates/zeph-sanitizer", version = "0.21.1" } +zeph-scheduler = { path = "crates/zeph-scheduler", version = "0.21.1" } +zeph-skills = { path = "crates/zeph-skills", version = "0.21.1" } +zeph-subagent = { path = "crates/zeph-subagent", version = "0.21.1" } +zeph-tools = { path = "crates/zeph-tools", version = "0.21.1" } +zeph-tui = { path = "crates/zeph-tui", version = "0.21.1" } +zeph-vault = { path = "crates/zeph-vault", version = "0.21.1" } zeroize = { version = "1.8.2", default-features = false } [workspace.lints.rust] diff --git a/README.md b/README.md index bfaeb16fc..bdfcfce98 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/bug-ops/zeph/ci.yml?branch=main&label=CI)](https://github.com/bug-ops/zeph/actions) [![codecov](https://codecov.io/gh/bug-ops/zeph/graph/badge.svg?token=S5O0GR9U6G)](https://codecov.io/gh/bug-ops/zeph) [![MSRV](https://img.shields.io/badge/MSRV-1.95-blue)](https://www.rust-lang.org) - [![Tests](https://img.shields.io/badge/tests-9139-brightgreen)](https://github.com/bug-ops/zeph/actions) + [![Tests](https://img.shields.io/badge/tests-9201-brightgreen)](https://github.com/bug-ops/zeph/actions) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) diff --git a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap index be890c855..91f3a7bc1 100644 --- a/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap +++ b/crates/zeph-tui/src/widgets/snapshots/zeph_tui__widgets__splash__tests__splash_default.snap @@ -1,5 +1,6 @@ --- source: crates/zeph-tui/src/widgets/splash.rs +assertion_line: 79 expression: output --- ┌──────────────────────────────────────────────────────────┐ @@ -14,7 +15,7 @@ expression: output │ ███████╗███████╗██║ ██║ ██║ │ │ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ │ │ │ -│ v0.21.0 │ +│ v0.21.1 │ │ │ │ Type a message to start. │ │ │ From caf0da6ce4345ecd657cc4ca82e534c9dbd96063 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Tue, 12 May 2026 21:33:25 +0200 Subject: [PATCH 2/3] docs: update READMEs, book, and specs for v0.21.1 --- book/src/advanced/channels.md | 2 +- book/src/advanced/tui.md | 6 +++++ book/src/concepts/hooks.md | 14 +++++++++-- book/src/guides/telegram.md | 36 ++++++++++++++++++++++++++++ crates/zeph-acp/README.md | 4 ++-- crates/zeph-agent-context/README.md | 4 ++-- crates/zeph-agent-feedback/README.md | 2 +- crates/zeph-bench/README.md | 2 +- specs/028-hooks/spec.md | 5 ++-- 9 files changed, 64 insertions(+), 11 deletions(-) diff --git a/book/src/advanced/channels.md b/book/src/advanced/channels.md index ddcf2a933..0d0132d39 100644 --- a/book/src/advanced/channels.md +++ b/book/src/advanced/channels.md @@ -9,7 +9,7 @@ Zeph supports six I/O channels. Each implements the `Channel` trait and can be s | CLI | Default | Token-by-token to stdout | y/N prompt | | Discord | `ZEPH_DISCORD_TOKEN` (requires `discord` feature) | Edit-in-place every 1.5s | Reply "yes" | | Slack | `ZEPH_SLACK_BOT_TOKEN` (requires `slack` feature) | `chat.update` every 2s | Reply "yes" | -| Telegram | `ZEPH_TELEGRAM_TOKEN` | Edit-in-place every 10s | Reply "yes" | +| Telegram | `ZEPH_TELEGRAM_TOKEN` | Edit-in-place every 10s (30s request timeout) | Reply "yes" | | TUI | `--tui` flag (requires `tui` feature) | Real-time in chat panel | Auto-confirm | | Loopback | `--daemon` flag (requires `daemon` + `a2a` features) | Via `LoopbackEvent` mpsc | Auto-confirm | diff --git a/book/src/advanced/tui.md b/book/src/advanced/tui.md index 653e29af0..02857a9e3 100644 --- a/book/src/advanced/tui.md +++ b/book/src/advanced/tui.md @@ -675,6 +675,12 @@ cargo build --release --features tui When [sub-agent orchestration](sub-agents.md) is active, the SubAgents panel in the right sidebar shows each running sub-agent, its current status, and allows you to inspect the full execution transcript. +### Automatic View Switching + +When you spawn a foreground sub-agent (one that blocks the main conversation), the TUI automatically switches the chat view to display the sub-agent's transcript in real time. This lets you monitor the sub-agent's progress without manually switching views. When the sub-agent completes, the view automatically switches back to the main conversation. + +To manually switch back before the sub-agent completes, press `Esc` in the transcript view or use keyboard navigation to return to the main chat. + ### Keybindings | Key | Action | diff --git a/book/src/concepts/hooks.md b/book/src/concepts/hooks.md index 5fa18ee21..773bd9fde 100644 --- a/book/src/concepts/hooks.md +++ b/book/src/concepts/hooks.md @@ -326,19 +326,29 @@ When a `[notifications]` block is configured, `turn_complete` hooks share the sa ### `PermissionDenied` -Fires when a tool execution is blocked by a `RuntimeLayer::before_tool` permission check. This allows you to log or audit blocked tool calls before they reach the user or external systems. +Fires when a tool execution is blocked by any gate check: policy gates, sandbox restrictions, permission layers, rate limiters, quota limits, utility action restrictions, or dependency failures. This comprehensive hook allows you to log or audit all blocked tool calls before they reach the user or external systems. Hook commands receive: | Variable | Description | |----------|-------------| | `ZEPH_DENIED_TOOL` | Name of the blocked tool | -| `ZEPH_DENY_REASON` | Reason the tool was denied (e.g., `"blocked by before_tool layer"`) | +| `ZEPH_DENY_REASON` | Reason the tool was denied (e.g., `"quota exceeded"`, `"policy gate: untrusted_model"`, `"utility action: ModelSwitch"`) | + +**Denial reasons include:** +- `quota exceeded` — tool execution quota exhausted +- `policy gate: ` — blocked by a named policy gate +- `sandbox violation: ` — sandbox restriction violated +- `rate limit exceeded` — API rate limit hit +- `dependency failed` — dependent tool or resource unavailable +- `utility action: ` — blocked by a utility gate (e.g., `ModelSwitch`, `ConfigReload`) +- `blocked by before_tool layer` — pre-execution permission check **Use cases:** - Log security audit events to a central system - Alert on suspicious tool invocation patterns - Track which policies are enforcing restrictions +- Monitor quota exhaustion ```toml [[hooks.permission_denied]] diff --git a/book/src/guides/telegram.md b/book/src/guides/telegram.md index 7e49a703e..3251669f4 100644 --- a/book/src/guides/telegram.md +++ b/book/src/guides/telegram.md @@ -111,12 +111,48 @@ max_bot_chain_depth = 3 When enabled, Zeph registers with Telegram via `setManagedBotAccessSettings` on startup and tracks consecutive bot-to-bot reply depth to prevent circular loops. Messages from unauthorized bots are silently rejected. +## Reaction Moderation Tools + +Group admins can remove reactions from messages using two tools. Both require the bot to be a group admin and will gracefully degrade to warnings if the admin check fails. + +### `telegram_delete_reaction` + +Remove a specific reaction from a message. The reaction field must be a non-empty string of up to 10 characters. + +```toml +# Example tool invocation in agent code +[tool.telegram_delete_reaction] +chat_id = "-1001234567890" +message_id = 123 +reaction = "👍" +``` + +### `telegram_delete_all_reactions` + +Remove all reactions from a message. + +```toml +# Remove all reactions from a message +[tool.telegram_delete_all_reactions] +chat_id = "-1001234567890" +message_id = 123 +``` + +Both tools require: +- Bot to be a member of the group +- Bot to have admin privileges in the group +- Valid chat ID and message ID + ## Voice and Image Support - **Voice notes**: automatically transcribed via STT when `stt` feature is enabled - **Photos**: forwarded to the LLM for visual reasoning (requires vision-capable model) - See [Audio & Vision](../advanced/multimodal.md) for backend configuration +## Network Timeouts + +All Telegram API client connections are subject to a 30-second timeout. This ensures that slow or unresponsive server connections fail fast rather than blocking indefinitely. If you experience timeout errors, check your network connectivity and Telegram's API status at [Telegram Bot API Changelog](https://core.telegram.org/bots/api-changelog). + ## Other Channels Zeph also supports Discord, Slack, CLI, and TUI. See [Channels](../advanced/channels.md) for the full reference. diff --git a/crates/zeph-acp/README.md b/crates/zeph-acp/README.md index 5a03ce740..8c77a1274 100644 --- a/crates/zeph-acp/README.md +++ b/crates/zeph-acp/README.md @@ -15,10 +15,10 @@ Implements the [Agent Client Protocol](https://agentclientprotocol.org) server s ```toml [dependencies] -zeph-acp = "0.20" +zeph-acp = "0.21" # With HTTP+SSE transport -zeph-acp = { version = "0.20", features = ["acp-http"] } +zeph-acp = { version = "0.21", features = ["acp-http"] } ``` **Important:** diff --git a/crates/zeph-agent-context/README.md b/crates/zeph-agent-context/README.md index be7f4b73c..65f3d79af 100644 --- a/crates/zeph-agent-context/README.md +++ b/crates/zeph-agent-context/README.md @@ -12,7 +12,7 @@ Provides `ContextService` — a stateless façade for all context operations: sy ```toml [dependencies] -zeph-agent-context = { version = "0.20", workspace = true } +zeph-agent-context = { version = "0.21", workspace = true } ``` > [!IMPORTANT] @@ -92,7 +92,7 @@ The `self-check` feature was consolidated as always-on in v0.20.x — retrieved- compile unconditionally. Only `index` remains optional. ```toml -zeph-agent-context = { version = "0.20", workspace = true, features = ["index"] } +zeph-agent-context = { version = "0.21", workspace = true, features = ["index"] } ``` ## License diff --git a/crates/zeph-agent-feedback/README.md b/crates/zeph-agent-feedback/README.md index 676203003..06066fc1a 100644 --- a/crates/zeph-agent-feedback/README.md +++ b/crates/zeph-agent-feedback/README.md @@ -17,7 +17,7 @@ Detects when a user is implicitly correcting a previous agent response — witho ```toml [dependencies] -zeph-agent-feedback = { version = "0.20", workspace = true } +zeph-agent-feedback = { version = "0.21", workspace = true } ``` **Note:** Requires Rust 1.95 or later (Edition 2024). diff --git a/crates/zeph-bench/README.md b/crates/zeph-bench/README.md index 7c357fb7c..d8b625c15 100644 --- a/crates/zeph-bench/README.md +++ b/crates/zeph-bench/README.md @@ -141,7 +141,7 @@ state between runs. Results accumulate into a `BenchRun` and are persisted by `R ```toml [dependencies] -zeph-bench = "0.20" +zeph-bench = "0.21" ``` This crate is part of the [Zeph](https://github.com/bug-ops/zeph) workspace. See the diff --git a/specs/028-hooks/spec.md b/specs/028-hooks/spec.md index ad434c8b6..4e0607a15 100644 --- a/specs/028-hooks/spec.md +++ b/specs/028-hooks/spec.md @@ -43,7 +43,7 @@ agent lifecycle events. Five event types are supported: |---|---| | `cwd_changed` | Agent working directory changes (via `set_working_directory` tool) | | `file_changed` | A watched file or directory subtree is modified on disk | -| `permission_denied` | A tool execution is short-circuited by a `RuntimeLayer::before_tool` check | +| `permission_denied` | A tool execution is denied by any gate in the tier loop (policy gate, sandbox, quota, utility gate, dep-failure, or rate limiter) | | `turn_complete` | An agent turn completes (after all tool calls and LLM response) | | `pre_tool_use` | Before any LLM-requested tool invocation — fires before the utility gate and `RuntimeLayer::before_tool` check (#3725) | | `post_tool_use` | After any tool invocation completes (carries `ZEPH_TOOL_DURATION_MS`) | @@ -243,8 +243,9 @@ validating the new config. `HookRunner::replace_config` is an atomic swap using - `FileChangeWatcher` debounce is mandatory — raw filesystem events must never bypass it - File change watcher is skipped in `--bare` mode — `with_hooks_config` is guarded by `!exec_mode.bare` in `runner.rs` (#3362) - Hook execution is never on the agent hot path — always background task -- `permission_denied` hook fires when `RuntimeLayer::before_tool` short-circuits execution; `LayerDenial.reason` is propagated to `ZEPH_DENY_REASON` (#3310) +- `permission_denied` hook fires at **all** gate denial points in the tier loop via the `fire_permission_denied_hooks` async helper (#3779): policy gate (`LayerDenial.reason`), sandbox violation, session quota exceeded, utility gate interception, dep-failure skip, and rate-limiter block; the utility gate reason includes the `UtilityAction` variant name for disambiguation. Prior to #3779 the hook was only reachable via `RuntimeLayer::before_tool` and never fired in practice. - `turn_complete` is added to `HooksConfig` and `HooksConfig::is_empty()` check (#3327) +- `turn_complete` hooks receive a live `McpManagerDispatch` handle (via `self.mcp_dispatch()`) so `type = "mcp_tool"` entries in `[[hooks.turn_complete]]` function correctly (#3773); prior to #3776 the dispatch was hardcoded to `None`, causing every `McpTool` turn-complete hook to fail with `HookError::McpUnavailable` - `type = "mcp_tool"` action requires MCP manager active; must fail gracefully per `fail_closed` setting when unavailable (#3293) - `HookRunner` uses `ArcSwap` — live reload is atomic, no lock contention on hook dispatch - `pre_tool_use` fires for ALL LLM-requested tool calls including those intercepted by the utility gate (Retrieve/Verify/Stop) — dispatch is ordered **before** `check_call_gates` in `build_tier_call_futures` (#3738); internal tools (`compress_context`, `start_focus`, `complete_focus`) are excluded via the early-continue guard From 173881fc0c8c6700afceadddd9d7e9b5143ec477 Mon Sep 17 00:00:00 2001 From: "Andrei G." Date: Tue, 12 May 2026 21:36:29 +0200 Subject: [PATCH 3/3] docs: add sub-agent memory sandbox docs and update specs for v0.21.1 --- book/src/advanced/sub-agents.md | 7 ++++ specs/007-channels/spec.md | 33 +++++++++++++++++ specs/026-tui-subagent-management/spec.md | 17 +++++++++ .../033-subagent-context-propagation/spec.md | 4 +- specs/044-subagent-lifecycle/spec.md | 9 ++++- .../spec.md | 37 +++++++++++++++---- specs/README.md | 12 +++--- 7 files changed, 102 insertions(+), 17 deletions(-) diff --git a/book/src/advanced/sub-agents.md b/book/src/advanced/sub-agents.md index 584c345c4..d8fef8482 100644 --- a/book/src/advanced/sub-agents.md +++ b/book/src/advanced/sub-agents.md @@ -440,6 +440,12 @@ tools: If all three file tools are blocked (via `tools.except` or `tools.deny`), memory is silently disabled — the directory is not created and no content is injected. +### Sandbox and File Tool Access + +Sub-agents run in a restricted sandbox that prevents file writes outside the agent's working directory. When an agent declares `memory: user`, Zeph automatically allows writes to the user-scoped memory directory (`~/.zeph/agent-memory//`) as an exception to the sandbox boundary. + +This allows agents with `memory: user` to persist state across projects while remaining sandboxed from accidental writes to system directories or other project data. File paths are validated and canonicalized to prevent traversal attacks. + ### Security - **Agent name validation** — Names must match `^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$`. Path traversal attempts (e.g., `../etc/passwd`) are rejected. @@ -448,6 +454,7 @@ If all three file tools are blocked (via `tools.except` or `tools.deny`), memory - **Null byte guard** — Files containing null bytes are rejected. - **Tag escaping** — `` tags in memory content are escaped to prevent prompt injection. Since `MEMORY.md` is agent-written (not user-written), this stricter escaping is applied by default. - **Local scope .gitignore check** — When using `local` scope, Zeph warns if `.zeph/agent-memory-local/` is not in `.gitignore`. +- **Path canonicalization** — Memory directory paths are canonicalized to detect and block symlink-based escape attempts. ## Tool and Skill Access diff --git a/specs/007-channels/spec.md b/specs/007-channels/spec.md index d2423cfef..6f063f69d 100644 --- a/specs/007-channels/spec.md +++ b/specs/007-channels/spec.md @@ -211,12 +211,45 @@ is a thin `reqwest`-based raw HTTP client that covers these gap methods. - All API calls share a single `#[tracing::instrument]` on the `post()` helper - `TelegramApiClient` is injected into `TelegramChannel` at construction; callers do not instantiate it directly +- The `reqwest::Client` is constructed with a 30-second request timeout (`REQUEST_TIMEOUT`) in both + `TelegramApiClient::new()` and `with_base_url()` (#3780); a client without a timeout violates the + project's Await Discipline rule ### Key Invariants - NEVER expose the bot token in `Debug`, `Display`, or log output - All methods must go through the shared `post()` helper — no ad-hoc `reqwest::Client` calls - New Bot API 10.0 methods that require raw HTTP must be added here, not as teloxide patches +- `TelegramApiClient` MUST set a `REQUEST_TIMEOUT` (30 s) on the `reqwest::Client` at construction — never create an unbounded client + +--- + +## Telegram Reaction Moderation Tools (#3770) + +Issue #3731. `crates/zeph-channels/src/telegram_moderation.rs`, `crates/zeph-tools/src/moderation.rs`. + +Two new tools allow group admins to remove reactions via the Telegram Bot API 10.0: + +| Tool | Description | +|---|---| +| `telegram_delete_reaction` | Delete the bot's own reaction on a specific message | +| `telegram_delete_all_reactions` | Delete all reactions on a specific message | + +### Design + +- `ReactionModerationBackend` trait lives in `zeph-tools` to avoid circular dependencies +- `TelegramModerationBackend` in `zeph-channels` implements the trait and enforces an admin check +- Admin status is verified via `bot_is_admin()` (calls `get_chat_member` + `get_me`) before every mutation +- If `getMe` fails at startup, the executor is not wired and a `WARN` is logged (graceful degradation) +- Reaction field is validated: non-empty, at most 10 characters +- `ClaimSource::Moderation` variant; `requires_confirmation = true` + +### Key Invariants + +- Mutation tools (`telegram_delete_reaction`, `telegram_delete_all_reactions`) MUST verify bot is chat admin before executing +- `requires_confirmation = true` — the agent must ask before removing reactions +- If bot admin check fails at startup, tools MUST be absent from the executor (not wired) — never silently no-op at call time +- Reaction strings MUST be validated (non-empty, ≤ 10 chars) before forwarding to the API --- diff --git a/specs/026-tui-subagent-management/spec.md b/specs/026-tui-subagent-management/spec.md index d57248497..a1cd5b1d7 100644 --- a/specs/026-tui-subagent-management/spec.md +++ b/specs/026-tui-subagent-management/spec.md @@ -251,6 +251,23 @@ The following details reflect the shipped implementation and may differ from the - Transcript is truncated to the **last 200 entries** (not configurable at runtime) - `[truncated]` marker shown at top when truncation occurs +### Automatic View Switch for Foreground Subagents (#3764) + +When the parent agent spawns a **foreground** subagent (i.e. `background: false`), the TUI +automatically switches to that subagent's transcript view so the user can follow its progress +in real time. On subagent completion the TUI returns to the Main view automatically. + +Two new `AgentEvent` variants drive the lifecycle transitions: + +| Variant | Fired | Effect | +|---|---|---| +| `ForegroundSubagentStarted { name }` | After spawn, before first poll | TUI switches to the subagent's transcript view | +| `ForegroundSubagentCompleted { name }` | After `poll_subagent_until_done` exits (including early drop) | TUI returns to Main view | + +The completion event is guaranteed to fire even when the subagent manager is dropped before +the poll loop finishes. This prevents the TUI from being stuck in a subagent view after the +parent turn ends. + ### Tab Cycling Tab cycling now includes `SubAgents` as a panel in the cycle order. `Shift+Tab` is not implemented; use `Esc` to return to main view. diff --git a/specs/033-subagent-context-propagation/spec.md b/specs/033-subagent-context-propagation/spec.md index c1f92d7c6..51fad18c2 100644 --- a/specs/033-subagent-context-propagation/spec.md +++ b/specs/033-subagent-context-propagation/spec.md @@ -365,7 +365,7 @@ See `TranscriptWriter` and `TranscriptReader` in `zeph-subagent`. | Gap | Priority | Resolved? | Status | Notes | |---|---|---|---|---| -| GAP-01: Conversation history | P1 | ✅ | Shipped #2575 | `parent_messages` in `SpawnContext` | +| GAP-01: Conversation history | P1 | ✅ | Shipped #2575, #3760, #3761 | `parent_messages` in `SpawnContext`; orphaned tool pairs pruned by `trim_parent_messages`; budget estimated by `estimate_parts_size` | | GAP-02: Parent context injection | P2 | ✅ | Shipped #2576 | `context_injection_mode` with `last_assistant_turn` | | GAP-03: Model inheritance | P2 | ✅ | Shipped #2577 | `model: "inherit"` in frontmatter | | GAP-04: MCP context propagation | P2 | ⚠️ Partial | Shipped #2578 | Tool executor shared; MCP server lifecycle not accessible | @@ -389,6 +389,8 @@ See `TranscriptWriter` and `TranscriptReader` in `zeph-subagent`. - Cancellation is propagated from parent to foreground children - Background spawns are tracked and can be collected or cancelled independently - All spawns include `spawn_depth` and are gated by `max_spawn_depth` +- Parent message history is pruned by `trim_parent_messages()` before passing to the subagent: any `ToolResult` without a paired `ToolUse` is removed first (pass 1), then any `ToolUse` without a paired `ToolResult` is removed (pass 2), except the trailing assistant message which is exempt from pass 2 to preserve pending tool calls at the conversation boundary (#3760) +- Token budget for the sliced parent history is estimated by `estimate_parts_size()`, which uses a 50-byte JSON overhead for `ToolUse`/`ToolResult` blocks, base64 expansion ratio for images, and `content.len()` fallback; `flatten_parts()` byte counting is NOT used for budget estimation because it underestimates structured JSON size (#3761) ### Ask First - Enabling `background = true` for long-running subagents (risk of orphaned tasks) diff --git a/specs/044-subagent-lifecycle/spec.md b/specs/044-subagent-lifecycle/spec.md index 317bb3382..6b3a7f70c 100644 --- a/specs/044-subagent-lifecycle/spec.md +++ b/specs/044-subagent-lifecycle/spec.md @@ -175,13 +175,15 @@ AND memory content exceeding the token budget is truncated, not omitted entirely | FR-005 | WHEN `SubAgentManager::spawn()` is called at the concurrency limit THEN the system SHALL return `Err(ConcurrencyLimitExceeded)` | must | | FR-006 | WHEN a subagent is spawned with `parent_cancel` THEN cancelling the parent token SHALL cancel the child's `CancellationToken` | must | | FR-007 | WHEN a `Grant` TTL expires THEN `PermissionGrants::check()` SHALL return an error for that grant | must | -| FR-008 | WHEN `FilteredToolExecutor` receives a tool call THEN it SHALL check the `ToolPolicy` and denylist before forwarding to the real executor | must | +| FR-008 | WHEN `FilteredToolExecutor` receives a tool call THEN it SHALL check the `ToolPolicy` and denylist before forwarding to the real executor, matching tool IDs case-insensitively after stripping argument suffixes (`"Bash(cargo *)"` → `"bash"`) via `normalize_tool_id` (#3765) | must | | FR-009 | WHEN a subagent session ends (normally or via cancellation) THEN a JSONL transcript SHALL be written with complete turn history | must | | FR-010 | WHEN `sweep_old_transcripts()` is called THEN transcripts beyond the retention window SHALL be deleted | must | | FR-011 | WHEN lifecycle hooks are defined THEN `fire_hooks()` SHALL execute matching hooks for each `HookType` event | must | | FR-012 | WHEN hook execution fails THEN the failure SHALL be logged at `WARN` and the subagent session SHALL continue | must | | FR-013 | WHEN `load_memory_content()` is called THEN it SHALL read `MEMORY.md` from the resolved memory directory and return its content | should | | FR-014 | WHEN `AgentCommand` or `AgentsCommand` is parsed from user input THEN it SHALL map to a typed command variant (`spawn`, `list`, `cancel`, `resume`, `show`) | must | +| FR-015 | WHEN a subagent is spawned with `memory: user` in its definition THEN the system SHALL wrap the tool executor with `MemoryAwareExecutor`, which retries `SandboxViolation` file-tool calls against a `FileExecutor` scoped to `~/.zeph/agent-memory//` (#3771) | must | +| FR-016 | WHEN `MemoryAwareExecutor` resolves the memory directory THEN it SHALL canonicalize the path and reject any resolved path that escapes `~/.zeph/agent-memory//` to prevent traversal attacks (#3771) | must | --- @@ -212,7 +214,8 @@ AND memory content exceeding the token budget is truncated, not omitted entirely | `PermissionGrants` | TTL-bounded permission registry | Map of `GrantKind` → expiry timestamp | | `Grant` | Single permission grant | `kind: GrantKind`, `ttl_secs`, expiry instant | | `GrantKind` | Type of permission | Variants: `VaultSecret`, `Tool` | -| `FilteredToolExecutor` | Tool executor with policy gate | Wraps real executor; enforces `ToolPolicy` and denylist | +| `FilteredToolExecutor` | Tool executor with policy gate | Wraps real executor; enforces `ToolPolicy` and denylist; tool ID comparison is case-insensitive and strips argument suffixes via `normalize_tool_id` | +| `MemoryAwareExecutor` | Sandbox-bypass executor for `memory: user` subagents | Wraps inner executor; retries `SandboxViolation` file-tool calls against a `FileExecutor` scoped to `~/.zeph/agent-memory//`; path canonicalization delegated to `FileExecutor` to prevent traversal (#3771) | | `PlanModeExecutor` | Executor for plan mode | Wraps real executor; disables write operations | | `HookDef` | Lifecycle hook definition | `hook_type: HookType`, shell command template | | `HookType` | Lifecycle event | `PreToolUse`, `PostToolUse`, `SubagentStart`, `SubagentStop` | @@ -236,6 +239,8 @@ AND memory content exceeding the token budget is truncated, not omitted entirely | Grant checked after TTL expiry | Returns `Err`; no panic | | Subagent cancelled mid-turn | Tool in progress receives cancellation signal; transcript records `Cancelled` exit reason | | `load_all()` encounters symlink outside allowed boundary | File is skipped with a security warning in logs | +| Subagent with `memory: user` writes to a file outside `~/.zeph/agent-memory//` | `MemoryAwareExecutor` rejects the call; the canonicalized path does not start with the allowed prefix | +| Subagent name contains path traversal components in memory path construction | `MemoryAwareExecutor` validates the agent name via `is_valid_agent_name()` before constructing the memory path | --- diff --git a/specs/050-security-capability-governance/spec.md b/specs/050-security-capability-governance/spec.md index 021c4eb94..5ed317f9e 100644 --- a/specs/050-security-capability-governance/spec.md +++ b/specs/050-security-capability-governance/spec.md @@ -161,10 +161,22 @@ prefixed by namespace before scope resolution: Glob patterns operate on these qualified ids. `mcp:*` is a single-namespace glob; `*` covers all namespaces and is allowed only in the explicit -`default_scope = "general"` configuration. Any *un-namespaced* tool id -returned by an executor at registration is a fatal startup error — there is -no path for an MCP server to register `search_arbitrary_shell` and have it -match `search_*` configured for builtins. +`default_scope = "general"` configuration. + +**Built-in id normalization (#3778).** Built-in executors register tools with +*unqualified* ids (`"bash"`, `"read"`, etc.) because they predate the namespace +convention. At the scope boundary, `ScopedToolExecutor::tool_definitions()` and +`execute_tool_call()` both synthesise the `builtin:` prefix for unqualified ids +before calling `scope.admits()`. Additionally, `runner.rs` qualifies unqualified +registry ids with `builtin:` before passing them to `build_scoped_executor` so +the pre-compiled admitted set resolves correctly. Without this normalization, +configuring `[security.capability_scopes]` caused a startup crash (#3775) because +the admitted set contained `"builtin:bash"` but incoming dispatch carried `"bash"`. + +MCP and other dynamic-namespace tools are expected to include a namespace separator +in their registered id; an MCP server that registers `search_arbitrary_shell` +(no colon) would be treated as a built-in and admitted only if `builtin:*` is in +scope — there is no cross-namespace leakage. **Build-time pattern resolution (C2 mitigation).** At agent build, every configured glob is compiled and matched against the materialised tool @@ -559,7 +571,12 @@ follow-on. full patterns are fatal startup errors. 5. **Tool ids are namespaced before scoping.** `builtin:`, `skill:/`, `mcp:/`, `acp:/`, `a2a:/`. No cross-namespace - accidental match. + accidental match. Built-in executors register tools with *unqualified* + ids (`"bash"`, `"read"`, etc.); `ScopedToolExecutor` normalises them to + `builtin:` at the scope boundary (#3778). `runner.rs` also qualifies + unqualified ids with `builtin:` before passing them to + `build_scoped_executor` so the scope's pre-compiled admitted set + resolves correctly. 6. **Sentinel state is per-`SecurityState`** (per-agent). Subagents inherit only via `spawn_child` with `subagent_inheritance_factor` damping, and only when the parent is `>= Elevated`. @@ -579,7 +596,8 @@ follow-on. `execute_confirmed()` fenced-block path. Documented carve-out (mirrors `PolicyGate` CRIT-03). - NEVER use `glob::Pattern::matches` against an unqualified, non-namespaced - tool id. All tool ids are `:` before scope resolution. + tool id. Scope resolution always operates on qualified ids; `ScopedToolExecutor` + auto-qualifies built-in ids with `builtin:` before calling `scope.admits()` (#3778). - NEVER let an MCP server register a tool id that contains a `:` other than the namespace separator inserted by the registry. - NEVER let `tool_definitions()` for one scope be reused for dispatch @@ -612,8 +630,11 @@ follow-on. 1. `crates/zeph-tools/src/scope.rs` — `ToolScope`, `ScopedToolExecutor`, `ScopeError { DeadPattern, AccidentallyFull, … }`. Wired in `agent::builder` between `PolicyGateExecutor` and the agent stack. -2. Tool-id namespacing: registry guarantees every `ToolDef.id` carries a - namespace prefix; un-namespaced ids are rejected at registration. +2. Tool-id namespacing: built-in executors register unqualified ids; + `ScopedToolExecutor` normalises them to `builtin:` at the scope + boundary. MCP / ACP / A2A tools carry an explicit namespace prefix from + their registry. Un-namespaced non-builtin ids that somehow reach the + scope boundary are treated as `builtin:` for admission purposes. 3. `zeph_config::types::security::CapabilityScopesConfig` — TOML schema, build-time glob resolution, `strict` and `default_scope` semantics. 4. `crates/zeph-core/src/agent/trajectory.rs` — diff --git a/specs/README.md b/specs/README.md index 776705fe8..c0ab33d12 100644 --- a/specs/README.md +++ b/specs/README.md @@ -77,7 +77,7 @@ Spec IDs (001–044) follow a logical grouping: | `004-memory/004-13-memory-memcot.md` | MemCoT: SemanticStateAccumulator, Zoom-In evidence localization, Zoom-Out causal expansion (#3592) | `zeph-memory` | | `005-skills/spec.md` | SKILL.md format, registry, matching, hot-reload, skill trust governance, two-stage matching, Wilson score confidence intervals, hub install pipeline, agent-invocable skills (`invoke_skill`) | `zeph-skills` | | `006-tools/spec.md` | ToolExecutor, CompositeExecutor, TAFC, schema filter, result cache, dependency graph, tool invocation phase taxonomy, native `tool_use` only; `invoke_skill`/`load_skill` utility-gate exemption | `zeph-tools` | -| `007-channels/spec.md` | Channel trait, AnyChannel dispatch, streaming, channel feature parity, `stream_interval_ms` (Bot API 10.0, #3727) | `zeph-channels` | +| `007-channels/spec.md` | Channel trait, AnyChannel dispatch, streaming, channel feature parity, `stream_interval_ms` (Bot API 10.0, #3727); `TelegramApiClient` 30s `REQUEST_TIMEOUT` on reqwest client (#3780); Telegram reaction moderation tools `telegram_delete_reaction` / `telegram_delete_all_reactions` (#3770) | `zeph-channels` | | `007-channels/007-1-telegram-guest-mode.md` | Telegram Guest Mode — `guest_message` update handling, `answerGuestQuery` routing, `allowed_users` access control, single-shot streaming (#3729) | `zeph-channels`, `zeph-core`, `zeph-config` | | `007-channels/007-2-telegram-bot-to-bot.md` | Telegram Bot-to-Bot — `setManagedBotAccessSettings` startup, `allowed_bots` authorization, reply-chain loop prevention, `is_from_bot` metadata (#3730) | `zeph-channels`, `zeph-core`, `zeph-config` | | `008-mcp/spec.md` | MCP client, server lifecycle, semantic tool discovery, per-message pruning cache, injection detection, tool collision detection, caller identity propagation, tool quota, structured error codes, OAP authorization, elicitation (2025-06-18) | `zeph-mcp` | @@ -101,14 +101,14 @@ Spec IDs (001–044) follow a logical grouping: | `023-complexity-triage-routing/spec.md` | Pre-inference complexity classification routing, ComplexityTier, TriageRouter, context escalation, metrics | `zeph-llm`, `zeph-config`, `zeph-core` | | `024-multi-model-design/spec.md` | Multi-model design principle: complexity tiers, `*_provider` subsystem reference pattern, STT unification | cross-cutting | | `025-classifiers/spec.md` | Candle-backed ML classifiers: injection detection, PII detection, LlmClassifier for feedback, unified regex+NER sanitization pipeline | `zeph-classifiers` | -| `026-tui-subagent-management/spec.md` | Interactive TUI subagent sidebar (a key), j/k navigation, Enter loads transcript, Esc returns, Tab cycling | `zeph-tui` | +| `026-tui-subagent-management/spec.md` | Interactive TUI subagent sidebar (a key), j/k navigation, Enter loads transcript, Esc returns, Tab cycling; automatic view switch to foreground subagent on spawn + return to Main on completion via `ForegroundSubagentStarted`/`ForegroundSubagentCompleted` `AgentEvent` variants (#3764) | `zeph-tui` | | `027-runtime-layer/spec.md` | RuntimeLayer middleware with before_chat/after_chat/before_tool/after_tool hooks, NoopLayer, LayerContext, unwind guards; plugin config overlay merge (tighten-only) | `zeph-core` | -| `028-hooks/spec.md` | Reactive hooks: cwd_changed / file_changed / permission_denied / turn_complete / pre_tool_use / post_tool_use events, set_working_directory tool, FileChangeWatcher, ZEPH_TOOL_NAME / ZEPH_TOOL_ARGS_JSON / ZEPH_SESSION_ID env vars (#3725); pre_tool_use fires before utility gate (#3738) | `zeph-core` | +| `028-hooks/spec.md` | Reactive hooks: cwd_changed / file_changed / permission_denied / turn_complete / pre_tool_use / post_tool_use events, set_working_directory tool, FileChangeWatcher, ZEPH_TOOL_NAME / ZEPH_TOOL_ARGS_JSON / ZEPH_SESSION_ID env vars (#3725); pre_tool_use fires before utility gate (#3738); permission_denied fires at all gate denial points in tier loop (#3779); turn_complete McpTool hooks wired to live MCP dispatch (#3776) | `zeph-core` | | `029-feature-flags/spec.md` | Feature flag decision rules, surviving flag inventory (22 flags), bundle definitions (desktop, ide, server, full) | `Cargo.toml`, cross-cutting | | `030-tui-slash-autocomplete/spec.md` | Inline autocomplete dropdown in TUI Insert mode, reuses filter_commands registry, Tab/Enter accepts, Esc dismisses | `zeph-tui` | | `031-database-abstraction/spec.md` | PostgreSQL backend, zeph-db crate, DatabaseDriver trait, Dialect trait, sql!() macro, migrations, CLI subcommands | `zeph-db`, cross-cutting | | `032-handoff-skill-system/spec.md` | Skill-based YAML handoff protocol for inter-agent communication, structured skill exchange | `zeph-orchestration` | -| `033-subagent-context-propagation/spec.md` | Gap analysis and resolution plan for `/agent spawn` context propagation | `zeph-subagent`, `zeph-core` | +| `033-subagent-context-propagation/spec.md` | Gap analysis and resolution plan for `/agent spawn` context propagation; orphaned ToolUse/ToolResult pairs pruned by `trim_parent_messages` (#3760); token budget estimated by `estimate_parts_size` (#3761) | `zeph-subagent`, `zeph-core` | | `034-zeph-bench/spec.md` | Benchmark harness: BenchmarkChannel, dataset loaders, CLI `zeph bench run`, memory isolation, deterministic mode, baseline comparison | `zeph-bench` | | `035-profiling/spec.md` | Two-tier telemetry (Tier 1: local chrome traces, Tier 2: OTLP + Pyroscope), per-span `#[instrument]` macros, allocation tracking, InstrumentedChannel wrappers, system metrics; zero-overhead when disabled | cross-cutting | | `036-prometheus-metrics/spec.md` | Prometheus `/metrics` endpoint, OpenMetrics export, ~25 gauge/counter metrics from MetricsSnapshot, feature-gated with gateway | `zeph-gateway`, binary | @@ -119,7 +119,7 @@ Spec IDs (001–044) follow a logical grouping: | `041-experiments/spec.md` | Experiments & Runtime Feature Gating: `[experiments]` config section, ExperimentConfig, rollout percentage, experiment results reporting, CLI subcommands; distinct from compile-time feature flags | `zeph-experiments` | | `042-zeph-commands/spec.md` | Slash command registry, `CommandHandler` object-safe trait, `CommandRegistry` with longest-word-boundary dispatch, `ChannelSink` abstraction, static `COMMANDS` list; `/recap` command, `/session` TUI commands; no dependency on `zeph-core` | `zeph-commands` | | `043-zeph-common/spec.md` | Shared primitives: `Secret` (zeroize-on-drop), `ToolName` (Arc), `SessionId` (UUID v4), `ToolDefinition`, `SkillTrustLevel`, `PolicyLlmClient`; no `zeph-*` peer dependencies | `zeph-common` | -| `044-subagent-lifecycle/spec.md` | Full `zeph-subagent` crate: `SubAgentDef` parsing, `SubAgentManager` spawning and concurrency cap, `PermissionGrants` TTL, `FilteredToolExecutor` policy gate, transcript JSONL persistence, lifecycle hooks, memory injection | `zeph-subagent` | +| `044-subagent-lifecycle/spec.md` | Full `zeph-subagent` crate: `SubAgentDef` parsing, `SubAgentManager` spawning and concurrency cap, `PermissionGrants` TTL, `FilteredToolExecutor` policy gate (case-insensitive tool ID normalization via `normalize_tool_id`, #3765), `MemoryAwareExecutor` for `memory: user` subagents (#3771), transcript JSONL persistence, lifecycle hooks, memory injection | `zeph-subagent` | | `045-interop-protocol-gaps/spec.md` | Agent interoperability protocol gap analysis (arXiv:2505.02279): capability matrix for MCP, ACP, A2A, ANP vs. Zeph; protocol selection guidance; ANP as P4 research; ACP re-negotiation as P3 follow-up | cross-cutting | | `046-march-quality/spec.md` | MARCH Proposer+Checker self-check pipeline: post-response factual consistency, information-asymmetry checker, `self-check` feature flag, per-turn `MarchVerdict`, Prometheus metrics (#3226) | `zeph-core` | | `047-cli-modes/spec.md` | CLI execution modes: `--bare` (skip scheduler/indexer/eviction), `--json` (JSONL event stream), `-y` (auto-approve), `/loop` command (supervised loop with inline errors), `/recap` command (#3170, #3218) | `zeph-channels`, binary | @@ -127,7 +127,7 @@ Spec IDs (001–044) follow a logical grouping: | `UX/mention-routing.md` | @agent mention routing: Goose pattern analysis, feasibility for Zeph TUI/A2A, verdict to defer pending `AgentRegistry` infrastructure (#3327) | `zeph-core`, `zeph-tui`, `zeph-a2a` | | `048-slm-cost-metrics/spec.md` | SLM survey findings (arXiv:2510.03847), CPS (cost per successful task) metric contract, `record_successful_task()` / `cps()` API, daily reset semantics | `zeph-core` | | `049-agent-decomposition/spec.md` | Agent god-object Phase 2 (#3509): split `Agent` 25+ direct sub-state fields into `services: Services` (background subsystems) and `runtime: AgentRuntime` (config, lifecycle, providers, metrics, debug, instructions); pure refactor, no API change, separately borrowable; `TurnContext` boundary sketched for P2-prereq-3 | `zeph-core` | -| `050-security-capability-governance/spec.md` | Capability scoping (`ScopedToolExecutor` + per-task-type allow-lists, #3563), `TrajectorySentinel` multi-turn risk accumulator with decay (#3570), Phase 2: `ShadowSentinel` safety probe — `SafetyProbe` trait, `LlmSafetyProbe`, `ShadowProbeExecutor`, `safety_shadow_events` table, `classify_tool` bare-ID fix (#3705, #3735, #3739, #3742, #3744, #3745), CapSeal/SUDP `VaultBroker::propose_operation` Phase-3 research sketch (#3569) | `zeph-tools`, `zeph-core` | +| `050-security-capability-governance/spec.md` | Capability scoping (`ScopedToolExecutor` + per-task-type allow-lists, #3563), `TrajectorySentinel` multi-turn risk accumulator with decay (#3570), Phase 2: `ShadowSentinel` safety probe — `SafetyProbe` trait, `LlmSafetyProbe`, `ShadowProbeExecutor`, `safety_shadow_events` table, `classify_tool` bare-ID fix (#3705, #3735, #3739, #3742, #3744, #3745), CapSeal/SUDP `VaultBroker::propose_operation` Phase-3 research sketch (#3569); built-in tool id `builtin:` normalization at `ScopedToolExecutor` dispatch boundary (#3778) | `zeph-tools`, `zeph-core` | | `051-gonka-gateway/spec.md` | Phase 1: gonka.ai inference via GonkaGate hosted gateway — zero new Rust code, `CompatibleProvider` reuse, wizard branch, vault key `ZEPH_COMPATIBLE_GONKAGATE_API_KEY` | `zeph-llm`, `zeph-config` | | `052-gonka-native/spec.md` | Phase 2: native gonka network transport — `GonkaProvider`, ECDSA secp256k1 signing (`RequestSigner`), `EndpointPool` round-robin fail-skip, `send_signed_with_retry`, `chat_with_tools`, `chat_typed`, `zeph gonka doctor` | `zeph-llm`, `zeph-config` | | `053-speculation-engine/spec.md` | `SpeculationEngine` — speculative tool execution: `PartialJsonParser` SSE decoding path, PASTE skill activation, `try_dispatch`/`try_commit`/`end_turn` API, `ToolStartEvent{speculative:true}`, `DynExecutor` confirmation delegation | `zeph-core`, `zeph-tools` |