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
brew tap astrid-runtime/tap && brew install astrid
astrid init --distro @yourorg/your-distro
astrid start
astrid status
astrid capsule listAstrid 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.
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| CapsulesCapsules 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, anduplink. 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.
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):
brew tap astrid-runtime/tap
brew install astridFrom crates.io (requires Rust 1.95+):
cargo install astridFrom source:
git clone https://github.com/astrid-runtime/astrid
cd astrid && cargo build --release # binary at ./target/release/astridAstrid 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.
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 defaultsWhen 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:
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
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 nameDaemon lifecycle
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 releasePer-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.
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 subsetCapsule 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.
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 restartCapsule 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.
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
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 graphWhat'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 andastrid models/astrid doctorfor 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
- The Astrid Book is the canonical reference: the
kernel, the capsule model, the host ABI, the bus, and the security model, grounded in the source
with file and line anchors. Start with Getting Started
to go from nothing to a working agent in a few minutes. - Operator guides live in
docs/: the unified config schema,
LLM model selection, distro signing, the
gateway deployment runbook, and
generating a gateway client. Architecture programs include
conservation of computation,
Muninn, Huginn, and the
Refinery.
Development
cargo build --workspace
cargo test --workspace -- --quiet
cargo clippy --workspace --all-features -- -D warnings
cargo fmt --all -- --checkAll 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
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/EscapeDecisionflows 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 asyolo), andcienvironments. - Worktrees:
ActiveWorktreecreates a per-session Git branch and worktree. Auto-commits WIP on drop, then removes the physical worktree to reclaim disk space.
Development
cargo test -p astrid-workspaceLicense
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
[dependencies]
astrid-vfs = { workspace = true }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_direagerly creates mirrored directories in upper but does not record them as dirty.boundaryandworktreemodules are built but not yet integrated into the public VFS path.
#![deny(unsafe_code)] is enforced crate-wide.
Development
cargo test -p astrid-vfsLicense
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 withMessageRole(system/user/assistant/tool) andMessageContent(text, tool calls, tool results, or multipart)LlmToolDefinitionโ tool schema passed to the modelToolCall/ToolCallResultโ structured call and response withcall_idcorrelation;ToolCall::parse_name()splits"server:tool"routing prefixesStreamEventโ incremental streaming lifecycle:TextDelta,ToolCallStart/Delta/End,ReasoningDelta(for chain-of-thought models),Usage,Done,ErrorLlmResponseโ final non-streaming response withStopReasonand tokenUsage
`kernel` โ Management API
Types for the out-of-band control channel between frontends and the core daemon:
KernelRequestโInstallCapsule,ApproveCapability,ListCapsules,ReloadCapsules,GetCommands,GetCapsuleMetadataKernelResponseโSuccess,Commands,CapsuleMetadata,Error,ApprovalRequiredSYSTEM_SESSION_UUIDโ the well-knownsource_id(00000000-0000-0000-0000-000000000000) used by all kernel-internal IPC messages; capsules that verify message provenance compare against this constant
Usage
[dependencies]
astrid-types = "0.2"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, andthiserror. No OS-specific crates, nostd-only I/O. - Forward-compatible IPC.
IpcPayloaduses#[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-typesdepends 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
# From the workspace root
cargo test -p astrid-types -- --quietLicense
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.
RequestContextcarriesrequest_id,correlation_id,parent_id,session_id, elapsed time, and arbitrary metadata..child(source)creates a correlated child context with a freshrequest_id..span()attaches the context to a tracing span. - Serializable config.
LogConfiground-trips through JSON, TOML, or any serde format.
Quick start
[dependencies]
astrid-telemetry = "0.2"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
cargo test -p astrid-telemetryLicense
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:
KvSecretStorestores secrets in the KV tier with a__secret:key prefix. Works everywhere. No OS-level encryption at rest.KeychainSecretStore(keychainfeature) uses the OS keychain via thekeyringcrate. Per-capsule isolation via service name scoping.FallbackSecretStore(keychainfeature) 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
[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.
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
cargo test -p astrid-storage --all-featuresLicense
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
[dependencies]
astrid-prelude = { workspace = true }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
cargo test -p astrid-preludeLicense
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
[dependencies]
astrid-mcp = { workspace = true }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
cargo test -p astrid-mcp --all-featuresLicense
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 anIdentityStore. 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:
- Resolve
~/.astrid/(or$ASTRID_HOME). Open and migrate the authoritative
durable principal store. - Initialize the MCP process manager with workspace-scoped sandboxing.
- Bootstrap the capability store (ed25519 key pair) and chain-linked audit log.
- Mount the copy-on-write VFS overlay. Writes land in a session-scoped
TempDir. The real workspace is read-only until explicit commit. - 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). - Create the identity store. Bootstrap the CLI root user idempotently.
- Spawn four background tasks: kernel management router, connection tracker, idle monitor, capsule health monitor.
- Spawn the
EventDispatcherto 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 configuresASTRID_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_pathlimit (104 bytes macOS, 108 bytes Linux). - The session token has no
/tmpfallback. Writing a secret under a world-listable directory would undermine the authentication it provides.
Development
cargo test -p astrid-kernelLicense
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:
Continueproceeds 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:
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 intoHookResult
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
cargo test -p astrid-hooksLicense
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.
cd site
npm ci
npm run check
npm run build
npm run devBoth 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 inAOS_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.