Skip to main content

zeph_config/
subagent.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use serde::{Deserialize, Serialize};
5
6// ── PermissionMode ─────────────────────────────────────────────────────────
7
8/// Controls tool execution and prompt interactivity for a sub-agent.
9///
10/// For sub-agents (non-interactive), `Default`, `AcceptEdits`, `DontAsk`, and
11/// `BypassPermissions` are functionally equivalent — sub-agents never prompt the
12/// user. The meaningful differentiator is `Plan` mode, which suppresses all tool
13/// execution and returns only the plan text.
14#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum PermissionMode {
17    /// Standard behavior — prompt for each action (sub-agents auto-approve).
18    #[default]
19    Default,
20    /// Auto-accept file edits without prompting.
21    AcceptEdits,
22    /// Auto-approve all tool calls without prompting.
23    DontAsk,
24    /// Unrestricted tool access; emits a warning when loaded.
25    BypassPermissions,
26    /// Read-only planning: tools are visible in the catalog but execution is blocked.
27    Plan,
28}
29
30// ── MemoryScope ────────────────────────────────────────────────────────────
31
32/// Persistence scope for sub-agent memory files.
33///
34/// Determines where the agent's `MEMORY.md` and topic files are stored across sessions.
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
36#[serde(rename_all = "snake_case")]
37pub enum MemoryScope {
38    /// User-level: `~/.zeph/agent-memory/<name>/`.
39    User,
40    /// Project-level: `.zeph/agent-memory/<name>/`.
41    Project,
42    /// Local-only: `.zeph/agent-memory-local/<name>/`.
43    Local,
44}
45
46// ── ToolPolicy ─────────────────────────────────────────────────────────────
47
48/// Tool access policy for a sub-agent.
49///
50/// Controls which tools the sub-agent may call, independent of the global tool denylist.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ToolPolicy {
54    /// Only the listed tool IDs are accessible.
55    AllowList(Vec<String>),
56    /// All tools except those in the list are accessible.
57    DenyList(Vec<String>),
58    /// Inherit the full tool set from the parent agent (no additional filtering).
59    InheritAll,
60}
61
62// ── SkillFilter ────────────────────────────────────────────────────────────
63
64/// Skill allow/deny filter for sub-agent definitions.
65///
66/// Skills named in `include` are the only ones loaded; `exclude` removes
67/// specific skills from the inherited set. When both are empty the sub-agent
68/// inherits all parent skills.
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70pub struct SkillFilter {
71    /// Explicit skill names to include (empty = inherit all).
72    pub include: Vec<String>,
73    /// Skill names to remove from the inherited set.
74    pub exclude: Vec<String>,
75}
76
77impl SkillFilter {
78    /// Returns `true` when no filter is applied (all skills are inherited).
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use zeph_config::SkillFilter;
84    ///
85    /// assert!(SkillFilter::default().is_empty());
86    /// ```
87    #[must_use]
88    pub fn is_empty(&self) -> bool {
89        self.include.is_empty() && self.exclude.is_empty()
90    }
91}
92
93// ── HookDef / HookAction / HookMatcher / SubagentHooks ────────────────────
94
95/// The action a hook executes when triggered.
96///
97/// Hooks either run a shell command or invoke an MCP server tool directly.
98///
99/// # Examples
100///
101/// ```toml
102/// # Shell command hook
103/// [[hooks.cwd_changed]]
104/// type = "command"
105/// command = "echo $ZEPH_NEW_CWD"
106/// timeout_secs = 10
107///
108/// # MCP tool hook
109/// [[hooks.permission_denied]]
110/// type = "mcp_tool"
111/// server = "policy-server"
112/// tool = "audit_denied"
113/// [hooks.permission_denied.args]
114/// severity = "high"
115/// ```
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(tag = "type", rename_all = "snake_case")]
118pub enum HookAction {
119    /// Execute a shell command via `sh -c`.
120    Command {
121        /// The shell command to run.
122        command: String,
123    },
124    /// Invoke an MCP server tool directly without spawning a subprocess.
125    McpTool {
126        /// The MCP server ID as declared in `[[mcp.servers]]`.
127        server: String,
128        /// The tool name to call on that server.
129        tool: String,
130        /// Optional JSON arguments passed to the tool. Defaults to `{}`.
131        #[serde(default)]
132        args: serde_json::Value,
133    },
134}
135
136fn default_hook_timeout() -> u64 {
137    30
138}
139
140/// A single hook definition.
141///
142/// Hooks are fired at specific lifecycle points. The `action` field determines
143/// whether the hook runs a shell command or dispatches to an MCP server tool.
144///
145/// # Examples
146///
147/// ```toml
148/// [[hooks.cwd_changed]]
149/// type = "command"
150/// command = "echo changed to $ZEPH_NEW_CWD"
151/// timeout_secs = 10
152/// fail_closed = false
153/// ```
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct HookDef {
156    /// The action to execute: shell command or MCP tool call.
157    #[serde(flatten)]
158    pub action: HookAction,
159    /// Maximum seconds to wait for the hook before timing out. Default: 30.
160    #[serde(default = "default_hook_timeout")]
161    pub timeout_secs: u64,
162    /// When `true`, a non-zero exit code or timeout aborts the remaining hooks in the same
163    /// sequence (no further hooks in the list run). When `false` (default), errors are logged
164    /// and the next hook in the sequence continues.
165    ///
166    /// Note: in `pre_tool_use` and `post_tool_use` contexts this field controls hook-chain
167    /// execution only — it does **not** block the tool call itself. Hook dispatch is always
168    /// fail-open at the agent level; the tool executes regardless of hook outcomes.
169    #[serde(default)]
170    pub fail_closed: bool,
171}
172
173/// Tool-name matcher with associated hooks.
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct HookMatcher {
176    pub matcher: String,
177    pub hooks: Vec<HookDef>,
178}
179
180/// Per-agent frontmatter hook collections (`PreToolUse` / `PostToolUse`).
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182#[serde(rename_all = "PascalCase")]
183pub struct SubagentHooks {
184    #[serde(default)]
185    pub pre_tool_use: Vec<HookMatcher>,
186    #[serde(default)]
187    pub post_tool_use: Vec<HookMatcher>,
188}
189
190impl SubagentHooks {
191    /// Returns `true` when no pre- or post-tool-use hooks are configured.
192    #[must_use]
193    pub fn is_empty(&self) -> bool {
194        self.pre_tool_use.is_empty() && self.post_tool_use.is_empty()
195    }
196}