From cccba9fd0c3fd7575d86041b1173c4eef471192d Mon Sep 17 00:00:00 2001 From: "Joshua J. Bouw" Date: Sun, 7 Jun 2026 03:34:31 +0400 Subject: [PATCH] feat(sdk): add capabilities::enumerate for sys enumerate-capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps astrid:sys/host.enumerate-capabilities (unicity-astrid/wit#13) as the infallible list dual of capabilities::check: returns the calling capsule's own held capability names — the categories declared in its [capabilities] manifest block (host_process, net_connect, fs_read, …), not the scoped arguments within them. Argument-free, returns Vec with no error path (an empty list is the valid no-capabilities answer). Contracts submodule bumped to pick up the new sys function. --- CHANGELOG.md | 1 + astrid-sdk/src/lib.rs | 28 +++++++++++++--- .../wit-staging/deps/astrid-sys/sys@1.0.0.wit | 33 +++++++++++++++++-- contracts | 2 +- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f46e2..da3f29f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked. ### Added +- **`capabilities::enumerate` — list the calling capsule's own held capability names.** The list dual of `capabilities::check`: returns the capability categories declared in this capsule's `[capabilities]` manifest block (`host_process`, `net_connect`, `fs_read`, …) — the names, not the scoped arguments within them (allowlists, `host:port`, paths). Argument-free (the kernel already knows the caller) and infallible — an empty list is the valid "no capabilities" answer — so a reusable capsule can ground its behaviour in what it can actually do instead of hard-coding it, avoiding code-vs-manifest drift. Backed by unicity-astrid/wit#13's `astrid:sys/host.enumerate-capabilities`; contracts submodule bumped accordingly. - **`process` persistent-process tier — `Command::spawn_persistent`, `PersistentProcess`, and `process::{attach, list, status_many}`.** Mirrors the host `astrid:process@1.0.0` persistent tier: a background child that **outlives the pooled, stateless instance** that started it (unlike `Process`, whose kernel resource is reaped on instance reset). `Command` gains the persistent-only builder knobs — `label`, `keep_stdin_open`, `overflow`, `log_ring_bytes`, `max_lifetime`, `idle_timeout`, `exit_retention`, `limits` — and a `spawn_persistent()` terminal returning a `PersistentProcess` keyed by an opaque `ProcessId`. `PersistentProcess` exposes `status` / `read_logs` (drain) / `read_since` (non-draining cursor, byte-faithful `LogChunk`) / `write_stdin` / `close_stdin` / `signal` / `wait` (bounded) / `stop` (consumes the handle; SIGTERM→grace→SIGKILL, frees the slot) / `release` (consumes). Persist the `ProcessId` (e.g. in KV) and `process::attach(id)` from a later invocation to reattach — the SDK `attach` is a thin id-wrapper, so it works without the host's deferred `attach` resource fn; the first id-keyed call validates ownership. `process::list` / `status_many` enumerate the capsule+principal's persistent processes. New types: `ProcessId`, `ProcessInfo`, `ProcessPhase`, `LogStream`, `LogCursor`, `LogChunk`, `OverflowPolicy`, `ResourceLimits`; `Signal` gains `Stop` / `Cont`. The host's `watch` / `unwatch` lifecycle-event channel and resource-limit enforcement are not yet wired (poll via `status` + bounded `wait`). Backed by unicity-astrid/wit#12; contracts submodule bumped to the merged `astrid:process@1.0.0` persistent tier. ## [0.7.0] - 2026-05-26 diff --git a/astrid-sdk/src/lib.rs b/astrid-sdk/src/lib.rs index d02f5f6..c57b8dc 100644 --- a/astrid-sdk/src/lib.rs +++ b/astrid-sdk/src/lib.rs @@ -467,11 +467,14 @@ pub mod runtime { } } -/// Cross-capsule capability queries. +/// Capability introspection. /// -/// Allows a capsule to check whether another capsule (identified by its -/// IPC session UUID) has a specific manifest capability. Used by the -/// prompt builder to enforce `allow_prompt_injection` gating. +/// [`check`] asks whether a capsule (self or any other, by IPC session UUID) +/// holds a specific manifest capability — used by the prompt builder to +/// enforce `allow_prompt_injection` gating. [`enumerate`] is the list dual for +/// the calling capsule's own set: the names for which a self-`check` returns +/// `true`. Capability posture is structural metadata, not a secret +/// (enforce-don't-conceal), so both are ungated. pub mod capabilities { use super::*; @@ -488,6 +491,23 @@ pub mod capabilities { let response = wit_sys::check_capsule_capability(&request).map_err(host_err)?; Ok(response.allowed) } + + /// Enumerate the calling capsule's own held capability names. + /// + /// Returns the capability categories declared in this capsule's + /// `[capabilities]` manifest block (`host_process`, `net_connect`, + /// `fs_read`, …) — the names, not the scoped arguments within them + /// (allowlists, `host:port`, paths). This is exactly the set of names for + /// which [`check`] against this capsule's own UUID returns `true`. + /// + /// Argument-free (the kernel already knows the caller) and infallible: the + /// kernel always knows the caller's own registered set, so there is no + /// error path — an empty list is the valid "no capabilities" answer. Lets + /// a reusable capsule ground its behaviour in what it can actually do + /// instead of hard-coding it, avoiding code-vs-manifest drift. + pub fn enumerate() -> Vec { + wit_sys::enumerate_capabilities() + } } pub mod net; diff --git a/astrid-sys/wit-staging/deps/astrid-sys/sys@1.0.0.wit b/astrid-sys/wit-staging/deps/astrid-sys/sys@1.0.0.wit index af39e06..138653e 100644 --- a/astrid-sys/wit-staging/deps/astrid-sys/sys@1.0.0.wit +++ b/astrid-sys/wit-staging/deps/astrid-sys/sys@1.0.0.wit @@ -133,10 +133,39 @@ interface host { /// Audit: not recorded (read-only, no side effects). random-bytes: func(length: u64) -> result, error-code>; - /// Check whether a capsule has a specific manifest capability. + /// Check whether a capsule has a specific manifest capability — self or + /// any other capsule (by UUID). The per-name dual of + /// `enumerate-capabilities`. /// /// Fail-closed: returns `allowed: false` for unknown UUIDs and /// unknown capabilities. Returns `registry-unavailable` when the - /// registry itself can't be consulted. + /// registry itself can't be consulted. Ungated: capability posture is + /// structural metadata, not a secret (enforce-don't-conceal) — knowing a + /// capability conveys no ability to use it. Audit: not recorded. check-capsule-capability: func(request: capability-check-request) -> result; + + /// Enumerate the CALLING capsule's OWN held capability NAMES — the + /// categories the kernel enforces at host-call time (e.g. `host_process`, + /// `net_connect`, `fs_read`), NOT the scoped arguments within them (a + /// `host_process` allowlist, `host:port`, paths). It is the list dual of + /// `check-capsule-capability`: the set of names for which a self-`check` + /// returns true. + /// + /// Argument-free — the kernel already knows the caller. The set is the + /// capsule's manifest-declared capabilities as the kernel registered them; + /// capsule capabilities are not revoked at runtime (the grant/revoke model + /// is principal-scoped, a separate axis), so "what I declared" and "what I + /// can do" coincide at the capsule level. + /// + /// Ungated, like `check-capsule-capability`: a capsule grounds its + /// behaviour in what it can actually do — a reusable supervisor binary + /// deployed under different manifests reads its real set instead of + /// hard-coding it; any capsule avoids code-vs-manifest drift by grounding + /// on the registered set. Audit: not recorded (read-only self- + /// introspection). See RFC: host_abi ("Capability introspection"). + /// + /// Infallible: the kernel always knows the caller's own registered set, so + /// there is no failure mode (an empty list is the valid "no capabilities" + /// answer) — like `clock-ms` / `clock-monotonic-ns`, no `result` wrapper. + enumerate-capabilities: func() -> list; } diff --git a/contracts b/contracts index 1a06cf4..9742f80 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 1a06cf454d481a2bfdbad0098b2aa2f5f2bac4f2 +Subproject commit 9742f80e83abb92e9ae954ef82664bdd2fcb89cd