astrid_types/kernel.rs
1//! Kernel management API request and response types.
2
3use astrid_core::PrincipalId;
4use astrid_core::profile::Quotas;
5use serde::{Deserialize, Serialize};
6
7/// The well-known system session UUID string used by the background daemon.
8///
9/// All kernel-internal IPC messages are published with this `source_id`.
10/// WASM capsules that verify message provenance should compare against
11/// this constant. Mirrors `astrid_core::SessionId::SYSTEM`.
12pub const SYSTEM_SESSION_UUID: &str = "00000000-0000-0000-0000-000000000000";
13
14/// Management API requests directed at the core daemon.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "method", content = "params")]
17pub enum KernelRequest {
18 /// Request to install a capsule from a local or remote path.
19 InstallCapsule {
20 /// The path or URL to the `.capsule` archive.
21 source: String,
22 /// True if this should be installed locally in the workspace.
23 workspace: bool,
24 },
25 /// Request to approve a capability grant (usually following an `ApprovalNeeded` response).
26 ApproveCapability {
27 /// The unique ID of the request being approved.
28 request_id: String,
29 /// Cryptographic signature proving Root Identity authorization.
30 signature: String,
31 },
32 /// Request the list of currently loaded capsules.
33 ListCapsules,
34 /// Reload all capsules from the file system.
35 ReloadCapsules,
36 /// Request the list of globally registered slash commands.
37 GetCommands,
38 /// Request metadata about loaded capsules (manifests, providers, interceptors).
39 /// The kernel's equivalent of `/proc` — exposing process table info.
40 GetCapsuleMetadata,
41 /// Request the daemon to shut down gracefully.
42 Shutdown {
43 /// Optional reason for shutdown.
44 reason: Option<String>,
45 },
46 /// Request daemon status information.
47 GetStatus,
48}
49
50/// Management API responses from the core daemon.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(tag = "status", content = "data")]
53pub enum KernelResponse {
54 /// The request succeeded.
55 Success(serde_json::Value),
56 /// A list of available slash commands across all capsules.
57 Commands(Vec<CommandInfo>),
58 /// Metadata about loaded capsules.
59 CapsuleMetadata(Vec<CapsuleMetadataEntry>),
60 /// The request failed.
61 Error(String),
62 /// Daemon status information.
63 Status(DaemonStatus),
64 /// The request requires user capability approval before it can proceed.
65 ApprovalRequired {
66 /// Unique ID for this specific action request.
67 request_id: String,
68 /// Description of what is being requested.
69 description: String,
70 /// The specific capabilities required (e.g. `["host_process", "fs_write"]`).
71 capabilities: Vec<String>,
72 },
73}
74
75/// Daemon runtime status information.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct DaemonStatus {
78 /// Process ID of the daemon.
79 pub pid: u32,
80 /// Daemon uptime in seconds.
81 pub uptime_secs: u64,
82 /// Daemon version string.
83 pub version: String,
84 /// Whether the daemon is running in ephemeral mode.
85 pub ephemeral: bool,
86 /// Number of currently connected clients.
87 pub connected_clients: u32,
88 /// Per-principal breakdown of `connected_clients`. Each entry is
89 /// `(principal, count)`; the sum equals `connected_clients`. Empty
90 /// on daemons that don't yet expose per-principal connection
91 /// attribution (older builds, or when no clients are connected).
92 /// Used by `astrid who` to show who is actually on the daemon
93 /// rather than the bare count.
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub connections_by_principal: Vec<PrincipalConnectionCount>,
96 /// Names of loaded capsules.
97 pub loaded_capsules: Vec<String>,
98}
99
100/// Per-principal connection count entry on [`DaemonStatus`].
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PrincipalConnectionCount {
103 /// The principal (agent) holding the connections.
104 pub principal: String,
105 /// Number of active connections owned by this principal.
106 pub count: u32,
107}
108
109/// Metadata entry for a loaded capsule.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct CapsuleMetadataEntry {
112 /// The capsule's unique name.
113 pub name: String,
114 /// Interceptor event patterns declared by this capsule.
115 pub interceptor_events: Vec<String>,
116}
117
118/// Information about a registered slash command.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CommandInfo {
121 /// The slash command trigger (e.g. `/git`).
122 pub name: String,
123 /// A brief description of what the command does.
124 pub description: String,
125 /// The capsule that provides this command.
126 pub provider_capsule: String,
127}
128
129// ---------------------------------------------------------------------------
130// Admin management API (issue #672 — Layer 6)
131// ---------------------------------------------------------------------------
132
133/// Admin management API request wrapper carrying an optional client
134/// correlation ID and the typed request kind.
135///
136/// `request_id` is echoed back on [`AdminKernelResponse::request_id`] so
137/// clients with multiple in-flight requests on the same response topic
138/// can disambiguate. Single-client deployments may leave it `None`.
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct AdminKernelRequest {
141 /// Optional client-supplied correlation ID. Echoed verbatim on the
142 /// response.
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub request_id: Option<String>,
145 /// The typed request body — `tag = "method", content = "params"`.
146 #[serde(flatten)]
147 pub kind: AdminRequestKind,
148}
149
150impl AdminKernelRequest {
151 /// Build a request with no correlation ID.
152 #[must_use]
153 pub const fn new(kind: AdminRequestKind) -> Self {
154 Self {
155 request_id: None,
156 kind,
157 }
158 }
159
160 /// Build a request with a correlation ID.
161 #[must_use]
162 pub fn with_request_id(request_id: impl Into<String>, kind: AdminRequestKind) -> Self {
163 Self {
164 request_id: Some(request_id.into()),
165 kind,
166 }
167 }
168}
169
170impl From<AdminRequestKind> for AdminKernelRequest {
171 fn from(kind: AdminRequestKind) -> Self {
172 Self::new(kind)
173 }
174}
175
176/// Typed admin request body — flattened into [`AdminKernelRequest`] on
177/// the wire as `{ "method": "...", "params": {...} }`.
178///
179/// Every variant is gated by the Layer 5 capability-enforcement preamble
180/// through a sibling of
181/// [`required_capability`](../../astrid-kernel/src/kernel_router.rs) —
182/// see `required_capability_for_admin_request` for the exact mapping.
183/// Mutating variants are serialized through the kernel's admin write lock
184/// so concurrent callers cannot interleave on `groups.toml` / `profile.toml`.
185#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(tag = "method", content = "params")]
187pub enum AdminRequestKind {
188 /// Create a new agent identity. `name` must pass
189 /// [`PrincipalId::new`](astrid_core::PrincipalId::new). Defaults to
190 /// the built-in `agent` group when `groups` is empty.
191 AgentCreate {
192 /// Human-readable name and principal identifier for the new agent.
193 name: String,
194 /// Group memberships for the new principal; empty → `["agent"]`.
195 #[serde(default)]
196 groups: Vec<String>,
197 /// Per-principal capability grants beyond group inheritance.
198 #[serde(default)]
199 grants: Vec<String>,
200 },
201 /// Delete an existing agent identity. The `default` principal is
202 /// rejected unconditionally. The principal's home directory is NOT
203 /// scrubbed — reclamation is an ops concern.
204 AgentDelete {
205 /// Principal to delete.
206 principal: PrincipalId,
207 },
208 /// Set `enabled = true` on the target principal's profile.
209 AgentEnable {
210 /// Principal to enable.
211 principal: PrincipalId,
212 },
213 /// Set `enabled = false` on the target principal's profile.
214 /// In-flight invocations finish under the old value; new invocations
215 /// are refused.
216 AgentDisable {
217 /// Principal to disable.
218 principal: PrincipalId,
219 },
220 /// List every agent principal with a profile on disk.
221 AgentList,
222 /// Partial-update an existing agent's group memberships. Built-in
223 /// group names (`admin`, `agent`, `restricted`) and custom groups
224 /// loaded from `groups.toml` are both accepted as identifiers;
225 /// validation that the named groups exist happens at the new
226 /// profile's `validate` step. Mutations are idempotent — adding an
227 /// already-present group or removing an absent one is a no-op.
228 AgentModify {
229 /// Principal to modify.
230 principal: PrincipalId,
231 /// Groups to add (idempotent).
232 #[serde(default)]
233 add_groups: Vec<String>,
234 /// Groups to remove (idempotent — missing entries are no-ops).
235 /// Removing the last group leaves the agent in zero groups,
236 /// which the `agent` built-in does NOT auto-restore; operators
237 /// who want a baseline should add `agent` explicitly.
238 #[serde(default)]
239 remove_groups: Vec<String>,
240 },
241 /// Replace the target principal's [`Quotas`] block. Values are
242 /// validated before the atomic profile write.
243 QuotaSet {
244 /// Principal whose quotas are being set.
245 principal: PrincipalId,
246 /// Replacement quota values.
247 quotas: Quotas,
248 },
249 /// Read the target principal's current [`Quotas`] block.
250 QuotaGet {
251 /// Principal whose quotas are being read.
252 principal: PrincipalId,
253 },
254 /// Create a custom group, validated through the same rules the boot
255 /// loader applies to `groups.toml`.
256 GroupCreate {
257 /// Name of the new custom group.
258 name: String,
259 /// Capability patterns conferred by the new group.
260 capabilities: Vec<String>,
261 /// Human-readable description.
262 #[serde(default)]
263 description: Option<String>,
264 /// Required when `capabilities` contains the universal `*` pattern.
265 #[serde(default)]
266 unsafe_admin: bool,
267 },
268 /// Remove a custom group. Built-in groups (`admin`, `agent`,
269 /// `restricted`) are rejected.
270 GroupDelete {
271 /// Name of the group to remove.
272 name: String,
273 },
274 /// Partial-update a custom group. Every provided field replaces the
275 /// corresponding field on the existing group. Built-ins are rejected.
276 GroupModify {
277 /// Name of the group to modify.
278 name: String,
279 /// New capability patterns, if changing.
280 #[serde(default)]
281 capabilities: Option<Vec<String>>,
282 /// New description, if changing. Outer `None` = keep, inner
283 /// `None` = clear.
284 #[serde(default)]
285 description: Option<Option<String>>,
286 /// New `unsafe_admin` flag, if changing.
287 #[serde(default)]
288 unsafe_admin: Option<bool>,
289 },
290 /// List every group (built-in + custom) with its capability set.
291 GroupList,
292 /// Append capability patterns to the principal's `grants` vec. Does
293 /// NOT clear matching revokes — revoke precedence is preserved.
294 CapsGrant {
295 /// Principal receiving the grants.
296 principal: PrincipalId,
297 /// Capability patterns to add.
298 capabilities: Vec<String>,
299 /// Required when `capabilities` contains the universal `*`
300 /// pattern. Mirrors the `unsafe_admin` rail on
301 /// [`Self::GroupCreate`] / [`Self::GroupModify`] so an
302 /// individual grant cannot escalate a principal to universal
303 /// admin without an explicit acknowledgement.
304 #[serde(default)]
305 unsafe_admin: bool,
306 },
307 /// Append capability patterns to the principal's `revokes` vec. Safe
308 /// to call on caps the principal does not currently hold
309 /// (pre-emptive revoke).
310 CapsRevoke {
311 /// Principal losing the capabilities.
312 principal: PrincipalId,
313 /// Capability patterns to revoke.
314 capabilities: Vec<String>,
315 },
316}
317
318/// Admin management API response wrapper carrying the echoed
319/// correlation ID and the typed response body.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct AdminKernelResponse {
322 /// Echoed `request_id` from the [`AdminKernelRequest`] this response
323 /// answers. `None` when the client did not provide one.
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub request_id: Option<String>,
326 /// The typed response body — `tag = "status", content = "data"`.
327 #[serde(flatten)]
328 pub body: AdminResponseBody,
329}
330
331impl AdminKernelResponse {
332 /// Build a response with the given body and no correlation ID.
333 #[must_use]
334 pub const fn new(body: AdminResponseBody) -> Self {
335 Self {
336 request_id: None,
337 body,
338 }
339 }
340
341 /// Build a response that echoes a request's correlation ID.
342 #[must_use]
343 pub fn for_request(request_id: Option<String>, body: AdminResponseBody) -> Self {
344 Self { request_id, body }
345 }
346}
347
348/// Typed admin response body.
349#[derive(Debug, Clone, Serialize, Deserialize)]
350#[serde(tag = "status", content = "data")]
351pub enum AdminResponseBody {
352 /// Generic success payload — used by mutating variants where the
353 /// interesting result is "the write landed."
354 Success(serde_json::Value),
355 /// Response for [`AdminRequestKind::AgentList`].
356 AgentList(Vec<AgentSummary>),
357 /// Response for [`AdminRequestKind::GroupList`].
358 GroupList(Vec<GroupSummary>),
359 /// Response for [`AdminRequestKind::QuotaGet`].
360 Quotas(Quotas),
361 /// The request failed.
362 Error(String),
363}
364
365/// Summary of an agent principal returned by
366/// [`AdminKernelRequest::AgentList`].
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368pub struct AgentSummary {
369 /// The principal identifier.
370 pub principal: PrincipalId,
371 /// Whether the principal is currently enabled (master switch).
372 pub enabled: bool,
373 /// Group memberships as written to `profile.toml`.
374 pub groups: Vec<String>,
375 /// Direct capability grants beyond group inheritance.
376 pub grants: Vec<String>,
377 /// Explicit revokes (highest-precedence deny).
378 pub revokes: Vec<String>,
379}
380
381/// Summary of a group returned by [`AdminKernelRequest::GroupList`].
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383pub struct GroupSummary {
384 /// Group name.
385 pub name: String,
386 /// Capability patterns conferred by this group.
387 pub capabilities: Vec<String>,
388 /// Human-readable description.
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub description: Option<String>,
391 /// Whether the group opted in to granting the universal `*`.
392 pub unsafe_admin: bool,
393 /// `true` for built-in groups (`admin`, `agent`, `restricted`).
394 /// Clients should treat built-ins as read-only.
395 pub builtin: bool,
396}