Skip to content
Closed
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
192 changes: 192 additions & 0 deletions host/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/// System-level runtime functions: logging, config, time, caller context,
/// entropy, sleep, capability introspection — version 1.1.0.
///
/// Additive superset successor to `astrid:[email protected]`. Every @1.0.0 type
/// (`error-code`, `log-level`, `caller-context`, `capability-check-request`,
/// `capability-check-response`) and every @1.0.0 function (`get-config`,
/// `get-caller`, `log`, `signal-ready`, `clock-ms`, `clock-monotonic-ns`,
/// `sleep-ns`, `random-bytes`, `check-capsule-capability`,
/// `enumerate-capabilities`) is preserved verbatim, so a guest can bind only
/// this version and still reach the whole system surface; @1.1.0 then adds one
/// orthogonal introspection function — `capsule-set-epoch` — at the end. The
/// host backs both versions from one implementation: the @1.1.0 carry-over
/// functions delegate to the @1.0.0 path, identical in behaviour.
///
/// Astrid does not expose any `wasi:*` interfaces to capsules. The host ABI is
/// fully Astrid-owned: every call is gated, principal-scoped, audited, and
/// dispatched through the kernel's capability layer. Readiness multiplexing is
/// provided by Astrid's own `astrid:io/[email protected]` interface; primitives
/// capsules need (random bytes, monotonic clock, sleep) live in this `sys`
/// package for the same reason: a single audit/principal layer covering every
/// host call.
///
/// This namespace ownership matters for the unikernel target as well — when
/// Astrid ships on hermit-rs, the WIT contract is unchanged and the kernel-side
/// impls dispatch to unikernel syscalls instead of wasmtime-wasi-backed
/// futures.
///
/// Frozen per the ABI evolution discipline (RFC: host_abi). Shape changes
/// ship as a new file at a new version path; never edit this file.

package astrid:[email protected];

interface host {
/// Typed error returned from every fallible sys operation.
variant error-code {
/// Capsule lacks the required capability.
capability-denied,
/// Config key is reserved and cannot be read (e.g. internal
/// kernel-only entries).
config-key-reserved,
/// Random-bytes / sleep length exceeded the server-side cap.
too-large,
/// Capsule registry is temporarily unavailable for capability
/// checks (fail-closed: returns this rather than allowed:false).
registry-unavailable,
/// Sleep was interrupted because the capsule is unloading.
cancelled,
/// Unspecific host error; detail is best-effort.
unknown(string),
}

/// Log severity level for structured capsule logging.
enum log-level {
trace,
debug,
info,
warn,
error,
}

/// Caller context returned by `get-caller`.
record caller-context {
/// The acting principal for this invocation.
principal: option<string>,
/// UUID of the capsule that originated the IPC message.
source-id: string,
/// ISO 8601 timestamp of the originating message.
timestamp: string,
}

/// Request to check a capsule's capability.
record capability-check-request {
/// UUID of the capsule to check.
source-uuid: string,
/// Capability name to check.
capability: string,
}

/// Response from a capability check.
record capability-check-response {
/// Whether the capability is allowed.
allowed: bool,
}

/// Read a configuration value from the capsule's manifest `[config]`.
///
/// Returns the raw config value, or `none` if the key is not set in
/// the manifest. String values are returned without JSON-encoding
/// (no extra quotes); the empty string is a valid value distinct
/// from `none`. Audit: not recorded (read-only manifest access).
get-config: func(key: string) -> result<option<string>, error-code>;

/// Get the caller context for the current invocation.
///
/// Returns the acting principal, originating capsule UUID, and message
/// timestamp. Returns an empty context if no caller is available.
/// Audit: not recorded.
get-caller: func() -> result<caller-context, error-code>;

/// Emit a structured log message attributed to the calling capsule.
///
/// Logs are routed to the current principal's log directory with
/// daily rotation. Cross-principal invocations write to the target
/// principal's log directory.
/// Audit: every log call recorded as a structured audit entry.
log: func(level: log-level, message: string);

/// Signal that the capsule's run loop is ready.
///
/// Called by the WASM guest after setting up IPC subscriptions.
/// Notifies the kernel to proceed with loading dependent capsules.
/// No-op if no readiness channel is configured.
signal-ready: func();

/// Get the current wall-clock time as milliseconds since UNIX epoch.
/// Infallible (matches `Instant::now` style; pre-1970 clocks are a
/// host misconfiguration, not a capsule concern).
clock-ms: func() -> u64;

/// Get the current monotonic clock reading in nanoseconds.
///
/// Suitable for measuring elapsed time within a process. Does not
/// jump with NTP adjustments. The absolute value is meaningless
/// across processes or capsule reloads — only differences are.
/// Infallible.
clock-monotonic-ns: func() -> u64;

/// Block the calling guest task for the given duration in nanoseconds.
///
/// Capped server-side at 60 seconds per call (callers needing longer
/// waits loop on shorter sleeps and check for cancellation between
/// iterations). Returns when the duration has elapsed; returns
/// `cancelled` if the capsule is unloading mid-sleep.
sleep-ns: func(duration-ns: u64) -> result<_, error-code>;

/// Fill the caller's requested length with cryptographically secure
/// random bytes from the host's OS-level CSPRNG. Length matches
/// WASI random-bytes (`u64`).
///
/// `length` is capped at 4096 bytes per call (cryptographic use
/// cases fit comfortably; bulk entropy callers loop). Larger
/// requests return `too-large`.
/// Audit: not recorded (read-only, no side effects).
random-bytes: func(length: u64) -> result<list<u8>, error-code>;

/// 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. 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<capability-check-response, error-code>;

/// 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<string>;

/// Return a stable hash of the currently-loaded capsule set — the sorted
/// (capsule-id, package-version) pairs across every registered capsule. The
/// value MOVES on install / removal / version-bump, is load-order
/// independent, and is restart-stable (no process state), so a consumer
/// keyed on the loaded set (e.g. a per-principal tool-schema cache) can
/// re-validate cheaply on its own next turn. Ungated, read-only
/// introspection like enumerate-capabilities. Infallible — returns 0 if the
/// registry is momentarily unavailable (a value that won't match a
/// populated cache's stored epoch, biasing toward a safe refresh).
Comment on lines +188 to +190
capsule-set-epoch: func() -> u64;
Comment on lines +182 to +191

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Returning a magic value of 0 when the registry is momentarily unavailable can lead to correctness issues. If a client starts up with an uninitialized or empty cache (which often defaults to 0 or is represented as 0), and the registry is temporarily unavailable, the host will return 0. The client will compare the returned 0 with its cached 0, assume they match, and skip the initial cache population, leading to a silent failure.\n\nUsing option<u64> instead of a magic 0 allows the client to explicitly handle the unavailable state (e.g., by retrying or keeping the existing cache) without risking a false match with an uninitialized state.

    /// Return a stable hash of the currently-loaded capsule set — the sorted\n    /// (capsule-id, package-version) pairs across every registered capsule. The\n    /// value MOVES on install / removal / version-bump, is load-order\n    /// independent, and is restart-stable (no process state), so a consumer\n    /// keyed on the loaded set (e.g. a per-principal tool-schema cache) can\n    /// re-validate cheaply on its own next turn. Ungated, read-only\n    /// introspection like enumerate-capabilities. Returns none if the\n    /// registry is momentarily unavailable.\n    capsule-set-epoch: func() -> option<u64>;

}
Loading