Skip to main content

zeph_subagent/manager/
secrets.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use std::collections::HashMap;
5use std::time::Duration;
6
7use super::SubAgentManager;
8use crate::error::SubAgentError;
9use crate::grants::SecretRequest;
10
11/// Build the standard hook environment for a sub-agent lifecycle event.
12pub(crate) fn make_hook_env(
13    task_id: &str,
14    agent_name: &str,
15    tool_name: &str,
16) -> HashMap<String, String> {
17    let mut env = HashMap::new();
18    env.insert("ZEPH_AGENT_ID".to_owned(), task_id.to_owned());
19    env.insert("ZEPH_AGENT_NAME".to_owned(), agent_name.to_owned());
20    env.insert("ZEPH_AGENT_TYPE".to_owned(), "subagent".to_owned());
21    env.insert("ZEPH_TOOL_NAME".to_owned(), tool_name.to_owned());
22    env
23}
24
25impl SubAgentManager {
26    /// Approve a secret request for a running sub-agent.
27    ///
28    /// Called after the user approves a vault secret access prompt. The secret
29    /// key must appear in the sub-agent definition's allowed `secrets` list;
30    /// otherwise the request is auto-denied.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`SubAgentError::NotFound`] if the task ID is unknown,
35    /// [`SubAgentError::Invalid`] if the key is not in the definition's allowed list.
36    pub fn approve_secret(
37        &mut self,
38        task_id: &str,
39        secret_key: &str,
40        ttl: Duration,
41    ) -> Result<(), SubAgentError> {
42        let handle = self
43            .agents
44            .get_mut(task_id)
45            .ok_or_else(|| SubAgentError::NotFound(task_id.to_owned()))?;
46
47        handle.grants.sweep_expired();
48
49        if !handle
50            .def
51            .permissions
52            .secrets
53            .iter()
54            .any(|k| k == secret_key)
55        {
56            tracing::warn!(task_id, "secret request denied: key not in allowed list");
57            return Err(SubAgentError::Invalid(format!(
58                "secret is not in the allowed secrets list for '{}'",
59                handle.def.name
60            )));
61        }
62
63        handle.grants.grant_secret(secret_key, ttl);
64        Ok(())
65    }
66
67    /// Deliver a secret value to a waiting sub-agent loop.
68    ///
69    /// Should be called after the user approves the request and the vault value
70    /// has been resolved. Returns an error if no such agent is found.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`SubAgentError::NotFound`] if the task ID is unknown.
75    pub fn deliver_secret(&mut self, task_id: &str, key: String) -> Result<(), SubAgentError> {
76        let handle = self
77            .agents
78            .get_mut(task_id)
79            .ok_or_else(|| SubAgentError::NotFound(task_id.to_owned()))?;
80        handle
81            .secret_tx
82            .try_send(Some(key))
83            .map_err(|e| SubAgentError::Channel(e.to_string()))
84    }
85
86    /// Deny a pending secret request — sends `None` to unblock the waiting sub-agent loop.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`SubAgentError::NotFound`] if the task ID is unknown,
91    /// [`SubAgentError::Channel`] if the channel is full or closed.
92    pub fn deny_secret(&mut self, task_id: &str) -> Result<(), SubAgentError> {
93        let handle = self
94            .agents
95            .get_mut(task_id)
96            .ok_or_else(|| SubAgentError::NotFound(task_id.to_owned()))?;
97        handle
98            .secret_tx
99            .try_send(None)
100            .map_err(|e| SubAgentError::Channel(e.to_string()))
101    }
102
103    /// Try to receive a pending secret request from any sub-agent (non-blocking).
104    ///
105    /// Polls each active agent's request channel once. Returns `Some((task_id, request))`
106    /// if any agent has a pending request, or `None` if all channels are empty.
107    /// Call this from the main agent loop to surface approval prompts to the user.
108    pub fn try_recv_secret_request(&mut self) -> Option<(String, SecretRequest)> {
109        for handle in self.agents.values_mut() {
110            if let Ok(req) = handle.pending_secret_rx.try_recv() {
111                return Some((handle.task_id.clone(), req));
112            }
113        }
114        None
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::make_hook_env;
121
122    #[test]
123    fn make_hook_env_sets_agent_type_subagent() {
124        let env = make_hook_env("task-42", "my-agent", "Shell");
125        assert_eq!(
126            env.get("ZEPH_AGENT_TYPE").map(String::as_str),
127            Some("subagent")
128        );
129        assert_eq!(
130            env.get("ZEPH_AGENT_ID").map(String::as_str),
131            Some("task-42")
132        );
133        assert_eq!(
134            env.get("ZEPH_AGENT_NAME").map(String::as_str),
135            Some("my-agent")
136        );
137        assert_eq!(env.get("ZEPH_TOOL_NAME").map(String::as_str), Some("Shell"));
138    }
139}