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
symbi run and symbi up accept any OpenAI-compatible endpoint, so a local
model works with no cloud key:
export OPENAI_API_KEY=ollama
export OPENAI_BASE_URL=http://localhost:11434/v1
export CHAT_MODEL=llama3.1The SSRF guard is deliberately not applied to these operator-supplied base
URLs — they are configuration at the same trust level as the key beside them.
It stays on every attacker-influenced destination (ToolClad HTTP backends,
SchemaPin key discovery). Both the URL check and the SSRF-filtering DNS
resolver had to be lifted for this path; see
net_guard::customise_operator_client.
- 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.)
For validation rather than inspection, use symbi dsl --check -f <file>: one
line per file and an exit code, so it can gate CI. The bare form prints the
full parse tree, which is for debugging, not for checking.
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.