astrid Documentation

Astrid is a portable, capability-secure operating system for composable software.

RAW Doc

1. Project Overview & Quickstart (unicity-astrid/astrid)

File: README.md

Astrid

A portable, capability-secure operating system for composable software.







Astrid treats a component the way an operating system treats a process. Every
ability is a sealed WebAssembly capsule: it can be composed with other
capsules, granted only explicit authority, and replaced without expanding its
reach. Astrid is independent of any particular product, model provider, agent
loop, user interface, or distribution.

The kernel underneath is small and deliberately dumb. It routes events,
enforces capabilities, runs the sandbox, and records the audit trail; it holds
no model, tool schema, or business logic. A jailbreak, poisoned tool, or plain
bug still cannot read a file, reach a network, or spawn a process outside its
grant. Authority is a capability the kernel enforces, not an instruction the
model is trusted to follow.

Quick start

bash
brew tap astrid-runtime/tap && brew install astrid
astrid init --distro @yourorg/your-distro
astrid start
astrid status
astrid capsule list

Astrid Runtime does not select or bundle a product distro. Choose a distro you
trust and pass its name, repository, local Distro.toml, or signed .shuttle
archive explicitly with --distro. Operators running an uncomposed runtime can
skip init and start the daemon directly.

Start with the Book for the
architecture or the Contributor Handbook
to contribute.

Why Astrid exists

Agent frameworks put trust in the prompt. Astrid puts it in the runtime. An agent is untrusted code
executing on your machine with access to your files, your network, and your credentials. Telling it
to behave is not a security boundary. An OS-grade boundary is.

  • Cryptographic capability model. Every file path, network host, and tool is a signed ed25519
    grant scoped to a resource pattern, principal-bound, expiry-checked, and globally revocable. No
    grant, no access.
  • WASM sandbox with no ambient authority. Capsules run in Wasmtime with no syscalls, no file
    descriptors, and no host memory. Every external effect is a capability-checked host call over a
    WIT-typed ABI.
  • The kernel is dumb. It instantiates an event bus, loads capsules, and routes IPC bytes under a
    capability ACL. It has no LLM handles, no conversation state, and no tool registry. All
    intelligence lives in capsules, so a capsule bug cannot corrupt shared kernel state.
  • Per-principal everything. Each identity gets isolated capsule access, KV data, secrets, home
    directory, quotas, and audit chain. One principal can never read another's namespace, and it fails
    closed if the caller cannot be resolved.
  • Signed, hash-linked audit chain. Each entry seals the hash of the one before it and is signed.
    Break the chain and the tampering shows.
  • Live capsule lifecycle. Install, upgrade, and remove capsules on a running daemon. No restart.

How it works

Frontends (the CLI, the HTTP gateway, Discord, and so on) are uplinks: protocol clients that
connect to the daemon over a Unix domain socket and speak in IPC events. There is no Frontend
trait. An uplink publishes events and receives responses like any other bus participant.

mermaid
flowchart TB
    CLI[CLI] -->|IPC events over Unix socket| Kernel
    HTTP[HTTP gateway] -->|IPC events| Kernel
    Discord[Discord] -->|IPC events| Kernel
    Uplinks[Other uplinks] -->|IPC events| Kernel

    subgraph Runtime[Astrid]
        Kernel[Kernel: astrid-daemondumb event routerevent bus - capability ACL - audit chain - Wasmtime sandbox]
        Capsules[WASM Component capsules: wasm32-unknown-unknownproviders - orchestrators - tools - fs - http - sessions - registry - identity]
    end

    Kernel -->|capability-checked host callsastrid:* WIT ABI| Capsules

Capsules communicate exclusively through the bus. Each declares what it needs and what it provides
in a Capsule.toml manifest with typed [imports]/[exports] tables; the kernel resolves the
dependency graph by topological sort and boots capsules in order. Tools are an IPC convention, not a
kernel concept: a tool capsule intercepts tool.v1.execute.<name>, and the kernel never sees a tool
schema.

The host ABI is the WebAssembly component model with versioned astrid:* WIT packages:
fs, io, kv, ipc, net, http, sys, process, approval, identity, elicit, and
uplink. Guests import only what their manifest allows, and every call is capability-gated at the
boundary.

The security model

Astrid's security is decomposed. There is no single gate every action funnels through. A capsule has
no ambient authority, and authorization is enforced by independent, per-area mechanisms, each
fail-closed and each enforced where the effect actually happens.

mermaid
flowchart TB
    Action[A capsule action] --> Sandbox[WASM sandboxno syscalls, file descriptors, or host memoryexternal resources are capability-checked host calls]
    Action --> Manifest[Manifest gatedeclared file, network, and process allow-listempty is deny-all; path traversal and SSRF defenses]
    Action --> IPC[IPC ACLdeclared publish and subscribe topics onlyper-principal routing]
    Action --> Capability[Capability tokened25519, principal-bound, scoped, expiring, revocableper-device tokens can be subset-scoped]
    Action --> Approval[Approval gateonce - session - always - denyallow always mints a token; local egress elicits consent]
    Action --> OS[OS sandboxbwrap on Linux - seatbelt on macOS for native subprocesses]
    Action --> Audit[Audit chainsigned, hash-linked JSONL decisions and callssplit per principal and independently verifiable]

These mechanisms are real and independently tested. There is no unified interceptor orchestrating
them. The five-layer gate chapter of The Astrid Book walks
each layer against the source.

Install

