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