Skip to main content

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}