Homebrew (macOS and Linux):

bash
brew tap astrid-runtime/tap
brew install astrid

From crates.io (requires Rust 1.95+):

bash
cargo install astrid

From source:

bash
git clone https://github.com/astrid-runtime/astrid
cd astrid && cargo build --release   # binary at ./target/release/astrid

Astrid installs four binaries that work together. You only ever invoke astrid; it starts and
manages the rest.

Binary Role
astrid CLI uplink. Connects to the daemon over the Unix socket. TUI, headless mode, capsule and agent management.
astrid-daemon The kernel process. Loads capsules, routes IPC, enforces capabilities, runs the sandbox.
astrid-build Capsule compiler and packager. Builds to wasm32-unknown-unknown.
astrid-emit Stdio-to-bus bridge for external hook producers.

Initial setup

astrid init --distro fetches a distro (a curated capsule bundle), presents any selection
groups declared by that distro, and prompts for its required configuration. Secrets are stored per
principal in the secret store, never passed on the command line. init writes a Distro.lock
pinning every capsule by BLAKE3 hash, so the same explicit distro input reproduces the same fleet.

bash
astrid init --distro @yourorg/your-distro          # repository distro
astrid init --distro ./Distro.toml                  # local manifest
astrid init --distro ./bundle.shuttle --offline     # signed bundle, no network
astrid init --distro @yourorg/your-distro --yes     # non-interactive defaults

When a distro includes an LLM provider, onboarding can discover that provider's live model list from
its /v1/models endpoint. If the distro also includes an agent loop, confirm that it is ready before
starting a session:

bash
astrid doctor    # daemon up? capsules ready? an LLM available?
astrid chat      # interactive session; the daemon auto-starts on first use
astrid models    # list the current provider's models (a registry-capsule verb)

Headless and scripting

bash
astrid -p "summarize the git log"                    # single prompt, prints and exits
git diff HEAD~1 | astrid -p "write a commit message" # stdin is appended to the prompt
astrid -p "fix all failing tests" --yes              # auto-approve tool requests
astrid -p "continue" --session "$SID"                # resume by id or name

Daemon lifecycle

bash
astrid start     # persistent daemon (survives terminal close)
astrid status    # PID, uptime, connected clients, loaded capsules
astrid ps        # loaded capsules and their lifecycle state
astrid stop      # graceful shutdown
astrid update    # authenticate, verify, and install the latest release

Per-principal isolation

Each principal (agent identity) is a fully isolated tenant: its own capsule access, KV namespace,
secrets, home directory, quotas, and audit chain. New principals inherit nothing by default.

bash
astrid agent create ci-bot                     # clean-slate, least-privilege agent
astrid agent create staging --clone production # full profile + state replica
astrid agent modify ci-bot \
  --add-capsule astrid-capsule-fs              # grant access to a capsule's tools
astrid caps show ci-bot                        # inspect capability grants
astrid quota set -a ci-bot --memory 128MB      # per-principal resource limits
astrid pair-device issue --scope use-only      # scope a device token to a subset

Capsule access is enforced kernel-side at dispatch. A principal can only invoke capsules explicitly
granted to it, and two principals installing the same capsule bytes share one content-addressed
on-disk artifact while getting separate in-memory runtime instances.

Write a capsule

A capsule is a WASM process described by a manifest. The scaffold generates a first-try-compiling
project targeting wasm32-unknown-unknown, plus an AUTHORING.md guide.

bash
astrid capsule new my-capsule    # scaffold Capsule.toml, Cargo.toml, src/lib.rs, .cargo/config.toml
cd my-capsule
astrid capsule build             # compile and package
astrid capsule install .         # hot-loaded into the running daemon, no restart

Capsule authors depend on astrid-sdk, which mirrors
the std module layout (fs, net, process, env, time, log) and adds Astrid modules
(ipc, kv, http, hooks, uplink, identity, approval). The #[capsule] proc macro
generates the WASM ABI boilerplate: exports, serialization, and dispatch for tools, commands, hooks,
and lifecycle entry points.

rust
use astrid_sdk::prelude::*;

#[derive(Default)]
struct Weather;

#[capsule]
impl Weather {
    #[astrid::tool]
    fn forecast(&self, args: ForecastArgs) -> Result<Forecast, SysError> {
        let key = env::var("WEATHER_API_KEY")?;
        let body = http::get(&format!("https://api.example.com/wx?q={}", args.city))?;
        Ok(serde_json::from_slice(&body)?)
    }
}

Building capsules is a first-class workflow in Astrid. The Forge (astrid capsule new plus authoring
tools and runtime-served guidance) is the on-ramp; the direction is an agent that writes, builds, and
installs its own capsules within the capability sandbox.

Reproducible builds and deterministic tests are consumers of
Muninn's verified computation memory: complete source and toolchain
closures become derivation inputs, so an unchanged fleet build is a verified lookup while install,
signing, and publication remain fresh authorized effects.

Live capsule lifecycle

bash
astrid capsule install @org/capsule-name   # install and hot-load
astrid capsule update my-capsule           # hot-swap the running instance
astrid capsule remove my-capsule           # live-unload, no restart
astrid capsule list --verbose              # installed capsules with capability metadata
astrid capsule tree                        # imports/exports dependency graph

What's new in 0.9

