Skip to content

Latest commit

 

History

History
313 lines (236 loc) · 16.7 KB

File metadata and controls

313 lines (236 loc) · 16.7 KB

Symbiont — Agent Instructions

Symbiont (Symbi) is a Rust-native, zero-trust agent framework for building autonomous, policy-aware AI agents. Part of the ThirdKey trust stack: SchemaPinAgentPinSymbiont.

Project Structure

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

Build and Test

cargo build --workspace
cargo test --workspace
cargo clippy --workspace
cargo fmt --check

All four commands must pass before committing. Clippy must produce zero warnings.

Code Style

  • Rust edition 2021
  • Run cargo fmt before committing
  • Run cargo clippy --workspace and 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) — .dsl is supported indefinitely for backward compatibility. Use dsl::is_symbi_file / dsl::strip_symbi_extension for file discovery instead of inlining extension checks. New scaffolding emits .symbi only.

Commit Guidelines

  • Write concise commit messages focused on the "why"
  • No mention of AI assistants or co-authoring in commit messages
  • Use date command to determine the current date when adding dates to docs

Local models

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.1

The 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.

Security

  • 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

Docker

  • 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*

Releasing

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

OSS Sync

Private repo is on Gitea. Public mirror is github.com:ThirdKeyAI/Symbiont.git.

bash scripts/sync_oss_to_github.sh --force

The script exits with code 1 during cleanup even on success — this is a known quirk.

DSL Quick Reference

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.

Sandbox Tiers (all OSS)

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.ext4

symbi init validates both paths exist before writing symbiont.toml. symbi doctor reports whether runsc and firecracker binaries are reachable.

Hosted execution: E2B (not a tier)

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.

Managed CLI agents (Mode B)

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/not policies/run/, which this surface does not read — or SYMBI_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 on SYMBIONT_MANAGED);
  • loads the plugin via --plugin-dir (resolve order: --plugin-dir flag, SYMBIONT_CLAUDE_PLUGIN_DIR, then sibling-repo autodetect) and wires the stdio symbi mcp back-channel via --mcp-config --strict-mcp-config;
  • bounds the run with --max-turns (primary, cooperative) and --budget-timeout (hard wall-clock backstop; CliExecutor kills 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.

ToolClad Tools

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.

Agent Delegation (chat coordinator)

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 Completed each produce an error observation.
  • The sub-agent is offered the coordinator's read-only monitoring tools, via CoordinatorExecutor's ActionExecutor::tool_definitions impl. It is not offered delegate (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_executor is wired into symbi run and the DSL builtins, not the coordinator, so a delegated agent cannot reach them either.
  • Conversion of a delegate tool call into a delegation only happens when the runner holds a delegation handle. Runners that implement their own delegate tool (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.

MCP Server

Start with symbi mcp (stdio transport). Available tools:

  • invoke_agent — Run a named agent with a prompt via LLM
  • list_agents — List all agents in the agents/ directory
  • parse_dsl — Parse and validate DSL content (file or inline)
  • get_agent_dsl — Get raw agent definition source (.symbi or legacy .dsl) for a specific agent
  • get_agents_md — Read the project's AGENTS.md file
  • verify_schema — Verify MCP tool schema via SchemaPin (ECDSA P-256)

HTTP API

The runtime API runs on port 8080 (configurable via --port):

  • GET /api/v1/health — Health check (no auth)
  • GET /api/v1/agents — List agents
  • POST /api/v1/agents — Create agent
  • POST /api/v1/agents/:id/execute — Execute agent
  • GET /api/v1/schedules — List cron schedules
  • POST /api/v1/schedules — Create schedule
  • GET /api/v1/channels — List channel adapters
  • POST /api/v1/workflows/execute — Execute workflow
  • GET /api/v1/metrics — Runtime metrics
  • GET /swagger-ui — Interactive API docs

All endpoints except health require Authorization: Bearer <token>.

Agent Capabilities

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

Trust Stack

Symbiont is part of the ThirdKey cryptographic trust chain:

  1. SchemaPin — Tool schema verification. Ensures MCP tool schemas haven't been tampered with by verifying ECDSA P-256 signatures against publisher-hosted public keys.
  2. AgentPin — Domain-anchored agent identity. Binds agent identities to DNS domains via .well-known/agentpin.json, enabling cross-runtime trust.
  3. Symbiont — The agent runtime. Executes policy-aware agents with sandbox isolation, integrating SchemaPin for tool trust and AgentPin for agent identity.