Symbiont (Symbi) is a Rust-native, zero-trust agent framework for building autonomous, policy-aware AI agents. Part of the ThirdKey trust stack: SchemaPin → AgentPin → Symbiont.
- Docs: https://docs.symbiont.dev
- Repo: https://github.com/ThirdKeyAI/Symbiont
- Crate: https://crates.io/crates/symbi
crates/
├── dsl/ # Symbi DSL parser with Tree-sitter integration
├── runtime/ # Agent runtime (scheduling, routing, sandbox, AgentPin)
├── channel-adapter/ # Slack, Teams, Mattermost adapters
├── repl-core/ # Core REPL engine
├── repl-proto/ # JSON-RPC wire protocol types
├── repl-cli/ # Command-line REPL interface
├── repl-lsp/ # Language Server Protocol implementation
src/ # Unified `symbi` CLI binary
cargo build --workspace
cargo test --workspace
cargo clippy --workspace
cargo fmt --checkAll four commands must pass before committing. Clippy must produce zero warnings.
- Rust edition 2021
- Run
cargo fmtbefore committing - Run
cargo clippy --workspaceand fix all warnings before committing - Inline tests in source files using
#[cfg(test)] mod tests - ES256 (ECDSA P-256) only for AgentPin identity — reject all other algorithms
- Agent files use
.symbi(canonical) —.dslis supported indefinitely for backward compatibility. Usedsl::is_symbi_file/dsl::strip_symbi_extensionfor file discovery instead of inlining extension checks. New scaffolding emits.symbionly.
- Write concise commit messages focused on the "why"
- No mention of AI assistants or co-authoring in commit messages
- Use
datecommand to determine the current date when adding dates to docs
- Zero-trust by default: all inputs are untrusted
- Cryptographic audit trails for agent actions
- Policy engine enforces runtime constraints via the Symbi DSL
- AgentPin integration for domain-anchored agent identity
- SchemaPin integration for tool schema verification
- Private keys (
*.private.pem,*.private.jwk.json) must never be committed
- Image:
ghcr.io/thirdkeyai/symbi:latest - Base:
rust:1.88-slim-bookworm(builder),debian:bookworm-slim(runtime) - The Dockerfile uses dependency caching with stub sources; cleanup globs must catch
libsymbi*and.fingerprint/symbi*
See .claude/RELEASE_RUNBOOK.md for the full release process, including:
- How to determine which crates need version bumps
- Cross-crate version reference update checklist
- CI verification steps before tagging
- Docker build cache pitfalls
- crates.io publish order
Private repo is on Gitea. Public mirror is github.com:ThirdKeyAI/Symbiont.git.
bash scripts/sync_oss_to_github.sh --forceThe script exits with code 1 during cleanup even on success — this is a known quirk.
Agent definitions live in agents/*.symbi (legacy .dsl is also recognized for backward compatibility). Key block types:
metadata { version "1.0", author "team", description "What this agent does" }
with { sandbox docker, timeout 30.seconds }
schedule daily_report { cron: "0 9 * * *", timezone: "UTC", agent: "reporter" }
channel slack_support { platform: "slack", default_agent: "helper", channels: ["#support"] }
webhook github_events { path: "/hooks/github", provider: github, agent: "deployer" }
memory context_store { store markdown, path "data/agents", retention "90d" }
Parse agent definitions with symbi dsl -f agents/<name>.symbi. (The symbi dsl subcommand name is intentionally preserved — it's a stable CLI surface, even though the file extension flipped.)
The tiers form a monotonically increasing host-isolation ladder:
| Tier | Backend | Selection | Prerequisites |
|---|---|---|---|
| tier0 | None (dev only) | with { sandbox = "none" } / SYMBIONT_ALLOW_UNISOLATED=1 |
— |
| tier1 | Docker | default | docker daemon |
| tier2 | gVisor (runsc) |
with { sandbox = "gvisor" } |
runsc registered as Docker runtime |
| tier3 | Firecracker microVM | with { sandbox = "firecracker" } |
firecracker binary + operator-supplied vmlinux + rootfs.ext4 |
All three host-isolation tiers ship in the OSS runtime — no "Enterprise" gating on gVisor or Firecracker. Per-agent tier comes from the DSL with { sandbox = "..." } block; project default lives in [sandbox] tier = "..." in symbiont.toml.
For Tier 3 setup (kernel + rootfs prep, in-VM init contract, hardening checklist), see docs/firecracker-setup.md. Scaffold a tier3 project with:
symbi init --profile assistant --sandbox tier3 \
--firecracker-kernel /path/to/vmlinux \
--firecracker-rootfs /path/to/rootfs.ext4symbi init validates both paths exist before writing symbiont.toml. symbi doctor reports whether runsc and firecracker binaries are reachable.
E2B is a separate hosted-cloud backend, not a peer of Tier 1/2/3. Code runs on E2B's infrastructure via their HTTPS API, so it carries no on-host isolation guarantees. Maps to SecurityTier::Hosted, which sorts below Tier1 — policies requiring host isolation (tier >= Tier1) will reject it.
| Backend | Selection | Prerequisites | Use cases |
|---|---|---|---|
| E2B (hosted) | with { sandbox = "e2b" } (DSL only — no --sandbox flag) |
E2B_API_KEY env var |
Quick-start demos, evaluation without setting up a sandbox host. Not for production workloads with privacy or compliance requirements. |
An agent whose metadata declares executor = "claude_code" is run by spawning a
governed Claude Code subprocess via crates/runtime/src/cli_executor (the
cli-executor feature, on by default) instead of the ORGA reasoning loop. The
reference agent is agents/code_reviewer.symbi; the path lives in
src/commands/managed_cli.rs.
symbi run code_reviewer --target <dir>:
- refuses to run at all unless the agent's metadata declares
allowed_tools(required — see below); - passes the spawn through the policy Gate (fail-closed; allow via Cedar in
policies/managed-cli/— notpolicies/run/, which this surface does not read — orSYMBI_INSECURE_ALLOW_ALL=1); - journals the child's tool calls live to
.symbiont/audit/mode-b-<session>.jsonl(see below); - injects the env handshake
SYMBIONT_MANAGED=true,SYMBIONT_SESSION_ID,SYMBIONT_BUDGET_TOKENS,SYMBIONT_BUDGET_TIMEOUT,CLAUDE_PROJECT_DIR(the symbi-claude-code plugin defers its hooks to the outer Gate onSYMBIONT_MANAGED); - loads the plugin via
--plugin-dir(resolve order:--plugin-dirflag,SYMBIONT_CLAUDE_PLUGIN_DIR, then sibling-repo autodetect) and wires the stdiosymbi mcpback-channel via--mcp-config --strict-mcp-config; - bounds the run with
--max-turns(primary, cooperative) and--budget-timeout(hard wall-clock backstop;CliExecutorkills with graceful SIGTERM → SIGKILL).
Do not pass --bare to the spawned claude — it skips reading ~/.claude
(credentials included) and breaks subscription auth.
One Gate decision authorizes the whole session, not each action inside it.
The policy Gate evaluates the spawn itself; it has no way to evaluate the
child's individual tool calls afterward, and whatever permission_mode resolves
to applies for the session's full lifetime. Per-action gating would require the
child to call back into Symbiont's Gate — a trust-boundary redesign, explicitly
out of scope. The only in-session restriction is the child's own
--allowedTools allowlist, sourced from the agent's DSL
metadata { allowed_tools = "Tool1,Tool2,..." } — that is the child's
allowlist, not Symbiont's Gate, and it is required: run_claude_code in
src/commands/managed_cli.rs refuses to spawn when it is empty rather than
handing the child its own unrestricted defaults for the whole run. There is
no bypass flag for this check.
permission_mode is opt-in per agent, read from the same metadata block.
Unset omits --permission-mode, leaving the child its own default, which still
prompts for anything outside allowed_tools; an agent that must run unattended
declares permission_mode = "dontAsk" and takes that trade-off explicitly. It
is deliberately not defaulted — a hardcoded dontAsk is a blanket grant no
agent asked for.
The session is journalled because it cannot be gated. The child runs with
--output-format stream-json and CliExecutor's stdout line sink
(with_stdout_line_sink) appends each tool call, each tool result, and a
closing summary (turn count, permission denials) to
.symbiont/audit/mode-b-<session>.jsonl as they happen. Live, not at exit: a
run killed by the wall-clock timeout never returns its buffered stdout, so a
post-hoc parse would lose the trail precisely when it matters. Argument values
under keys like token/api_key/password are redacted and oversize arguments
truncated, so a Write call does not deposit a whole file into the audit log.
This is a visibility mechanism, not an enforcement one — it records what the
child did, it does not stop it.
Tools live in tools/<name>.clad.toml and are auto-discovered at startup by symbi up, the HTTP Input server, and symbi tools. The watcher (crates/runtime/src/toolclad/watcher.rs) hot-reloads on file changes — no restart needed.
The manifest carries everything: binary path, description, risk tier, human-approval flag, Cedar resource/action for policy evaluation, optional evidence-capture config. Cedar policies are auto-generated from manifest metadata via crates/runtime/src/toolclad/cedar_gen.rs. The ORGA Gate phase evaluates these before any tool invocation.
Argument types are validated in crates/runtime/src/toolclad/validator.rs. agent_summary is a best-effort defense-in-depth sanitizer for free text bound for a downstream prompt — not a load-bearing control. For a privileged downstream decision (routing, escalation, authorization), use typed enum args grounded in trusted context via Cedar, not free text: see crates/runtime/src/toolclad/decision.rs (route_grounded/decide_route), tools/submit_triage.clad.toml, and examples/policies/triage_routing.cedar. Mark decision-feeding args with feeds_decision = true; ToolClad manifest validation (validate_toolclad) flags free-text args that feed a privileged decision.
Adding a new tool does not require Rust code. Drop a .clad.toml in tools/, the runtime picks it up.
MCP backend (mcp-client feature). A manifest can carry an [mcp] block (server, tool, optional field_map) to route the tool to an upstream MCP server over stdio instead of a local binary. Servers are declared in mcp-config.toml (per-project, then ~/.symbiont/). Invocation is SchemaPin-verified fail-closed by default (TOFU key pinning; a post-pin key swap is rejected); ToolCladExecutor::with_mcp_verification(false) opts out for local dev. This is how symbi run and the DSL reason()/tool_call() builtins execute real tools — see docs/mcp-tools.md.
The symbi up chat coordinator advertises a delegate tool listing the agents
found in ./agents. Calling it resolves the target in a name→prompt registry
(both the DSL-declared name and the filename stem are registered), runs it as a
bounded sub-loop (crates/runtime/src/reasoning/delegation_executor.rs), and
returns its reply as a tool result correlated to the originating call id.
Bounds and current limits, all worth knowing before relying on it:
- Depth is capped (
max_delegation_depth, default 3) with cycle detection; both guards reject before the target runs. - Failures are explicit: unknown target, cycle, depth exceeded, policy denial, or
a sub-agent that does not reach
Completedeach produce an error observation. - The sub-agent is offered the coordinator's read-only monitoring tools, via
CoordinatorExecutor'sActionExecutor::tool_definitionsimpl. It is not offereddelegate(no target registry of its own), so nested delegation is not reachable from a sub-loop today even though the depth guard allows it. It gets no knowledge bridge, so no retrieval. - The sub-loop runs under an id derived from the target's name
(
delegated_agent_id), so its policy decisions and journal entries are attributable and a Cedar policy can name the principal. - Sub-loop token usage is recorded on the delegation handle but has no reader, so the operator-visible token count excludes it. Each hop inherits the parent's configured ceiling rather than its remaining budget.
- The sub-loop's journal is not surfaced to the operator.
- The chat surface cannot run ToolClad/MCP tools:
build_tool_executoris wired intosymbi runand the DSL builtins, not the coordinator, so a delegated agent cannot reach them either. - Conversion of a
delegatetool call into a delegation only happens when the runner holds a delegation handle. Runners that implement their owndelegatetool (symbi-shell) keep receiving it as a plain tool call.
delegate names three different mechanisms across the tree — see the table in
SKILL.md before assuming which guarantees apply.
Start with symbi mcp (stdio transport). Available tools:
invoke_agent— Run a named agent with a prompt via LLMlist_agents— List all agents in theagents/directoryparse_dsl— Parse and validate DSL content (file or inline)get_agent_dsl— Get raw agent definition source (.symbior legacy.dsl) for a specific agentget_agents_md— Read the project's AGENTS.md fileverify_schema— Verify MCP tool schema via SchemaPin (ECDSA P-256)
The runtime API runs on port 8080 (configurable via --port):
GET /api/v1/health— Health check (no auth)GET /api/v1/agents— List agentsPOST /api/v1/agents— Create agentPOST /api/v1/agents/:id/execute— Execute agentGET /api/v1/schedules— List cron schedulesPOST /api/v1/schedules— Create scheduleGET /api/v1/channels— List channel adaptersPOST /api/v1/workflows/execute— Execute workflowGET /api/v1/metrics— Runtime metricsGET /swagger-ui— Interactive API docs
All endpoints except health require Authorization: Bearer <token>.
Agents defined in the Symbi DSL can:
- Invoke LLMs (OpenRouter, OpenAI, Anthropic) with policy-governed prompts
- Use skills (verified via SchemaPin cryptographic signatures)
- Run in sandboxed environments — choose Tier 1 (Docker), Tier 2 (gVisor), or Tier 3 (Firecracker) per agent (all OSS host-isolation tiers); E2B is a separate hosted-cloud backend opt-in via the DSL
- Operate on cron schedules with timezone support
- Connect to chat platforms (Slack, Teams, Mattermost) as channel adapters
- Receive webhooks (GitHub, Stripe, Slack, custom) with signature verification
- Maintain persistent memory stores with hybrid search (vector + keyword)
- Enforce runtime policies (allow, deny, require, audit)
- Produce cryptographic audit trails for all actions
Symbiont is part of the ThirdKey cryptographic trust chain:
- SchemaPin — Tool schema verification. Ensures MCP tool schemas haven't been tampered with by verifying ECDSA P-256 signatures against publisher-hosted public keys.
- AgentPin — Domain-anchored agent identity. Binds agent identities to DNS domains via
.well-known/agentpin.json, enabling cross-runtime trust. - Symbiont — The agent runtime. Executes policy-aware agents with sandbox isolation, integrating SchemaPin for tool trust and AgentPin for agent identity.