diff --git a/CHANGELOG.md b/CHANGELOG.md index 72bb9a5..9b2e95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ in this file. The format follows [Keep a Changelog](https://keepachangelog.com/e ### Added +- **`capabilities.enumerate` — list the calling capsule's own held capability names.** Mirrors the Rust SDK's `capabilities::enumerate`. 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 array 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 (the `astrid:contracts` events bundle is unchanged). - **`process` persistent-process tier — `spawnPersistent`, `PersistentProcess`, and `process.{attach, listProcesses, statusMany}`.** Mirrors the Rust SDK and the host `astrid:process@1.0.0` persistent tier: a background child that **outlives the pooled, stateless instance** that started it (unlike `BackgroundProcessHandle`, whose kernel resource is reaped on instance reset). `spawnPersistent(cmd, args, options)` takes the persistent knobs (`label`, `keepStdinOpen`, `overflow`, `logRingBytes`, `maxLifetimeMs`, `idleTimeoutMs`, `exitRetentionMs`, `limits`) and returns a `PersistentProcess` keyed by an opaque id. `PersistentProcess` exposes `status` / `readLogs` (drain) / `readSince` (non-draining cursor → byte-faithful `LogChunkResult`; start with `logCursorStart()`) / `writeStdin` / `closeStdin` / `signal` / `wait` (bounded) / `stop` (SIGTERM→grace→SIGKILL, frees the slot) / `release`. Persist `proc.id` (e.g. in KV) and `process.attach(id)` from a later invocation to reattach — `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.listProcesses` / `statusMany` enumerate the capsule+principal's persistent processes. New types: `PersistentProcessInfo`, `SpawnPersistentOptions`, `LogChunkResult`, `ResourceLimits`, `ProcessPhase`, `LogStream`, `LogCursor`, `OverflowPolicy`; `ProcessSignal` 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 (the `astrid:contracts` events bundle is unchanged). ## [0.1.0] - 2026-05-26 diff --git a/contracts b/contracts index 1a06cf4..9742f80 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 1a06cf454d481a2bfdbad0098b2aa2f5f2bac4f2 +Subproject commit 9742f80e83abb92e9ae954ef82664bdd2fcb89cd diff --git a/packages/astrid-sdk/src/capabilities.ts b/packages/astrid-sdk/src/capabilities.ts index bc9edf6..21a80c6 100644 --- a/packages/astrid-sdk/src/capabilities.ts +++ b/packages/astrid-sdk/src/capabilities.ts @@ -1,17 +1,20 @@ /** - * Cross-capsule capability queries. Mirrors `astrid_sdk::capabilities`. + * Capability introspection. Mirrors `astrid_sdk::capabilities`. * - * 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, for example. + * {@link 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, for example. {@link 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. * - * Fail-closed: returns `false` for unknown UUIDs, unknown capabilities, or - * registry-unavailable conditions (the host returns `registry-unavailable`, - * which `callHost` raises as a SysError — capsule code that wants to swallow - * registry outages should wrap the call). + * Fail-closed: `check` returns `false` for unknown UUIDs, unknown + * capabilities, or registry-unavailable conditions (the host returns + * `registry-unavailable`, which `callHost` raises as a SysError — capsule code + * that wants to swallow registry outages should wrap the call). */ -import { checkCapsuleCapability } from "astrid:sys/host@1.0.0"; +import { checkCapsuleCapability, enumerateCapabilities } from "astrid:sys/host@1.0.0"; import { callHost } from "./errors.js"; export function check(sourceUuid: string, capability: string): boolean { @@ -20,3 +23,21 @@ export function check(sourceUuid: string, capability: string): boolean { ); return resp.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 + * {@link check} against this capsule's own UUID returns `true`. + * + * Argument-free (the kernel already knows the caller) and infallible: an empty + * array 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. + */ +export function enumerate(): string[] { + return callHost("capabilities.enumerate", () => enumerateCapabilities()); +} diff --git a/packages/astrid-sdk/src/wit-imports.d.ts b/packages/astrid-sdk/src/wit-imports.d.ts index 51a1c1a..a90837f 100644 --- a/packages/astrid-sdk/src/wit-imports.d.ts +++ b/packages/astrid-sdk/src/wit-imports.d.ts @@ -462,6 +462,7 @@ declare module "astrid:sys/host@1.0.0" { export function checkCapsuleCapability( request: CapabilityCheckRequest, ): CapabilityCheckResponse; + export function enumerateCapabilities(): string[]; } // ────────────────────────────────────────────────────────────────────