astrid_core/kernel_api.rs
1//! Kernel management API request and response types.
2//!
3//! These types describe the CLI ↔ daemon RPC surface (admin requests,
4//! status queries, capsule lifecycle ops). They live in `astrid-core`
5//! because they reference `PrincipalId` and `Quotas` from this crate.
6//!
7//! Capsule-facing IPC types live in `astrid-types` (which intentionally
8//! has no dependency on `astrid-core` — it must compile on
9//! `wasm32-unknown-unknown` without dragging in the kernel).
10
11use crate::PrincipalId;
12use crate::profile::Quotas;
13use serde::{Deserialize, Serialize};
14
15/// The well-known system session UUID string used by the background daemon.
16///
17/// All kernel-internal IPC messages are published with this `source_id`.
18/// WASM capsules that verify message provenance should compare against
19/// this constant. Mirrors `astrid_core::SessionId::SYSTEM`.
20pub const SYSTEM_SESSION_UUID: &str = "00000000-0000-0000-0000-000000000000";
21
22/// Management API requests directed at the core daemon.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(tag = "method", content = "params")]
25pub enum KernelRequest {
26 /// Request to install a capsule from a local or remote path.
27 InstallCapsule {
28 /// The path or URL to the `.capsule` archive.
29 source: String,
30 /// True if this should be installed locally in the workspace.
31 workspace: bool,
32 },
33 /// Request to approve a capability grant (usually following an `ApprovalNeeded` response).
34 ApproveCapability {
35 /// The unique ID of the request being approved.
36 request_id: String,
37 /// Cryptographic signature proving Root Identity authorization.
38 signature: String,
39 },
40 /// Request the list of currently loaded capsules.
41 ListCapsules,
42 /// Reload all capsules from the file system.
43 ReloadCapsules,
44 /// Request the list of globally registered slash commands.
45 GetCommands,
46 /// Request metadata about loaded capsules (manifests, providers, interceptors).
47 /// The kernel's equivalent of `/proc` — exposing process table info.
48 GetCapsuleMetadata,
49 /// Request the daemon to shut down gracefully.
50 Shutdown {
51 /// Optional reason for shutdown.
52 reason: Option<String>,
53 },
54 /// Request daemon status information.
55 GetStatus,
56}
57
58/// Management API responses from the core daemon.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(tag = "status", content = "data")]
61pub enum KernelResponse {
62 /// The request succeeded.
63 Success(serde_json::Value),
64 /// A list of available slash commands across all capsules.
65 Commands(Vec<CommandInfo>),
66 /// Metadata about loaded capsules.
67 CapsuleMetadata(Vec<CapsuleMetadataEntry>),
68 /// The request failed.
69 Error(String),
70 /// Daemon status information.
71 Status(DaemonStatus),
72 /// The request requires user capability approval before it can proceed.
73 ApprovalRequired {
74 /// Unique ID for this specific action request.
75 request_id: String,
76 /// Description of what is being requested.
77 description: String,
78 /// The specific capabilities required (e.g. `["host_process", "fs_write"]`).
79 capabilities: Vec<String>,
80 },
81}
82
83/// Daemon runtime status information.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct DaemonStatus {
86 /// Process ID of the daemon.
87 pub pid: u32,
88 /// Daemon uptime in seconds.
89 pub uptime_secs: u64,
90 /// Daemon version string.
91 pub version: String,
92 /// Whether the daemon is running in ephemeral mode.
93 pub ephemeral: bool,
94 /// Number of currently connected clients.
95 pub connected_clients: u32,
96 /// Per-principal breakdown of `connected_clients`. Each entry is
97 /// `(principal, count)`; the sum equals `connected_clients`. Empty
98 /// on daemons that don't yet expose per-principal connection
99 /// attribution (older builds, or when no clients are connected).
100 /// Used by `astrid who` to show who is actually on the daemon
101 /// rather than the bare count.
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub connections_by_principal: Vec<PrincipalConnectionCount>,
104 /// Names of loaded capsules.
105 pub loaded_capsules: Vec<String>,
106}
107
108/// Per-principal connection count entry on [`DaemonStatus`].
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct PrincipalConnectionCount {
111 /// The principal (agent) holding the connections.
112 pub principal: String,
113 /// Number of active connections owned by this principal.
114 pub count: u32,
115}
116
117/// Metadata entry for a loaded capsule.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CapsuleMetadataEntry {
120 /// The capsule's unique name.
121 pub name: String,
122 /// Interceptor event patterns declared by this capsule.
123 pub interceptor_events: Vec<String>,
124}
125
126/// Information about a registered slash command.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CommandInfo {
129 /// The slash command trigger (e.g. `/git`).
130 pub name: String,
131 /// A brief description of what the command does.
132 pub description: String,
133 /// The capsule that provides this command.
134 pub provider_capsule: String,
135}
136
137// ---------------------------------------------------------------------------
138// Admin management API (issue #672 — Layer 6)
139// ---------------------------------------------------------------------------
140
141/// Admin management API request wrapper carrying an optional client
142/// correlation ID and the typed request kind.
143///
144/// `request_id` is echoed back on [`AdminKernelResponse::request_id`] so
145/// clients with multiple in-flight requests on the same response topic
146/// can disambiguate. Single-client deployments may leave it `None`.
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct AdminKernelRequest {
149 /// Optional client-supplied correlation ID. Echoed verbatim on the
150 /// response.
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub request_id: Option<String>,
153 /// The typed request body — `tag = "method", content = "params"`.
154 #[serde(flatten)]
155 pub kind: AdminRequestKind,
156}
157
158impl AdminKernelRequest {
159 /// Build a request with no correlation ID.
160 #[must_use]
161 pub const fn new(kind: AdminRequestKind) -> Self {
162 Self {
163 request_id: None,
164 kind,
165 }
166 }
167
168 /// Build a request with a correlation ID.
169 #[must_use]
170 pub fn with_request_id(request_id: impl Into<String>, kind: AdminRequestKind) -> Self {
171 Self {
172 request_id: Some(request_id.into()),
173 kind,
174 }
175 }
176}
177
178impl From<AdminRequestKind> for AdminKernelRequest {
179 fn from(kind: AdminRequestKind) -> Self {
180 Self::new(kind)
181 }
182}
183
184/// Typed admin request body — flattened into [`AdminKernelRequest`] on
185/// the wire as `{ "method": "...", "params": {...} }`.
186///
187/// Every variant is gated by the Layer 5 capability-enforcement preamble
188/// through a sibling of
189/// [`required_capability`](../../astrid-kernel/src/kernel_router.rs) —
190/// see `required_capability_for_admin_request` for the exact mapping.
191/// Mutating variants are serialized through the kernel's admin write lock
192/// so concurrent callers cannot interleave on `groups.toml` / `profile.toml`.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(tag = "method", content = "params")]
195pub enum AdminRequestKind {
196 /// Create a new agent identity. `name` must pass
197 /// [`PrincipalId::new`](astrid_core::PrincipalId::new). Defaults to
198 /// the built-in `agent` group when `groups` is empty.
199 AgentCreate {
200 /// Human-readable name and principal identifier for the new agent.
201 name: String,
202 /// Group memberships for the new principal; empty → `["agent"]`.
203 #[serde(default)]
204 groups: Vec<String>,
205 /// Per-principal capability grants beyond group inheritance.
206 #[serde(default)]
207 grants: Vec<String>,
208 },
209 /// Delete an existing agent identity. The `default` principal is
210 /// rejected unconditionally. The principal's home directory is NOT
211 /// scrubbed — reclamation is an ops concern.
212 AgentDelete {
213 /// Principal to delete.
214 principal: PrincipalId,
215 },
216 /// Set `enabled = true` on the target principal's profile.
217 AgentEnable {
218 /// Principal to enable.
219 principal: PrincipalId,
220 },
221 /// Set `enabled = false` on the target principal's profile.
222 /// In-flight invocations finish under the old value; new invocations
223 /// are refused.
224 AgentDisable {
225 /// Principal to disable.
226 principal: PrincipalId,
227 },
228 /// List every agent principal with a profile on disk.
229 AgentList,
230 /// Partial-update an existing agent's group memberships. Built-in
231 /// group names (`admin`, `agent`, `restricted`) and custom groups
232 /// loaded from `groups.toml` are both accepted as identifiers;
233 /// validation that the named groups exist happens at the new
234 /// profile's `validate` step. Mutations are idempotent — adding an
235 /// already-present group or removing an absent one is a no-op.
236 AgentModify {
237 /// Principal to modify.
238 principal: PrincipalId,
239 /// Groups to add (idempotent).
240 #[serde(default)]
241 add_groups: Vec<String>,
242 /// Groups to remove (idempotent — missing entries are no-ops).
243 /// Removing the last group leaves the agent in zero groups,
244 /// which the `agent` built-in does NOT auto-restore; operators
245 /// who want a baseline should add `agent` explicitly.
246 #[serde(default)]
247 remove_groups: Vec<String>,
248 },
249 /// Replace the target principal's [`Quotas`] block. Values are
250 /// validated before the atomic profile write.
251 QuotaSet {
252 /// Principal whose quotas are being set.
253 principal: PrincipalId,
254 /// Replacement quota values.
255 quotas: Quotas,
256 },
257 /// Read the target principal's current [`Quotas`] block.
258 QuotaGet {
259 /// Principal whose quotas are being read.
260 principal: PrincipalId,
261 },
262 /// Read the target principal's current resource **usage** vs budget —
263 /// the cross-capsule CPU total plus the configured ceilings. Read-only,
264 /// scoped exactly like [`QuotaGet`](Self::QuotaGet) (`self:quota:get` /
265 /// `quota:get`): a principal can read its own usage, an admin can read
266 /// anyone's.
267 UsageGet {
268 /// Principal whose usage is being read.
269 principal: PrincipalId,
270 },
271 /// Create a custom group, validated through the same rules the boot
272 /// loader applies to `groups.toml`.
273 GroupCreate {
274 /// Name of the new custom group.
275 name: String,
276 /// Capability patterns conferred by the new group.
277 capabilities: Vec<String>,
278 /// Human-readable description.
279 #[serde(default)]
280 description: Option<String>,
281 /// Required when `capabilities` contains the universal `*` pattern.
282 #[serde(default)]
283 unsafe_admin: bool,
284 },
285 /// Remove a custom group. Built-in groups (`admin`, `agent`,
286 /// `restricted`) are rejected.
287 GroupDelete {
288 /// Name of the group to remove.
289 name: String,
290 },
291 /// Partial-update a custom group. Every provided field replaces the
292 /// corresponding field on the existing group. Built-ins are rejected.
293 GroupModify {
294 /// Name of the group to modify.
295 name: String,
296 /// New capability patterns, if changing.
297 #[serde(default)]
298 capabilities: Option<Vec<String>>,
299 /// New description, if changing. Outer `None` = keep, inner
300 /// `None` = clear.
301 #[serde(default)]
302 description: Option<Option<String>>,
303 /// New `unsafe_admin` flag, if changing.
304 #[serde(default)]
305 unsafe_admin: Option<bool>,
306 },
307 /// List every group (built-in + custom) with its capability set.
308 GroupList,
309 /// Append capability patterns to the principal's `grants` vec. Does
310 /// NOT clear matching revokes — revoke precedence is preserved.
311 CapsGrant {
312 /// Principal receiving the grants.
313 principal: PrincipalId,
314 /// Capability patterns to add.
315 capabilities: Vec<String>,
316 /// Required when `capabilities` contains the universal `*`
317 /// pattern. Mirrors the `unsafe_admin` rail on
318 /// [`Self::GroupCreate`] / [`Self::GroupModify`] so an
319 /// individual grant cannot escalate a principal to universal
320 /// admin without an explicit acknowledgement.
321 #[serde(default)]
322 unsafe_admin: bool,
323 },
324 /// Append capability patterns to the principal's `revokes` vec. Safe
325 /// to call on caps the principal does not currently hold
326 /// (pre-emptive revoke).
327 CapsRevoke {
328 /// Principal losing the capabilities.
329 principal: PrincipalId,
330 /// Capability patterns to revoke.
331 capabilities: Vec<String>,
332 },
333 /// Issue a new invite token. Capability-gated by `invite:issue`.
334 /// The kernel persists the token under `etc/invites.toml` with
335 /// expiry + remaining use count, and the caller publishes the
336 /// returned redeem URL out-of-band.
337 InviteIssue {
338 /// Group new redeemers join. Must already exist (built-in or
339 /// custom) — validated against the live `GroupConfig`.
340 group: String,
341 /// Seconds until the token expires. `None` = no expiry (the
342 /// max-uses counter is the only stop). Capped server-side to
343 /// 30 days to bound forever-tokens.
344 #[serde(default, skip_serializing_if = "Option::is_none")]
345 expires_secs: Option<u64>,
346 /// Maximum number of successful redemptions before the token is
347 /// invalidated. Zero is rejected (issuing a dead token serves
348 /// no purpose).
349 max_uses: u32,
350 /// Free-form short label (e.g. "alice's tablet") attached to
351 /// the persisted record. Surfaced by `InviteList`.
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 metadata: Option<String>,
354 },
355 /// Redeem an invite token. The token IS the auth: the kernel-side
356 /// dispatcher special-cases this variant to skip the capability
357 /// preamble (the caller principal does not yet exist), and the
358 /// handler verifies the token, mints a fresh principal via the
359 /// existing `AgentCreate` machinery, registers the supplied
360 /// ed25519 public key on the new principal's profile, and decrements
361 /// the token's use counter (deleting the record on the last use).
362 InviteRedeem {
363 /// Opaque token bytes (URL-safe base64) returned from a prior
364 /// `InviteIssue`.
365 token: String,
366 /// Hex-encoded ed25519 public key (32 bytes / 64 hex chars).
367 /// Registered on the new principal's `AuthConfig.public_keys`.
368 public_key: String,
369 /// Optional human-friendly name attached to the minted principal.
370 /// When `Some(s)`, the kernel generates the underlying
371 /// `PrincipalId` from `s` (slugified, collision-checked); when
372 /// `None`, a random `agent-<8-hex>` id is allocated.
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 display_name: Option<String>,
375 },
376 /// List outstanding invite tokens. Gated by `invite:list`.
377 InviteList,
378 /// Revoke an outstanding invite token without consuming it.
379 /// Gated by `invite:revoke`.
380 InviteRevoke {
381 /// The opaque token to invalidate.
382 token: String,
383 },
384 /// Issue a pair-device token. Gated by `self:auth:pair` (the
385 /// caller can only mint pair-tokens for their own principal —
386 /// the kernel ignores any target field on the wire and ties the
387 /// token to the caller). Used to add a new device's ed25519
388 /// public key to an existing principal's `AuthConfig.public_keys`
389 /// without minting a separate principal.
390 PairDeviceIssue {
391 /// Seconds until the token expires. Capped server-side to
392 /// 1 hour — pair-tokens are intended for immediate use on a
393 /// neighbouring device, not for long-lived sharing.
394 #[serde(default, skip_serializing_if = "Option::is_none")]
395 expires_secs: Option<u64>,
396 /// Free-form short label (e.g. "alice's phone") persisted
397 /// alongside the new public key on
398 /// `AuthConfig.public_keys` once the token is redeemed.
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 label: Option<String>,
401 },
402 /// Redeem a pair-device token. Like `InviteRedeem`, the kernel
403 /// dispatcher special-cases this to bypass the capability
404 /// preamble — the token IS the auth. The handler verifies the
405 /// token, appends the supplied public key to the issuing
406 /// principal's `AuthConfig.public_keys`, and decrements / deletes
407 /// the token record.
408 PairDeviceRedeem {
409 /// The opaque token from a prior `PairDeviceIssue`.
410 token: String,
411 /// Hex-encoded ed25519 public key (32 bytes / 64 hex chars).
412 public_key: String,
413 },
414}
415
416/// Admin management API response wrapper carrying the echoed
417/// correlation ID and the typed response body.
418#[derive(Debug, Clone, Serialize, Deserialize)]
419pub struct AdminKernelResponse {
420 /// Echoed `request_id` from the [`AdminKernelRequest`] this response
421 /// answers. `None` when the client did not provide one.
422 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub request_id: Option<String>,
424 /// The typed response body — `tag = "status", content = "data"`.
425 #[serde(flatten)]
426 pub body: AdminResponseBody,
427}
428
429impl AdminKernelResponse {
430 /// Build a response with the given body and no correlation ID.
431 #[must_use]
432 pub const fn new(body: AdminResponseBody) -> Self {
433 Self {
434 request_id: None,
435 body,
436 }
437 }
438
439 /// Build a response that echoes a request's correlation ID.
440 #[must_use]
441 pub fn for_request(request_id: Option<String>, body: AdminResponseBody) -> Self {
442 Self { request_id, body }
443 }
444}
445
446/// Typed admin response body.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(tag = "status", content = "data")]
449pub enum AdminResponseBody {
450 /// Generic success payload — used by mutating variants where the
451 /// interesting result is "the write landed."
452 Success(serde_json::Value),
453 /// Response for [`AdminRequestKind::AgentList`].
454 AgentList(Vec<AgentSummary>),
455 /// Response for [`AdminRequestKind::GroupList`].
456 GroupList(Vec<GroupSummary>),
457 /// Response for [`AdminRequestKind::QuotaGet`].
458 Quotas(Quotas),
459 /// Response for [`AdminRequestKind::UsageGet`].
460 Usage(ResourceUsage),
461 /// Response for [`AdminRequestKind::InviteIssue`] — the freshly
462 /// minted token plus its persisted metadata. The redemption URL is
463 /// derived client-side from the deployment's public gateway base
464 /// URL; the kernel never knows where the gateway is reachable.
465 Invite(InviteIssued),
466 /// Response for [`AdminRequestKind::InviteRedeem`] — the new
467 /// principal id (so the redeemer can locally pin the binding) and
468 /// the assigned group. The redeemer also gets back the issuing
469 /// public-key fingerprint so out-of-band verification of the
470 /// minted principal becomes possible.
471 InviteRedeemed(InviteRedeemed),
472 /// Response for [`AdminRequestKind::InviteList`].
473 InviteList(Vec<InviteSummary>),
474 /// Response for [`AdminRequestKind::PairDeviceIssue`].
475 PairToken(PairTokenIssued),
476 /// Response for [`AdminRequestKind::PairDeviceRedeem`].
477 PairTokenRedeemed(PairTokenRedeemed),
478 /// The request failed.
479 Error(String),
480}
481
482/// Per-principal resource usage vs configured budget — the payload of
483/// [`AdminRequestKind::UsageGet`], rendered by `astrid quota`/`astrid top` and
484/// `GET /api/sys/principals/{id}/usage` so per-principal usage is measurable.
485///
486/// **CPU** is the live cross-capsule aggregate: the kernel's shared fuel ledger
487/// sums every interceptor's exact wasmtime-fuel cost per invoking principal
488/// across all capsules. **Memory** is reported as a per-principal *peak*
489/// (`memory_bytes_peak_total`): the kernel's shared memory ledger records the
490/// high-water linear-memory size each invoking principal grows a Store to,
491/// max'd across all capsules. A live cross-capsule *current* total
492/// (`memory_bytes_current_total`) is not implemented — under pooled, shared
493/// Stores it is not cleanly attributable — so it stays `None`; the limit field
494/// reports the per-instance ceiling.
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct ResourceUsage {
497 /// Principal this usage report describes.
498 pub principal: PrincipalId,
499 /// Cumulative interceptor CPU burned across ALL capsules, in wasmtime fuel
500 /// units (exact deterministic instruction count, monotonic for the process
501 /// lifetime).
502 pub cpu_fuel_consumed_total: u64,
503 /// Configured CPU rate ceiling ([`Quotas::max_cpu_fuel_per_sec`]), always
504 /// `> 0` (validation rejects `0` — there is no "unlimited" sentinel;
505 /// unbounded CPU is a capability, surfaced by `exempt`).
506 pub cpu_fuel_per_sec_limit: u64,
507 /// Whether the principal is exempt from resource budgets — it holds
508 /// `system:resources:unbounded`, `net_bind`, or `uplink` (admins via `*`).
509 /// When `true` the limit fields are advisory, never enforced.
510 pub exempt: bool,
511 /// Per-capsule-instance memory ceiling ([`Quotas::max_memory_bytes`]). This
512 /// is a per-Store cap, not a cross-capsule total.
513 pub memory_bytes_limit_per_instance: u64,
514 /// Current cross-capsule resident memory total, or `None` — a live
515 /// "current" total is not cleanly attributable under pooled, shared Stores,
516 /// so the peak (below) is the reported memory signal instead.
517 pub memory_bytes_current_total: Option<u64>,
518 /// Peak cross-capsule linear-memory high-water mark this principal has
519 /// driven, in bytes, max'd across every capsule it invokes (from the shared
520 /// memory ledger). `None` while no peak has been recorded — including
521 /// single-tenant deployments before any guest grows memory. The principal
522 /// that *grows* a Store owns the peak; one reusing an already-grown Store
523 /// without growing is not charged.
524 pub memory_bytes_peak_total: Option<u64>,
525}
526
527/// Summary of an agent principal returned by
528/// [`AdminKernelRequest::AgentList`].
529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
530pub struct AgentSummary {
531 /// The principal identifier.
532 pub principal: PrincipalId,
533 /// Whether the principal is currently enabled (master switch).
534 pub enabled: bool,
535 /// Group memberships as written to `profile.toml`.
536 pub groups: Vec<String>,
537 /// Direct capability grants beyond group inheritance.
538 pub grants: Vec<String>,
539 /// Explicit revokes (highest-precedence deny).
540 pub revokes: Vec<String>,
541}
542
543/// Response payload for [`AdminRequestKind::InviteIssue`].
544#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545pub struct InviteIssued {
546 /// Opaque token (URL-safe base64). The caller delivers this to the
547 /// redeemer out-of-band — e.g. printed by the CLI, surfaced by the
548 /// gateway as a redeem URL fragment, or pasted into a chat.
549 pub token: String,
550 /// Group the redeemer will join on success.
551 pub group: String,
552 /// Number of remaining redemptions before the token is invalidated.
553 pub remaining_uses: u32,
554 /// Wall-clock Unix-epoch timestamp at which the token expires.
555 /// `None` when the issuer requested no expiry.
556 #[serde(default, skip_serializing_if = "Option::is_none")]
557 pub expires_at_epoch: Option<u64>,
558 /// Operator-supplied label (`metadata` from the issue request).
559 #[serde(default, skip_serializing_if = "Option::is_none")]
560 pub metadata: Option<String>,
561}
562
563/// Response payload for [`AdminRequestKind::InviteRedeem`].
564#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
565pub struct InviteRedeemed {
566 /// The freshly minted principal id. The redeemer pins this locally
567 /// alongside its keypair so subsequent gateway sessions can verify
568 /// the binding.
569 pub principal: PrincipalId,
570 /// Group the new principal is now a member of.
571 pub group: String,
572 /// SHA-256 fingerprint (hex) of the registered ed25519 public key.
573 /// Lets the redeemer verify that the kernel registered the key it
574 /// sent rather than substituting one of its own.
575 pub public_key_fingerprint: String,
576}
577
578/// Response payload for [`AdminRequestKind::PairDeviceIssue`].
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580pub struct PairTokenIssued {
581 /// Opaque token. The issuing device hands this to the new
582 /// device out-of-band (QR code, NFC, manual copy).
583 pub token: String,
584 /// Principal the new device's key will attach to (always the
585 /// caller, never request-body derived).
586 pub principal: PrincipalId,
587 /// Wall-clock Unix-epoch timestamp at which the token expires.
588 pub expires_at_epoch: u64,
589 /// Operator-supplied label (echoed; not yet bound).
590 #[serde(default, skip_serializing_if = "Option::is_none")]
591 pub label: Option<String>,
592}
593
594/// Response payload for [`AdminRequestKind::PairDeviceRedeem`].
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596pub struct PairTokenRedeemed {
597 /// The principal the new device is now bound to.
598 pub principal: PrincipalId,
599 /// SHA-256 fingerprint (hex) of the registered ed25519 key.
600 /// Lets the redeemer verify the kernel registered the key it
601 /// sent rather than substituting one of its own.
602 pub public_key_fingerprint: String,
603}
604
605/// Summary of an outstanding invite returned by
606/// [`AdminRequestKind::InviteList`].
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608pub struct InviteSummary {
609 /// SHA-256 fingerprint (hex) of the token — the kernel does not
610 /// leak the raw token through list responses. Issuers retain the
611 /// raw value from the original [`InviteIssued`] response.
612 pub token_fingerprint: String,
613 /// Group the redeemer will join.
614 pub group: String,
615 /// Remaining redemptions.
616 pub remaining_uses: u32,
617 /// Wall-clock Unix-epoch timestamp at which the token expires.
618 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub expires_at_epoch: Option<u64>,
620 /// Wall-clock Unix-epoch timestamp at which the token was issued.
621 pub issued_at_epoch: u64,
622 /// Operator-supplied label.
623 #[serde(default, skip_serializing_if = "Option::is_none")]
624 pub metadata: Option<String>,
625}
626
627/// Summary of a group returned by [`AdminKernelRequest::GroupList`].
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629pub struct GroupSummary {
630 /// Group name.
631 pub name: String,
632 /// Capability patterns conferred by this group.
633 pub capabilities: Vec<String>,
634 /// Human-readable description.
635 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub description: Option<String>,
637 /// Whether the group opted in to granting the universal `*`.
638 pub unsafe_admin: bool,
639 /// `true` for built-in groups (`admin`, `agent`, `restricted`).
640 /// Clients should treat built-ins as read-only.
641 pub builtin: bool,
642}