Five bodies of work plus a security-hardening pass. See CHANGELOG.md for the full
list.

  • Live capsule lifecycle. Hot-load on install, hot-swap on upgrade, live-unload on remove, all
    without a daemon restart.
  • Per-principal isolation, hardened end-to-end. Principal-view-aware capsule loading with
    content-addressed artifact reuse, kernel-side tool-surface access enforcement, and per-device
    capability scope threaded through the HTTP gateway.
  • LLM provider and model binding. Multi-provider onboarding with live model discovery, plus
    gateway routes and astrid models / astrid doctor for per-principal model management and
    loop-readiness checks.
  • Conversation threads over the HTTP gateway. List, fetch, update, delete, and full-text search
    threads, plus a per-principal live conversation feed with cross-principal isolation enforced at the
    bus.
  • astrid:[email protected] and operator-configurable limits. Per-request timeouts, redirect policy,
    body caps, https-only, and subresource integrity; seven previously-hardcoded ceilings become
    operator knobs.
  • Security. Local-egress consent (transport-origin marker, runtime elicitation, per-capsule and
    per-principal grants), an SSRF airlock that closes IP-literal and redirect bypasses, and a hardened
    supply-chain path (Sigstore attestation, CodeQL, pinned Action SHAs, least-privilege tokens).

Documentation

Development

bash
cargo build --workspace
cargo test --workspace -- --quiet
cargo clippy --workspace --all-features -- -D warnings
cargo fmt --all -- --check

All crates enforce #![deny(unsafe_code)] except astrid-sys and astrid-sdk, where WASM FFI
requires it. Clippy runs at pedantic level and integer-overflow arithmetic is a lint error. Release
binaries for macOS and GNU/musl Linux (x86_64 and aarch64) are built on tag push and signed with keyless
Sigstore. Self-managed updates authenticate the exact archive and its pinned Astrid release-workflow
identity before independently checking the BLAKE3 manifest and extracting any bytes. Homebrew and
Cargo remain responsible for updates they install; signed SHA-256 manifests remain available for
their compatibility requirements. GitHub build-provenance attestations are published as additional
evidence and are not substituted for the updater's release-archive signature. See the
self-update security model.

Contributing

Contributions are welcome. Astrid uses a tiered contributor system that protects security-critical
code while keeping the door open to new contributors. Every pull request must be linked to a GitHub
issue. See CONTRIBUTING.md for the issue-first workflow and tier descriptions.

Changes to any contract surface (the host ABI, the IPC protocol, the capability model, the manifest
schema, or the SDK public API) go through the RFC process in the
RFCs repository before implementation.

License

Dual-licensed under MIT and Apache 2.0, at your option.

Copyright (c) 2025-2026 Joshua J. Bouw and Unicity Labs.


File: crates/astrid-workspace/README.md

astrid-workspace


Host-level process containment. The second sandbox.

Astrid has two sandboxes. The WASM sandbox is inescapable by design: capsule code runs in WebAssembly with zero ambient authority. But the agent also spawns native processes (npm, python, shell commands) that run outside WASM. This crate wraps those processes in OS-native containment so they cannot write outside the workspace or read sensitive host paths.

On Linux: bwrap (bubblewrap). On macOS: sandbox-exec with a generated Seatbelt profile. On other platforms: passthrough with a warning.

The WASM sandbox protects against malicious capsule code. This sandbox protects against malicious tool output that reaches native execution. They are complementary. The workspace sandbox is escapable with user approval. The WASM sandbox is not.

How it works

SandboxCommand takes a std::process::Command and returns a new command prepended with the platform's sandbox wrapper. On Linux, it prepends bwrap with --ro-bind / / for read access, --bind for the writable root, --tmpfs /tmp, --unshare-all, --share-net, and --die-with-parent. On macOS, it generates an SBPL profile inline (passed via -p, not a temp file) that denies by default and allows reads/writes only to the workspace and /tmp.

ProcessSandboxConfig is a data-oriented builder for when you need a tokio::process::Command or finer control. It produces a SandboxPrefix (program + args) that you prepend to any command type.

Injection-safe profile generation

