Your AI agent proposes. You approve. The runtime enforces it with math, not hope.
Astrid is a secure runtime for building AI agents that cannot go rogue.
Most agent frameworks control what an AI can do with system prompts, text that tells the model "do not delete files" or "do not spend money." The problem: models can be tricked into ignoring those instructions (prompt injection), and there is no way to prove they followed them.
Astrid takes a different approach. When an agent wants to do something risky (delete a file, make an HTTP request, run a shell command), it has to get permission first. That permission is recorded as a signed token (the same kind of digital signature used in SSH keys) that the runtime checks before executing anything. The agent cannot forge the token, cannot replay an old one, and cannot talk its way past the check. Every decision goes into an append-only log where each entry is chained to the previous one, so you can detect if anyone tampers with the history.
# Start an interactive session
astrid chat
# Or run the daemon for multi-frontend access
astridd --ephemeralHere is the flow when an agent tries to delete a file:
-
Agent calls
delete_file("/home/user/important.txt") -
The runtime classifies this as a
FileDeleteaction (risk level: High) -
Policy check: Is this path blocked? (e.g.,
/etc/**is always off-limits) -
Token check: Does a signed authorization token already cover this? If yes, proceed.
-
Budget check: Is the session within its spending limit?
-
Human approval: If no token exists, the user sees a prompt:
Delete file: /home/user/important.txt [Allow Once] [Allow Session] [Allow Always] [Deny] -
If the user picks "Allow Always," the runtime creates a signed token so they will not be asked again for this file. That token is scoped, time-limited (1 hour by default), and linked back to the audit entry that created it.
-
The action is logged with the authorization proof, the session context, and a hash linking it to the previous log entry.
The agent never decides what it is allowed to do. It proposes actions. You approve them. The runtime enforces your decision.
- Coding assistants and dev tools: where an unconstrained agent could
rm -rf /or push to production - Multi-user bots (Telegram, Discord): where multiple users share one runtime and need isolated sessions and budgets
- Enterprise integrations: agents that touch internal APIs and databases where every action needs a paper trail
- Extension ecosystems: where third-party code runs alongside trusted operations and you need real sandboxing
Every tool call passes through a SecurityInterceptor that combines five checks. Both the admin policy AND the user's authorization must agree before anything executes:
Agent proposes action
|
[1. Policy] Admin sets hard boundaries: blocked commands, denied paths/hosts.
| "sudo" is always blocked. "/etc/**" is always blocked.
| These cannot be overridden.
|
[2. Token] Does a signed authorization token cover this action?
| Tokens use ed25519 signatures (same algorithm as SSH keys).
| They are scoped to specific resources with glob patterns,
| have expiration times, and link back to the audit entry
| that created them.
|
[3. Budget] Is the session within its spending limit?
| Per-action and per-session limits are enforced atomically.
|
[4. Approval] If no token exists, ask the human.
| Options: Allow Once, Allow Session, Allow Always, Deny.
| "Allow Always" creates a signed token for future use.
| If the human is unavailable, the action is queued (not silently skipped).
|
[5. Audit] Log the decision (allowed or denied) with the authorization proof.
Each entry contains the hash of the previous entry,
so tampering with history is detectable.
This is real code. Look at crates/astrid-approval/src/interceptor.rs. The SecurityInterceptor::intercept() method implements this exact flow. The tests in that file cover policy blocks, budget enforcement, token-based authorization, and the "Allow Always" flow that creates new tokens.
Untrusted code and trusted agent actions are fundamentally different threats, so they get different sandboxes.
Extensions run inside WebAssembly via Wasmtime. This is not a policy you configure. It is a physical boundary enforced by the WASM runtime. A WebAssembly component cannot make a syscall. It has no file descriptors, no network sockets, no access to process memory outside its own linear memory. It is like running code inside a calculator that only has 12 buttons.
Those 12 buttons are host functions, the only way sandboxed code can interact with the outside world:
| Host function | What it does | Security gated? |
|---|---|---|
read-file |
Read a file inside the workspace | Yes |
write-file |
Write a file inside the workspace | Yes |
http-request |
Make an HTTP request | Yes |
fs-exists, fs-mkdir, fs-readdir, fs-stat, fs-unlink |
Filesystem operations | Yes |
kv-get, kv-set |
Scoped key-value storage | No (isolated per component) |
get-config |
Read extension config values | No |
log |
Write to the host log | No |
Every "Yes" in that table means the host function goes through a security gate check before the operation happens. Code calling write-file("../../etc/passwd", ...) gets rejected twice: once by path confinement (all paths are canonicalized and must resolve inside the workspace root), and again by the security gate.
The hard limits:
- 64 MB memory (configurable down, not up). The WASM linear memory ceiling.
- 30-second timeout per call. If execution hangs, it gets killed.
- BLAKE3 hash verification: every
.wasmbinary is hashed on load and checked against the manifest. If someone swaps the file, it will not load. In production mode, components without a hash in their manifest are rejected entirely.
The agent itself operates within your project directory. Inside the workspace, it can read and write files freely. But the moment it tries to do something outside that boundary (write to a system path, run a shell command, make a network request), it goes through the approval flow described above.
You configure how strict this is:
[workspace]
mode = "safe" # "safe" = ask about everything
# "guided" = auto-approve reads, ask about writes
# "autonomous" = trust the agent within the workspace
escape_policy = "ask" # what happens when the agent tries to leave the workspace
# "ask" = prompt the user
# "deny" = block silentlyThe key difference: the WASM sandbox is a hard wall that nobody can turn off. The workspace boundary is a fence with a gate that you control.
Astrid implements a Phase 4 Manifest-First architecture for all user-provided code and extensions, powered by the astrid-capsule engine.
Rather than forcing developers into a single execution paradigm, Astralis uses a Composite Architecture. A single Capsule.toml manifest can orchestrate multiple execution engines under one logical unit. The system parses this declarative manifest to wire up capabilities, tools, and environment variables before a single line of user code executes.
A single capsule can wrap any combination of:
- WASM Engine: Executes compiled WebAssembly within a strictly sandboxed Extism runtime.
- MCP Host Engine: The "airlock override". Spawns native child processes (e.g.,
node,python) to communicate via legacy Model Context Protocol (MCP) JSON-RPC over standard I/O. - Static Engine: Injects static context, declarative skills, and predefined commands directly into OS memory without booting any virtual machines.
The manifest is the absolute source of truth. It defines the entrypoints, requests OS capabilities, and declares dependencies.
[package]
name = "github-agent"
version = "1.0.0"
# 1. The primary WASM logic
[component]
entrypoint = "bin/github_agent.wasm"
# 2. OS Capabilities requested
[capabilities]
net = ["api.github.com"]
fs_read = ["/home/user/.gitconfig"]
# 3. Environment Variables elicited during docking
[env.GITHUB_TOKEN]
type = "secret"
request = "Please provide a GitHub Personal Access Token"
# 4. The Airlock Override: Legacy MCP stdio server
[[mcp_server]]
id = "local-git"
type = "stdio"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-git"]
# 5. Scheduled Background Tasks (Cron)
[[cron]]
name = "sync-issues"
schedule = "0 * * * *"
action = "astrid.github.sync"
# 6. eBPF-style Lifecycle Hooks
[[interceptor]]
event = "BeforeToolCall"
# 7. LLM Provider (Agent Brain)
[[llm_provider]]
id = "claude-3-5-sonnet"
description = "Anthropic Claude 3.5 Sonnet Provider"
capabilities = ["text", "vision", "tools"]If you have TypeScript/JavaScript extensions from the OpenClaw ecosystem, Astrid can compile them to WASM automatically. The pipeline is pure Rust, no Node.js required for compilation:
TypeScript --> OXC transpiler --> JS --> ABI shim --> QuickJS/Wizer --> capsule.wasm
The key trick: Wizer pre-initializes the QuickJS engine at compile time, so cold start is near-zero. Extensions that need heavy npm dependencies fall back to running as a Node.js subprocess via the MCP Host Engine.
- Signed authorization tokens (ed25519): scoped, time-limited, linked to audit trail
- Append-only audit log: each entry hashes the previous one (BLAKE3), detects tampering
- Input classification: everything entering the system is tagged as trusted (verified user), pre-authorized (token), or untrusted (tool results, external data)
- Admin policy layer: blocked commands, denied paths/hosts, argument size limits
- Budget tracking: per-session and per-action spending limits, enforced atomically
- Deferred approval: if you are away, risky actions queue instead of failing silently
- Streaming LLM support: Anthropic Claude, OpenAI-compatible providers, and Zai
- MCP client (2025-11-25 spec): sampling, roots, elicitation, URL elicitation, and tasks via
rmcp - 8 built-in tools:
read_file,write_file,edit_file,glob,grep,bash,list_directory,task. All run in-process. - Sub-agent delegation: spawn child agents with restricted permissions and configurable concurrency
- Session persistence: sessions survive daemon restarts
- CLI (
astrid): interactive REPL with TUI, syntax highlighting, clipboard support - Telegram (
astrid-telegram): bot with inline approval buttons and streaming responses - Daemon (
astridd): background server with JSON-RPC over WebSocket - Build your own: implement the
Frontendtrait to add new interfaces
astrid (CLI) ----+
+--> astridd (daemon)
astrid-telegram--+ |
|-- astrid-llm (provider abstraction)
|-- astrid-mcp (MCP client + server lifecycle)
|-- astrid-tools (built-in tools)
|-- astrid-approval (security interceptor)
|-- astrid-capabilities (signed tokens)
|-- astrid-workspace (boundaries)
|
|-- astrid-capsule (composite execution engines & manifest routing)
| +-- astrid-openclaw (TS/JS -> WASM compiler)
|
|-- astrid-audit (chain-linked logging)
|-- astrid-crypto (ed25519 + BLAKE3)
|-- astrid-storage (SurrealKV + SurrealDB)
+-- astrid-config (layered TOML)
The Frontend trait is how you plug in new UIs. Every frontend shares the same runtime, sessions, authorization tokens, budget tracking, and audit log.
- Rust 1.94+ (edition 2024)
- An Anthropic API key (or any OpenAI-compatible provider)
git clone https://github.com/unicity-astrid/astrid.git
cd astrid
cargo build --releaseThis produces two binaries:
target/release/astrid: the CLI clienttarget/release/astridd: the daemon server
# Initialize a workspace (creates .astrid/ directory)
astrid init
# Start chatting (creates ~/.astrid/config.toml on first run)
astrid chat# Ephemeral mode (shuts down when all clients disconnect)
astridd --ephemeral
# Or manage via the CLI
astrid daemon run --ephemeral
astrid daemon status
astrid daemon stop| Command | Description |
|---|---|
astrid chat |
Start an interactive chat session |
astrid init |
Initialize a workspace |
astrid daemon run|status|stop |
Manage the background daemon |
astrid sessions list|show|delete|cleanup |
Manage sessions |
astrid capsule list|install|remove|compile|info |
Manage loaded capsules and manifests |
astrid servers list|running|start|stop|tools |
Manage legacy MCP servers |
astrid audit list|show|verify|stats |
View and verify audit logs |
astrid config show|validate|paths |
View and validate configuration |
astrid keys show|generate |
Manage signing keys |
astrid-core: Foundation types, Frontend trait, input classification.astrid-capsule: Manifest-first composite engine orchestrating WASM, MCP, and static context.astrid-approval: Security interceptor, budget tracking, and allowance system.astrid-capabilities: Signed authorization tokens with glob-based resource patterns.astrid-audit: Chain-linked audit log with SurrealKV persistence.astrid-workspace: Workspace boundaries and escape approval.astrid-mcp: MCP client implementation.astrid-tools: Built-in core runtime tools.astrid-cli/astrid-gateway: CLI binary and background daemon implementation.
# Build
cargo build --workspace
# Test
cargo test --workspace -- --quiet
# Format check
cargo fmt --all -- --check
# Clippy (pedantic, no unsafe, no integer overflow)
cargo clippy --workspace --all-features -- -D warningsAll crates enforce #![deny(unsafe_code)]. There is no unsafe Rust anywhere in the codebase. Clippy runs at pedantic level, and integer arithmetic overflow is a compile error.
Dual-licensed under MIT and Apache 2.0.
Copyright (c) 2026 Joshua J. Bouw and Unicity Labs.