Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ in this file. The format follows [Keep a Changelog](https://keepachangelog.com/e

## [Unreleased]

### 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

First non-prerelease. `0.1.0-alpha.0` was the test version; this is the
Expand Down
2 changes: 1 addition & 1 deletion contracts
39 changes: 30 additions & 9 deletions packages/astrid-sdk/src/capabilities.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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());
}
29 changes: 29 additions & 0 deletions packages/astrid-sdk/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,35 @@ export namespace hook {
data?: string;
}

/**
* Payload of `hook.v1.event.<hook_name>` envelopes published by the
* hook bridge during fan-out to subscriber capsules.
*
* Subscribers wishing to influence the outcome (merge strategies
* `ToolCallBefore` / `LastNonNull`) reply on the per-request
* scoped topic `hook.v1.response.<hook_name>.<correlation-id>`.
* When `correlation-id` is absent, the dispatch is fire-and-forget
* (merge semantics `None`) and no reply topic exists.
*/
export interface HookEventRequest {
/**
* Semantic hook name (e.g. `before_tool_call`, `session_start`,
* `on_compaction_started`). Matches the mapping table in
* `astrid-capsule-hook-bridge`.
*/
hook: string;
/**
* Original lifecycle event payload, serialized as JSON. Opaque
* to the bus — subscribers parse based on the hook name.
*/
payload: string;
/**
* Per-request correlation id for scoped replies. `none` for
* fire-and-forget hooks.
*/
correlation_id?: string;
}

}

/** Types generated from the `llm` WIT interface. */
Expand Down
9 changes: 9 additions & 0 deletions packages/astrid-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ export type {
SpawnOptions,
ProcessSignal,
EnvVar,
PersistentProcess,
PersistentProcessInfo,
SpawnPersistentOptions,
LogChunkResult,
ResourceLimits,
ProcessPhase,
LogStream,
LogCursor,
OverflowPolicy,
} from "./process.js";
export type { CallerContext } from "./runtime.js";
export type { ResolvedUser, Link } from "./identity.js";
Expand Down
Loading
Loading