scv_protocol/daemon.rs
1//! Daemon control: health, delegations, and the commands `scv` sends over
2//! `daemon.control`.
3
4use serde::{Deserialize, Serialize};
5
6/// Where a daemon component (a channel account) is in its lifecycle.
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum ComponentState {
10 /// Turned off in configuration.
11 Disabled,
12 /// Starting up.
13 Starting,
14 /// Running and connected to its platform.
15 Connected,
16 /// Running but not connected.
17 Disconnected,
18 /// Waiting before a restart after a failure.
19 Backoff,
20 /// Shutting down.
21 Stopping,
22 /// Not running.
23 Stopped,
24 /// Stopped after an error it cannot recover from, such as expired credentials.
25 Failed,
26}
27
28/// The health of one channel account.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ComponentHealth {
31 /// Component ID, `<channel>:<account>`.
32 pub id: String,
33 /// The chat channel this account belongs to, such as `wechat`.
34 #[serde(default)]
35 pub channel: String,
36 /// The account name within the channel.
37 pub account: String,
38 /// The platform's ID of the bot account, when known.
39 pub bot_id: Option<String>,
40 /// The platform's ID of the account's owner, when known.
41 pub user_id: Option<String>,
42 /// Whether the account should run.
43 pub enabled: bool,
44 /// Lifecycle state.
45 pub state: ComponentState,
46 /// When the platform last answered successfully.
47 pub last_success_unix_seconds: Option<u64>,
48 /// The latest error, if any.
49 pub error: Option<String>,
50 /// Restarts since the daemon started.
51 pub restarts: u64,
52 /// Effective remote tool authority; `owner` only when the owner ID is known.
53 #[serde(default)]
54 pub remote_tools: RemoteTools,
55 /// Who the account answers, as set; `owner` with no known owner ID
56 /// answers nobody. `None` from daemons before 0.3.0, which answer anyone.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub senders: Option<Senders>,
59}
60
61/// Who may use tools through a remote bridge account.
62#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "snake_case")]
64pub enum RemoteTools {
65 /// Every remote session is tool-free (the default).
66 #[default]
67 None,
68 /// The account's authenticated owner gets full, auto-approved tools.
69 Owner,
70}
71
72/// Whose messages a remote bridge account answers.
73#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
74#[serde(rename_all = "snake_case")]
75pub enum Senders {
76 /// Only the account's authenticated owner (the default); everyone else's
77 /// messages are dropped unanswered. Without a known owner ID, nobody.
78 #[default]
79 Owner,
80 /// Anyone who can reach the bot; everyone but the owner stays tool-free.
81 Anyone,
82}
83
84/// What `scv status` shows: the daemon, its components, and its delegations.
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86pub struct DaemonStatus {
87 /// The daemon's release.
88 pub version: String,
89 /// The daemon's process ID.
90 pub pid: u32,
91 /// Channel accounts.
92 pub components: Vec<ComponentHealth>,
93 /// Delegated agent runs.
94 #[serde(default)]
95 pub delegations: DelegationSummary,
96 /// A restart the daemon has scheduled, if any.
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub restart: Option<RestartInfo>,
99 /// The question a `confirm_ask` or `confirm_status` request is about.
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub confirm: Option<ConfirmInfo>,
102}
103
104/// How long a question to the owner waits for an answer unless the asker
105/// says otherwise.
106pub const DEFAULT_CONFIRM_SECONDS: u64 = 30 * 60;
107/// The longest a question to the owner may wait: the default ceiling of a
108/// delegated agent's own tool call (`tools.max_timeout_seconds`), which is
109/// how a delegated agent asks.
110pub const MAX_CONFIRM_SECONDS: u64 = 4 * 60 * 60;
111
112/// A yes/no question to the owner in chat (`scv confirm`).
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct ConfirmInfo {
115 /// The question's ID, for `confirm_status`.
116 pub id: String,
117 /// Where it stands.
118 pub state: ConfirmState,
119 /// The account whose owner was asked, as `<channel>:<account>`.
120 pub chat: String,
121 /// When no answer counts as no.
122 pub deadline_unix_seconds: u64,
123}
124
125/// Where a question to the owner stands.
126#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
127#[serde(rename_all = "snake_case")]
128pub enum ConfirmState {
129 /// Sent, or being stored for sending, and waiting for an answer.
130 Pending,
131 /// The owner answered yes.
132 Yes,
133 /// The owner answered no.
134 No,
135 /// No answer came before the deadline, which counts as no.
136 Expired,
137 /// The asker stopped asking about it before an answer came.
138 Withdrawn,
139 /// The question could not be handed to the chat, or its answer was lost.
140 Failed,
141 /// A state this client does not know, from a newer daemon.
142 #[serde(other)]
143 Unknown,
144}
145
146/// A restart into a newly installed release, waiting for owner work to end.
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148pub struct RestartInfo {
149 /// The release it restarts into.
150 pub to_version: String,
151 /// What it still waits for, such as the requesting delegation or an
152 /// owner's message; `None` once it restarts.
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub waiting_for: Option<String>,
155 /// The delegation that asked, whose report goes out first.
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub requester: Option<String>,
158 /// The chat the announcement goes to, as `<channel>:<account>`.
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub origin: Option<String>,
161 /// When it restarts even if work is still running.
162 pub deadline_unix_seconds: u64,
163}
164
165/// Delegated agent runs of the daemon's SCV instance.
166#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
167pub struct DelegationSummary {
168 /// Running delegations, whichever SCV process of the instance started them.
169 pub active: u64,
170 /// Orphaned delegations the daemon has stopped since it started.
171 pub reaped: u64,
172 /// Listed delegations, for `delegations` and `delegation_kill`.
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub entries: Vec<DelegationInfo>,
175 /// Handles this request stopped.
176 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub killed: Vec<String>,
178}
179
180/// One running delegated agent.
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
182pub struct DelegationInfo {
183 /// The delegation to stop.
184 pub handle: String,
185 /// The agent, such as `codex`.
186 pub agent: String,
187 /// The SCV session that started it.
188 pub session: String,
189 /// Delegation depth: 1 for an agent SCV started directly.
190 pub depth: u32,
191 /// The agent's process ID (and process group).
192 pub pid: u32,
193 /// The SCV process that started it.
194 pub owner_pid: u32,
195 /// Live processes in its group plus tagged processes outside it.
196 pub processes: u32,
197 /// Its working directory.
198 pub cwd: String,
199 /// When it started.
200 pub started_unix_seconds: u64,
201 /// The owning SCV process is gone; the daemon will stop it.
202 pub orphaned: bool,
203 /// The conversation this run is a turn of, and which turn.
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub conversation: Option<String>,
206 /// Which turn of that conversation.
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub turn: Option<u32>,
209}
210
211/// What a `daemon.control` message asks the daemon to do.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213#[serde(tag = "action", rename_all = "snake_case")]
214pub enum DaemonCommand {
215 /// Report [`DaemonStatus`].
216 Status,
217 /// Reread channel settings and reconcile the components now.
218 Reload,
219 /// Enable or disable one channel account, optionally changing its
220 /// workspace, remote tool grant, and whose messages it answers.
221 ChannelSet {
222 /// The chat channel, such as `wechat` or `feishu`.
223 channel: String,
224 /// The account name within the channel.
225 account: String,
226 /// Whether the account should run.
227 enabled: bool,
228 /// The account's workspace directory; `None` keeps the saved one.
229 workspace: Option<String>,
230 /// Omitted keeps the saved setting.
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 remote_tools: Option<RemoteTools>,
233 /// Whose messages the account answers; omitted keeps the saved
234 /// setting.
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 senders: Option<Senders>,
237 },
238 /// Stop one channel account and remove its credentials and state.
239 ChannelLogout {
240 /// The chat channel, such as `wechat` or `feishu`.
241 channel: String,
242 /// The account name within the channel.
243 account: String,
244 },
245 /// List running delegations; `all` includes orphans awaiting cleanup.
246 Delegations {
247 /// Include orphans awaiting cleanup.
248 #[serde(default)]
249 all: bool,
250 },
251 /// Stop one delegation by handle, or every orphaned one.
252 DelegationKill {
253 /// The delegation to stop.
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 handle: Option<String>,
256 /// Stop every orphaned delegation instead.
257 #[serde(default)]
258 orphans: bool,
259 },
260 /// Restart into the release installed at the daemon's own path once the
261 /// requesting delegation has finished and its report is stored and no
262 /// owner message is being answered, or at `max_wait_seconds` anyway.
263 RestartWhenIdle {
264 /// The release the caller installed; the daemon checks it.
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 version: Option<String>,
267 /// The commit it was built from, for the announcement.
268 #[serde(default, skip_serializing_if = "Option::is_none")]
269 commit: Option<String>,
270 /// The caller's `SCV_PARENT` chain, naming the delegation to wait for.
271 #[serde(default, skip_serializing_if = "Option::is_none")]
272 parent: Option<String>,
273 /// Longest wait before restarting anyway.
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 max_wait_seconds: Option<u64>,
276 },
277 /// Ask the owner a yes/no question in chat: in the chat that started the
278 /// work `parent` names, or else the notify target. The reply's `confirm`
279 /// names the question; `confirm_status` then follows it.
280 ConfirmAsk {
281 /// The question, as the owner reads it.
282 question: String,
283 /// The caller's `SCV_PARENT` chain, naming the delegation whose chat
284 /// is asked.
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 parent: Option<String>,
287 /// How long no answer waits before it counts as no
288 /// ([`DEFAULT_CONFIRM_SECONDS`], at most [`MAX_CONFIRM_SECONDS`]).
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 timeout_seconds: Option<u64>,
291 },
292 /// Report where the question `id` stands. Asking also keeps it alive: a
293 /// question nobody asks about for a minute is withdrawn.
294 ConfirmStatus {
295 /// The ID `confirm_ask` returned.
296 id: String,
297 },
298}