Every path interpolated into a Seatbelt profile is validated first. The crate rejects:

  • Relative paths (must be absolute)
  • Non-UTF-8 paths (lossy coercion would misalign the SBPL rule with the real path)
  • Double-quote (") in paths (SBPL string delimiter, allows sandbox escape)
  • Backslash (\) in paths (SBPL escape character, silently reinterprets the path)
  • Null bytes (defense in depth)

Validation runs on all platforms, not just macOS, so the API contract is consistent everywhere. The tests include an actual SBPL injection payload to verify rejection.

Builder API

rust
use astrid_workspace::ProcessSandboxConfig;

let config = ProcessSandboxConfig::new("/home/user/project")
    .with_network(true)
    .with_extra_read("/usr/local/share/myapp")
    .with_extra_write("/data/output")
    .with_hidden("/home/user/.astrid");

if let Some(prefix) = config.sandbox_prefix()? {
    let mut cmd = tokio::process::Command::new(&prefix.program);
    cmd.args(&prefix.args);
    cmd.arg("npx").arg("@anthropic/mcp-server-filesystem");
}

Hidden paths are overlaid with empty tmpfs on Linux and excluded from the Seatbelt allowlist on macOS.

Internal modules

Several modules are pub(crate) and consumed by higher-level Astrid crates, not directly by users:

  • Boundaries: pre-compiled glob matching, canonical path resolution, and symlink evaluation for directory traversal prevention. Three modes: Safe (always ask), Guided (smart defaults), Autonomous (unrestricted).
  • Escape/airlock: structured EscapeRequest/EscapeDecision flows with session-scoped and permanent approval memory. Remembered paths are canonicalized on store and validated on restore (relative and non-existent paths rejected).
  • Profiles: predefined workspace configurations for safe, power_user, autonomous (aliased as yolo), and ci environments.
  • Worktrees: ActiveWorktree creates a per-session Git branch and worktree. Auto-commits WIP on drop, then removes the physical worktree to reclaim disk space.

Development

bash
cargo test -p astrid-workspace

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-vfs/README.md

astrid-vfs


The agent operates against a copy of the filesystem. The workspace is protected until you say otherwise.

In the OS model, this is the kernel's virtual filesystem layer. Agents read from the workspace. Writes go into an ephemeral upper layer backed by a temp directory. Session ends: commit the diff to the workspace, or drop the temp directory to discard everything. The agent never touches the real files until a human approves the commit.

Two sandboxes, two layers

Path sandbox. path::resolve_path performs a purely lexical traversal check before any syscall. ../../etc/passwd is rejected at this layer. Below that, cap-std enforces OS-level directory confinement. Holding a path string grants nothing. Every operation requires an opaque DirHandle or FileHandle issued by astrid-capabilities.

Copy-on-write overlay. OverlayVfs stacks a read-write upper layer over a read-only lower layer. Reads fall through: if the file exists in upper, serve it; otherwise read from lower. Writes go strictly to upper. dirty_paths() returns what changed. commit() propagates dirty files to lower, creating parent directories as needed. rollback() discards the upper layer.

Key design decisions

Transparent copy-up. Opening a lower-layer file for writing copies its content to the upper layer first. A per-path Mutex prevents duplicate copy-ups under concurrent access. After acquiring the lock, the code re-checks whether another task already completed the copy. Truncate-on-open skips the copy entirely.

Resource quotas. 64 concurrent open file handles, enforced via a Tokio semaphore acquired before calling the OS. 50 MB max file size on reads, copy-ups, and commits. Protects against OOM from large files.

Dirty tracking. OverlayVfs tracks written paths with their kind (File or Dir). commit() handles them differently: files are read-then-written, directories are created. rollback() unlinks files and relies on TempDir drop for directory cleanup. open_dir eagerly creates mirrored directories in upper for handle mapping but does not record them as dirty.

.astridignore boundary. The internal WorktreeVfs (not yet wired into the main VFS path) denies paths matched by gitignore-syntax rules, protecting host secrets like .env files. The boundary module and worktree module exist but are marked pub(crate) and #[allow(dead_code)]. They are built, not yet integrated.

Usage

toml
[dependencies]
astrid-vfs = { workspace = true }
rust
use astrid_vfs::{HostVfs, OverlayVfs, Vfs};
use astrid_capabilities::DirHandle;
use std::path::PathBuf;

// Host-backed sandbox
let vfs = HostVfs::new();
let handle = DirHandle::new();
vfs.register_dir(handle.clone(), PathBuf::from("/var/astrid/sandbox")).await?;

let fh = vfs.open(&handle, "output.txt", true, false).await?;
vfs.write(&fh, b"result").await?;
vfs.close(&fh).await?;

// Copy-on-write overlay
let lower = Box::new(HostVfs::new());
let upper = Box::new(HostVfs::new());
let overlay = OverlayVfs::new(lower, upper);
// All writes go to upper. commit() propagates to lower. rollback() discards.

Known limitations

  • Deleting a file that exists only in the lower layer returns VfsError::NotSupported. No whiteout support.
  • open_dir eagerly creates mirrored directories in upper but does not record them as dirty.
  • boundary and worktree modules are built but not yet integrated into the public VFS path.

#![deny(unsafe_code)] is enforced crate-wide.

Development

bash
cargo test -p astrid-vfs

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-types/README.md

astrid-types


Shared data types for the Astrid secure agent runtime.

This crate is the single source of truth for the types that cross boundaries in Astrid: between the host kernel and WASM capsule guests over IPC, between the runtime and LLM provider capsules, and between frontends and the kernel management API. Both the kernel (astrid-events) and the user-space SDK (astrid-sdk) depend on it โ€” nothing else does.

Modules

`ipc` โ€” Cross-boundary messaging

IpcMessage is the envelope for every event published on the Astrid event bus. Its IpcPayload enum covers the full protocol surface:

Variant Direction Purpose
UserInput frontend โ†’ kernel Raw text from CLI, Telegram, etc.
AgentResponse kernel โ†’ frontend Agent output (streaming or final)
LlmRequest kernel โ†’ LLM capsule Dispatch a generation request
LlmStreamEvent LLM capsule โ†’ kernel Incremental token/tool-call events
LlmResponse LLM capsule โ†’ kernel Final non-streaming response
ToolExecuteRequest kernel โ†’ tool router Execute a named tool
ToolExecuteResult tool router โ†’ kernel Tool output or error
ToolCancelRequest kernel โ†’ tool router Cancel in-flight tool calls
ApprovalRequired capsule โ†’ frontend Request user capability approval
ApprovalResponse frontend โ†’ capsule User's approve/deny decision
OnboardingRequired capsule โ†’ frontend Request missing env vars
SelectionRequired capsule โ†’ frontend Ask user to pick from a list
ElicitRequest capsule โ†’ frontend Prompt for a single runtime input
ElicitResponse frontend โ†’ capsule User's input value
Connect / Disconnect frontend โ†” kernel Session lifecycle
Custom any Escape hatch for unstructured plugins

Unknown variant tags from newer protocol versions deserialize to IpcPayload::Unknown rather than failing โ€” capsules built against an older astrid-types stay forward-compatible.

Messages carry an optional signature: Option<Vec> for stateless verification across a distributed swarm.

`llm` โ€” LLM message and streaming types

Provider-agnostic types that every LLM capsule speaks:

  • Message โ€” conversation turn with MessageRole (system/user/assistant/tool) and MessageContent (text, tool calls, tool results, or multipart)
  • LlmToolDefinition โ€” tool schema passed to the model
  • ToolCall / ToolCallResult โ€” structured call and response with call_id correlation; ToolCall::parse_name() splits "server:tool" routing prefixes
  • StreamEvent โ€” incremental streaming lifecycle: TextDelta, ToolCallStart/Delta/End, ReasoningDelta (for chain-of-thought models), Usage, Done, Error
  • LlmResponse โ€” final non-streaming response with StopReason and token Usage

`kernel` โ€” Management API

Types for the out-of-band control channel between frontends and the core daemon:

  • KernelRequest โ€” InstallCapsule, ApproveCapability, ListCapsules, ReloadCapsules, GetCommands, GetCapsuleMetadata
  • KernelResponse โ€” Success, Commands, CapsuleMetadata, Error, ApprovalRequired
  • SYSTEM_SESSION_UUID โ€” the well-known source_id (00000000-0000-0000-0000-000000000000) used by all kernel-internal IPC messages; capsules that verify message provenance compare against this constant

Usage

toml
[dependencies]
astrid-types = "0.2"
rust
use astrid_types::{IpcMessage, IpcPayload, Message, StreamEvent};
use uuid::Uuid;

// Build an outbound IPC message
let msg = IpcMessage::new(
    "llm.v1.request.generate.anthropic",
    IpcPayload::LlmRequest {
        request_id: Uuid::new_v4(),
        model: "claude-3-5-sonnet".into(),
        messages: vec![Message::user("Hello!")],
        tools: vec![],
        system: "You are a helpful assistant.".into(),
    },
    Uuid::new_v4(), // source_id
);

// Deserialize a payload received from the event bus
let payload = IpcPayload::from_json_value(raw_json);

Design notes

  • WASM-compatible. Dependencies are limited to serde, serde_json, uuid, chrono, and thiserror. No OS-specific crates, no std-only I/O.
  • Forward-compatible IPC. IpcPayload uses #[serde(other)] so capsules compiled against an older schema degrade gracefully when they receive unfamiliar variants.
  • No reverse dependencies. Kernel crates depend on astrid-types; astrid-types depends on nothing in the Astrid workspace. This keeps the dependency graph acyclic and the crate publishable independently.
  • Strict lints. deny(unsafe_code), deny(missing_docs), deny(clippy::unwrap_used).

Development

bash
# From the workspace root
cargo test -p astrid-types -- --quiet

License

Dual-licensed under MIT and Apache 2.0.

Copyright (c) 2025-2026 Joshua J. Bouw and Unicity Labs.


File: crates/astrid-telemetry/README.md

astrid-telemetry



The structured logging layer of the Astrid OS.

Every kernel and every daemon needs observability. This crate wraps the tracing ecosystem into a typed, serializable configuration API. Components call setup_logging once and get consistent output format, file rotation, and per-target directive filtering. RequestContext propagates correlation IDs across async boundaries so distributed call chains remain traceable in any output format.

Why it exists

Astrid has many crates that all emit tracing events: the kernel, the capsule runtime, the MCP client, the approval system. Without a shared logging configuration, each crate invents its own setup. This crate centralizes that into a single LogConfig struct that implements Serialize/Deserialize, loads from any config file format, and produces a consistent subscriber across the entire process.

The RequestContext exists because agent operations fan out. A single user message triggers orchestration, tool calls, approval checks, and audit writes. Correlation IDs tie these together so grep across JSON logs works.

What it provides

  • Four output formats. Pretty (ANSI-colored), Compact (single-line), Json (machine-ingestible), Full (all span fields). Switch with a single enum variant.
  • Three output targets. Stdout, stderr, or a rolling file appender. File output automatically disables ANSI codes.
  • File rotation. Daily, hourly, minutely, or never. Backed by tracing-appender.
  • Per-target directives. Apply astrid_mcp=debug,astrid_core=trace-style filters on top of the base level.
  • Request correlation. RequestContext carries request_id, correlation_id, parent_id, session_id, elapsed time, and arbitrary metadata. .child(source) creates a correlated child context with a fresh request_id. .span() attaches the context to a tracing span.
  • Serializable config. LogConfig round-trips through JSON, TOML, or any serde format.

Quick start

toml
[dependencies]
astrid-telemetry = "0.2"
rust
use astrid_telemetry::{LogConfig, LogFormat, setup_logging};

let config = LogConfig::new("info")
    .with_format(LogFormat::Pretty)
    .with_directive("astrid_mcp=trace");

setup_logging(&config)?;

Development

bash
cargo test -p astrid-telemetry

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-storage/README.md

astrid-storage


The persistence layer. Disk for the OS.

An operating system needs disk. Astrid has a raw key-value contract projected
onto typed, durable per-principal roots. The legacy SurrealKV reader remains
available only during the migration window.

Storage model

Capsules and kernel services need fast, isolated byte storage. The native
object store supplies that contract directly; query services belong above this
crate rather than inside its persistence substrate.

Deployment KV backend
Dev / single-agent Durable principal store
Production / multi-node Durable principal store plus future placement execution

Distributed placement for the principal store remains implementation work; it
is not exposed as a runtime backend selector.

Namespace isolation

Every KV operation is scoped to a namespace. WASM guests receive a ScopedKvStore bound to wasm:{capsule_id} and never see the raw key structure. The kernel uses system:* namespaces for internal state.

Empty namespaces, empty keys, and names containing null bytes are rejected before reaching the storage engine. The native runtime maps host-stamped {principal}:capsule:{id} namespaces onto one typed root per validated principal; kernel namespaces remain under a separate system owner.

Secret storage

The SecretStore trait provides synchronous credential storage (called from synchronous Extism host functions that bridge to async via block_on). Three implementations:

  • KvSecretStore stores secrets in the KV tier with a __secret: key prefix. Works everywhere. No OS-level encryption at rest.
  • KeychainSecretStore (keychain feature) uses the OS keychain via the keyring crate. Per-capsule isolation via service name scoping.
  • FallbackSecretStore (keychain feature) probes the keychain once at construction. If accessible, all operations go to keychain. If not, all go to KV. No per-operation fallback that could scatter secrets across both backends.

The build_secret_store convenience constructor picks the best available backend.

Identity

IdentityStore manages users and cross-platform identity links. A Discord user, a Telegram user, and a CLI user can all resolve to the same AstridUserId. Platform names are normalized (case, whitespace). Path-injection characters (/, \0) in platform names, user IDs, and display names are rejected before key construction.

Feature flags

Feature Enables
legacy-surrealkv Legacy SurrealKvStore reader and migrator
keychain KeychainSecretStore + FallbackSecretStore
full Legacy compatibility features

The KV contract is never feature-gated. KvStore, MemoryKvStore,
ScopedKvStore, KvSecretStore, and the principal-store adapter are always
available. The old kv name remains only so existing dependent manifests keep
building; only the transition dependency is optional.

Usage

toml
[dependencies]
astrid-storage = { workspace = true }

Enable legacy-surrealkv only in a binary responsible for importing an
existing SurrealKV store. New deployments and ordinary consumers do not need
the transition dependency.

rust
use std::sync::Arc;
use astrid_storage::kv::{MemoryKvStore, ScopedKvStore};

let store = Arc::new(MemoryKvStore::new());
let scoped = ScopedKvStore::new(store, "wasm:my-plugin")?;

scoped.set("config", b"{}".to_vec()).await?;
scoped.set_json("prefs", &serde_json::json!({"key": "value"})).await?;
let loaded: serde_json::Value = scoped.get_json("prefs").await?.unwrap();

Performance

Performance values are evidence artifacts, not crate API documentation. Run
the manual Storage benchmark evidence workflow to produce content-bound
reports for Linux, macOS, and Windows from the same exact revision and workload
configuration. The report records the code revision, tree state, executable,
arguments, host environment, cache policy, and raw samples.

Historical device-local results remain useful for diagnosing regressions, but
they are not portable release numbers. The full methodology, cross-platform CI
contract, historical baselines, raw samples, and content-bound evidence
envelopes live in
../../docs/astrid-storage-performance.md
and ../../docs/benchmarks/storage-io/.

Development

bash
cargo test -p astrid-storage --all-features

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-prelude/README.md

astrid-prelude


The unified import for Astrid OS kernel-side types.

In the OS model, kernel crates (astrid-kernel, astrid-approval, astrid-cli) need types from across the kernel surface: crypto primitives, capability tokens, audit entries, event bus handles, MCP definitions, hook types, telemetry config, and workspace boundaries. Importing nine crate preludes individually is tedious and produces import blocks that obscure the actual code.

This crate re-exports the public prelude of every foundational kernel crate into a single use astrid_prelude::*. It contains zero runtime logic. The compiler erases the indirection entirely.

What it re-exports

Source crate What you get
astrid-core AgentId, SessionId, Permission, RiskLevel, ApprovalDecision, ElicitationRequest
astrid-crypto KeyPair, PublicKey, Signature, ContentHash
astrid-capabilities CapabilityToken, CapabilityStore, CapabilityValidator, ResourcePattern, TokenScope
astrid-audit AuditLog, AuditEntry, AuditAction, AuthorizationProof, ChainVerificationResult
astrid-events EventBus, EventReceiver, AstridEvent, EventMetadata
astrid-hooks Hook, HookEvent, HookHandler, HookResult
astrid-mcp McpClient, SecureMcpClient, ServerManager, ServerConfig, ToolDefinition
astrid-telemetry LogConfig, LogFormat, setup_logging, RequestContext
astrid-workspace SandboxCommand

When to use it

Use astrid-prelude in crates that already depend on most of these transitively: the kernel, the CLI, integration tests. It saves import noise without adding new dependencies.

Do not use it in leaf crates that only need one or two upstream types. Import astrid_core::prelude::* or astrid_crypto::prelude::* directly to keep dependency trees narrow.

Quick start

toml
[dependencies]
astrid-prelude = { workspace = true }
rust
use astrid_prelude::*;

// Full kernel type surface available:
// KeyPair, CapabilityToken, AuditEntry, EventBus, McpClient, ...

Lint policy

Enforces deny(unsafe_code), deny(missing_docs), deny(clippy::unwrap_used), deny(unreachable_pub). This crate is pure re-exports, so these lints protect against accidental logic creep.

Development

bash
cargo test -p astrid-prelude

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-mcp/README.md

astrid-mcp


The syscall boundary for external tools. Device drivers for the OS.

Astrid capsules live inside WASM. External tool servers live outside it. This crate bridges the gap: it manages MCP server processes, verifies their binaries before spawning them, gates every tool call through the capability system, and writes audit entries for every invocation. It is the sole path through which the kernel reaches external tools.

Wraps the rmcp SDK. Implements the MCP 2025-11-25 spec (sampling, roots, elicitation, URL elicitation).

Why this exists

An agent calling mcp://filesystem:read_file is analogous to a process calling read(2). The kernel must verify authorization, log the call, and manage the server process that handles it. Without this crate, every tool call would bypass the security model.

Two client layers

McpClient manages server processes, caches tools, and dispatches calls without authorization checks. It is the raw interface for kernel-internal use.

SecureMcpClient wraps McpClient with the full security chain: capability token validation, single-use token consumption (atomic, blocks replay), issuer verification against the runtime's ed25519 public key, and audit logging of every call with a BLAKE3 hash of the arguments. This is what the kernel's tool dispatch path uses.

Both clients are cheaply cloneable. All clones share the same ServerManager, tools cache, and capability state via Arc.

Server lifecycle

ServerManager owns server processes. It starts them, monitors health via RunningService::is_closed(), enforces restart policies (Never, OnFailure { max_retries }, Always), and tracks restart counts for backoff. try_reconnect checks the restart policy and reconnects atomically to avoid TOCTOU races.

Binary hash verification runs before any process spawns. If a binary_hash is configured, the server binary is read, BLAKE3-hashed, and compared. Mismatch aborts with McpError::BinaryHashMismatch. No fallback, no override.

Untrusted servers (the default) are wrapped in an OS-level sandbox via astrid-workspace. Trusted servers (trusted = true) run natively.

Reactive tool cache

When a server sends notifications/tools/list_changed, a background listener refreshes the in-memory tool cache without polling. The notice channel also handles uplink registrations for Tier 2 capsule processes.

Server name validation

Server names are validated against a strict ASCII allowlist: alphanumeric, hyphens, underscores, colons, dots (not leading). Path separators, null bytes, shell metacharacters, and Unicode lookalikes are rejected. Display names in error messages are truncated to 40 characters to prevent log poisoning from attacker-controlled input.

Usage

toml
[dependencies]
astrid-mcp = { workspace = true }
rust
use astrid_mcp::{McpClient, ServersConfig, ServerConfig};

let mut config = ServersConfig::default();
config.add(
    ServerConfig::stdio("filesystem", "npx")
        .with_args(["-y", "@anthropics/mcp-server-filesystem", "/tmp"])
        .auto_start(),
)?;

let client = McpClient::with_config(config);
client.connect("filesystem").await?;

let tools = client.list_tools().await?;
let result = client.call_tool(
    "filesystem", "read_file",
    serde_json::json!({"path": "/tmp/hello.txt"})
).await?;

Development

bash
cargo test -p astrid-mcp --all-features

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-kernel/README.md

astrid-kernel


The microkernel.

In the OS model, this is init. It boots every subsystem, binds the Unix socket, mounts the VFS overlay, opens the audit log, and starts the IPC bus. No business logic lives here. No cognitive loops, no LLM calls, no tool implementations. The kernel routes bytes between capsules and enforces system-wide invariants. Frontends (CLI, Discord, web) connect over the Unix socket and drive the same Kernel instance.

What it owns

The Kernel struct holds every system-wide resource: EventBus,
CapsuleRegistry, SecureMcpClient, CapabilityStore, OverlayVfs, the
injected KvStore, AuditLog, AllowanceStore, SessionToken, and an
IdentityStore. Native boot injects TreeKvStore over the authoritative
durable principal store. All fields are pub. One struct, one owner, no
ambient state.

Boot sequence

Kernel::new(session_id, workspace_root) runs the full boot:

  1. Resolve ~/.astrid/ (or $ASTRID_HOME). Open and migrate the authoritative
    durable principal store.
  2. Initialize the MCP process manager with workspace-scoped sandboxing.
  3. Bootstrap the capability store (ed25519 key pair) and chain-linked audit log.
  4. Mount the copy-on-write VFS overlay. Writes land in a session-scoped TempDir. The real workspace is read-only until explicit commit.
  5. Bind ~/.astrid/run/system.sock (parent directory 0o700). Generate a 256-bit CSPRNG session token and write it to ~/.astrid/run/system.token (a 0o600 file).
  6. Create the identity store. Bootstrap the CLI root user idempotently.
  7. Spawn four background tasks: kernel management router, connection tracker, idle monitor, capsule health monitor.
  8. Spawn the EventDispatcher to route IPC events to capsule interceptors.

Requires a multi-threaded tokio runtime. The constructor asserts this at the top and panics on single-threaded runtimes because block_in_place would deadlock.

Management API

The kernel router listens on astrid.v1.request.* and handles requests like ListCapsules, GetCommands, GetCapsuleMetadata, ReloadCapsules, and InstallCapsule. Mutating operations are rate-limited with a sliding-window limiter (e.g. ReloadCapsules capped at 5/min). Read-only operations are unlimited.

ApproveCapability is defined in the protocol but not yet implemented (it returns an error).

Daemon lifetime

Ephemeral daemons use authenticated client lifecycle leases and shut down when
the final client disconnects. Lease accounting uses a synchronous, non-lossy
observer rather than the bounded broadcast queue used for ordinary events.
Persistent daemons remain running unless the operator explicitly configures
ASTRID_IDLE_TIMEOUT_SECS.

Socket security

  • Stale sockets are detected via connect() probe. Connection refused = stale, safe to remove. Successful connect = live kernel, boot aborted.
  • Symlinks at the socket path are unconditionally removed before bind.
  • Socket path length is validated against the platform sun_path limit (104 bytes macOS, 108 bytes Linux).
  • The session token has no /tmp fallback. Writing a secret under a world-listable directory would undermine the authentication it provides.

Development

bash
cargo test -p astrid-kernel

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.


File: crates/astrid-hooks/README.md

astrid-hooks


User-defined interceptors. Signal handlers for the OS.

The kernel fires events at 23 points in the execution lifecycle. Hooks intercept those events and return typed verdicts: continue, block, ask the human, or continue with modifications. Shell commands, HTTP webhooks, and Extism WASM modules can all serve as handlers. No core engine changes required.

This is how operators customize Astrid without forking it. A security team blocks rm via a shell hook. A compliance system logs every tool call to an external webhook. A WASM module rewrites prompts before they reach the model. All without touching kernel code.

How it works

A hook binds one handler to one event. When the kernel fires that event, the HookExecutor runs all matching hooks in priority order (lower integer runs first). Each handler returns a HookResult:

  • Continue proceeds normally.
  • Block { reason } short-circuits the chain and rejects the operation.
  • Ask { question } pauses for human input.
  • ContinueWith { modifications } proceeds with altered context.

If multiple hooks fire on the same event, Block takes absolute precedence. Ask takes precedence over Continue. ContinueWith modifications merge across hooks.

Each hook declares a FailAction for when the handler itself fails (timeout, crash, bad output): Warn (log and continue, the default), Block (treat failure as rejection), or Ignore (silent).

23 lifecycle events

Session start/end/reset. Prompt assembly. Tool calls (pre, post, error, result persist). Approval flows (pre, post). Context compaction (pre, post). Subagent start/stop. Model resolution. Message send/receive/sent. Agent loop end. Kernel start/stop. Notification.

Three handler types

  • Command: spawns a shell process, passes context as ASTRID_HOOK_* environment variables and JSON on stdin. Reads the verdict from stdout.
  • HTTP: POSTs to a webhook URL. Reads the verdict from the response body.
  • WASM: calls a function in an Extism module. Reads the verdict from the return value.

TOML discovery

Hooks load from HOOK.toml, hook.toml, or hooks.toml files in .astrid/hooks/ or configured extra paths. Example:

toml
event = "pre_tool_call"
name = "block-rm"
timeout_secs = 5
fail_action = "block"

[handler]
type = "command"
command = "sh"
args = ["-c", "case \"$ASTRID_HOOK_DATA\" in *rm*) echo 'block: rm blocked';; *) echo continue;; esac"]

Output protocol

Handlers signal results through stdout (command) or response body (HTTP):

  • Empty or "continue": Continue
  • "block: <reason>": Block
  • "ask: <question>": Ask
  • JSON with "action" field: deserialized directly into HookResult

Current state

The public API surface is intentionally narrow: Hook, HookHandler, HookEvent, and HookResult. The manager, executor, discovery, profiles, and handler modules are all pub(crate) internal, consumed by the kernel. Most builder methods on Hook and HookHandler are also pub(crate). This crate defines the types and execution model. The kernel drives it.

Built-in profiles exist for minimal, logging, security, and development setups, but these are internal and not yet exposed as user-facing configuration.

Development

bash
cargo test -p astrid-hooks

License

Dual MIT/Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.

2. Official Technical Reference & Guides (unicity-astrid/unicity-astrid.github.io)

Unicity AOS website

Product website and developer documentation for Unicity AOS, the modular
Agent Operating System from Unicity. The site explains the product distribution
while preserving the boundary to Astrid Runtime,
the open operating-system engine beneath it.

The production hostname is aos.unicity.ai. Until the
first signed AOS product release is published, the install surface visibly marks
stable, dev, nightly, Homebrew, and AOS Oracle channels as staged and keeps every
copy action disabled.

Repository layout

Path Purpose
site/ Astro website, developer guide, local docs lens, and product pages
kernel-web/ Browser bridge for the real Astrid kernel used by interactive explanations
site-capsules/ WebAssembly components used only by the in-browser experience
kernel-smoke/ and spike/ Runtime portability and component-model probes
notes/ and DESIGN.md Implementation notes and the current product-site contract

Develop

The website requires Node.js 22.12.0 or newer.

sh
cd site
npm ci
npm run check
npm run build
npm run dev

Both check and build are release gates. The product release integration is
centralized in site/src/lib/release.ts; available may become true only
after matching AOS artifacts, checksums, signatures, and installer assets exist.

The default installer channel is stable. Development and nightly channels are
always explicit (--channel dev and --channel nightly) and never fall back to
stable. The commands are documentation only while their channel metadata is
unavailable.

site/public/install.sh is a byte-for-byte mirror of the canonical aos-ce
installer. Pages CI records the exact source commit in
AOS_INSTALLER_SOURCE_COMMIT and compares both source and built copies. Update
that commit and the mirror together only after the canonical installer contract
has landed.

Community Edition source lives in
unicity-aos/aos-ce.