Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ Single binary. No server dependencies. Runs on tokio. All data lives in embedded

**Stack:** Rust (edition 2024), tokio, Rig (v0.30.0, agentic loop framework), SQLite (sqlx), LanceDB (embedded vector + FTS), redb (embedded key-value).

## JavaScript Tooling (Critical)

- For UI work in `spacebot/interface/`, use `bun` for all JS/TS package management and scripts.
- **NEVER** use `npm`, `pnpm`, or `yarn` in this repo unless the user explicitly asks for one.
- Standard commands:
- `bun install`
- `bun run dev`
- `bun run build`
- `bun run test`
- `bunx <tool>` (instead of `npx <tool>`)

## Migration Safety

- **NEVER edit an existing migration file in place** once it has been committed or applied in any environment.
Expand Down
25 changes: 25 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ notify = "7"
# Cryptography (for secrets)
aes-gcm = "0.10"
sha2 = "0.10"
argon2 = "0.5"
rand = "0.9"

# UUID generation
Expand Down Expand Up @@ -165,6 +166,10 @@ unimplemented = "deny"
[dev-dependencies]
tokio-test = "0.4"

# OS keystore (macOS Keychain for master key storage)
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "3"

[profile.release]
lto = "thin"
strip = true
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,20 +242,35 @@ headers = { Authorization = "Bearer ${SENTRY_TOKEN}" }

### Security

Workers execute arbitrary shell commands and subprocesses on your behalf. Spacebot uses defense-in-depth to contain what those processes can do:
Spacebot runs autonomous LLM processes that execute arbitrary shell commands and spawn subprocesses. Security isn't an add-on — it's a layered system designed so that no single failure exposes credentials or breaks containment.

- **Process sandbox** — shell and exec tools run inside OS-level filesystem containment. On Linux, [bubblewrap](https://github.com/containers/bubblewrap) creates a mount namespace where the entire filesystem is read-only except the agent's workspace and any explicitly configured writable paths. On macOS, `sandbox-exec` enforces equivalent restrictions via SBPL profiles. No amount of LLM creativity can write outside the sandbox — it's kernel-enforced, not string-filtered
- **Workspace isolation** — file tools canonicalize all paths and reject anything outside the agent's workspace. Symlinks that escape the workspace are blocked
#### Credential Isolation

Secrets are split into two categories: **system** (LLM API keys, messaging tokens — never exposed to subprocesses) and **tool** (CLI credentials like `GH_TOKEN` — injected as env vars into workers). The category is auto-assigned based on the secret name, or set explicitly.

- **Environment sanitization** — every subprocess starts with a clean environment (`--clearenv` on Linux, `env_clear()` everywhere else). Only safe baseline vars (`PATH`, `HOME`, `LANG`), tool-category secrets, and explicit `passthrough_env` entries are present. System secrets never enter any subprocess
- **Secret store** — credentials live in a dedicated redb database, not in `config.toml`. Config references secrets by alias (`anthropic_key = "secret:ANTHROPIC_API_KEY"`), so the config file is safe to display, screenshot, or `cat`
- **Encryption at rest** — optional AES-256-GCM encryption with a master key derived via Argon2id. The master key lives in the OS credential store (macOS Keychain, Linux kernel keyring) — never on disk, never in an env var, never accessible to worker subprocesses
- **Keyring isolation** — on Linux, workers are spawned with a fresh empty session keyring via `pre_exec`. Even without the sandbox, workers cannot access the parent's kernel keyring where the master key lives
- **Output scrubbing** — all tool secret values are redacted from worker output before it reaches channels or LLM context. A rolling buffer handles secrets split across stream chunks. Channels see `[REDACTED]`, never raw values
- **Worker secret management** — workers can store credentials they obtain (API keys from account creation, OAuth tokens) via the `secret_set` tool. Stored secrets are immediately available to future workers

#### Process Containment

- **Process sandbox** — shell and exec tools run inside OS-level filesystem containment. On Linux, [bubblewrap](https://github.com/containers/bubblewrap) creates a mount namespace where the entire filesystem is read-only except the agent's workspace and configured writable paths. On macOS, `sandbox-exec` enforces equivalent restrictions via SBPL profiles. Kernel-enforced, not string-filtered
- **Dynamic sandbox mode** — sandbox settings are hot-reloadable. Toggle via the dashboard or API without restarting the agent
- **Workspace isolation** — file tools canonicalize all paths and reject anything outside the agent's workspace. Symlinks that escape are blocked
- **Leak detection** — a hook scans every tool argument before execution and every tool result after execution for secret patterns (API keys, tokens, PEM private keys) across plaintext, URL-encoded, base64, and hex encodings. Leaked secrets in arguments skip the tool call; leaked secrets in output terminate the agent
- **Library injection blocking** — the exec tool blocks dangerous environment variables (`LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, `NODE_OPTIONS`, etc.) that could hijack child process loading
- **SSRF protection** — the browser tool blocks requests to cloud metadata endpoints, private IPs, loopback, and link-local addresses
- **Identity file protection** — writes to `SOUL.md`, `IDENTITY.md`, and `USER.md` are blocked at the application level
- **Secret encryption** — credentials stored via the secrets system are encrypted at rest with AES-256-GCM
- **Durable binary storage** — `tools/bin` directory on PATH survives hosted rollouts. Workers are instructed to install binaries there instead of ephemeral package manager locations

```toml
[agents.sandbox]
mode = "enabled" # "enabled" (default) or "disabled"
writable_paths = ["/home/user/projects/myapp"] # additional writable dirs beyond workspace
passthrough_env = ["CUSTOM_VAR"] # forward specific env vars to workers
```

---
Expand Down Expand Up @@ -505,6 +520,8 @@ No server dependencies. Single binary. All data lives in embedded databases in a
| [Cortex](docs/content/docs/(core)/cortex.mdx) | Memory bulletin and system observation |
| [Cron Jobs](docs/content/docs/(features)/cron.mdx) | Scheduled recurring tasks |
| [Routing](docs/content/docs/(core)/routing.mdx) | Model routing and fallback chains |
| [Secrets](docs/content/docs/(configuration)/secrets.mdx) | Credential storage, encryption, and output scrubbing |
| [Sandbox](docs/content/docs/(configuration)/sandbox.mdx) | Process containment and environment sanitization |
| [Messaging](docs/content/docs/(messaging)/messaging.mdx) | Adapter architecture (Discord, Slack, Telegram, Twitch, Webchat, webhook) |
| [Discord Setup](docs/content/docs/(messaging)/discord-setup.mdx) | Discord bot setup guide |
| [Browser](docs/content/docs/(features)/browser.mdx) | Headless Chrome for workers |
Expand Down
54 changes: 35 additions & 19 deletions docs/content/docs/(configuration)/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,18 +167,33 @@ agent_id = "main"
channel = "webhook"
```

## Environment Variable References
## Value References

Any string value in the config can reference an environment variable with the `env:` prefix:
Any string value in the config supports three resolution modes:

| Prefix | Resolution | Example |
|--------|-----------|---------|
| `secret:` | Look up from the [secret store](/docs/secrets) | `"secret:ANTHROPIC_API_KEY"` |
| `env:` | Read from system environment variable | `"env:ANTHROPIC_API_KEY"` |
| _(none)_ | Literal value | `"sk-ant-..."` |

```toml
anthropic_key = "env:ANTHROPIC_API_KEY"
# From the secret store (recommended)
anthropic_key = "secret:ANTHROPIC_API_KEY"

# From an environment variable
openai_key = "env:OPENAI_API_KEY"

# Literal value (not recommended — use secret: or env: instead)
groq_key = "gsk_abc123..."
```

This reads `ANTHROPIC_API_KEY` from the environment at startup. If the variable is unset, the value is treated as missing.
The `secret:` prefix resolves from the agent's secret store at config load time. If the secret doesn't exist, the value is treated as missing and implicit env fallbacks are tried.

LLM keys also have implicit env fallbacks — if no key is set in the TOML, Spacebot checks `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `KILO_API_KEY`, and `OPENCODE_GO_API_KEY` automatically.

Use `POST /api/secrets/migrate` to automatically move plaintext credentials from `config.toml` into the secret store and replace them with `secret:` references. See [Secret Store -- Migration](/docs/secrets#migration) for details.

## Env-Only Mode

If no `config.toml` exists, Spacebot runs from environment variables alone:
Expand Down Expand Up @@ -241,7 +256,7 @@ Most config values are hot-reloaded when their files change. Spacebot watches `c

| Setting | Why |
|---------|-----|
| LLM API keys | Provider clients are initialized once |
| LLM API keys | Provider clients are initialized once (applies to `secret:`, `env:`, and literal values) |
| Messaging adapters (Discord token, webhook bind/port) | Adapter connections are long-lived |
| Agent topology (adding/removing `[[agents]]`) | Databases and event buses are per-agent |
| Database paths | Connections are opened once at startup |
Expand Down Expand Up @@ -295,6 +310,7 @@ System prompts (channel, branch, worker, compactor, cortex, etc.) are Jinja2 tem
│ ├── lancedb/ # vector search
│ ├── config.redb # key-value settings
│ ├── settings.redb # runtime settings (worker_log_mode, etc.)
│ ├── secrets.redb # secret store (categories, encryption)
│ └── logs/ # worker execution logs
└── archives/ # compaction transcripts
```
Expand Down Expand Up @@ -331,19 +347,19 @@ If you define a custom provider with the same ID as a legacy key, your custom co

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `anthropic_key` | string | None | Anthropic API key (or `env:VAR_NAME`) |
| `openai_key` | string | None | OpenAI API key (or `env:VAR_NAME`) |
| `openrouter_key` | string | None | OpenRouter API key (or `env:VAR_NAME`) |
| `kilo_key` | string | None | Kilo Gateway API key (or `env:VAR_NAME`) |
| `zhipu_key` | string | None | Zhipu AI (GLM) API key (or `env:VAR_NAME`) |
| `groq_key` | string | None | Groq API key (or `env:VAR_NAME`) |
| `together_key` | string | None | Together AI API key (or `env:VAR_NAME`) |
| `fireworks_key` | string | None | Fireworks AI API key (or `env:VAR_NAME`) |
| `deepseek_key` | string | None | DeepSeek API key (or `env:VAR_NAME`) |
| `xai_key` | string | None | XAI API key (or `env:VAR_NAME`) |
| `mistral_key` | string | None | Mistral API key (or `env:VAR_NAME`) |
| `opencode_zen_key` | string | None | OpenCode Zen API key (or `env:VAR_NAME`) |
| `opencode_go_key` | string | None | OpenCode Go API key (or `env:VAR_NAME`) |
| `anthropic_key` | string | None | Anthropic API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `openai_key` | string | None | OpenAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `openrouter_key` | string | None | OpenRouter API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `kilo_key` | string | None | Kilo Gateway API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `zhipu_key` | string | None | Zhipu AI (GLM) API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `groq_key` | string | None | Groq API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `together_key` | string | None | Together AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `fireworks_key` | string | None | Fireworks AI API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `deepseek_key` | string | None | DeepSeek API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `xai_key` | string | None | XAI API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `mistral_key` | string | None | Mistral API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `opencode_zen_key` | string | None | OpenCode Zen API key (`secret:NAME`, `env:VAR_NAME`, or literal) |
| `opencode_go_key` | string | None | OpenCode Go API key (`secret:NAME`, `env:VAR_NAME`, or literal) |

#### Custom Providers

Expand All @@ -361,7 +377,7 @@ name = "My Provider" # Optional - friendly name for display
|-------|------|----------|-------------|
| `api_type` | string | Yes | API protocol type. One of: `anthropic`, `openai_completions`, `openai_chat_completions`, `openai_responses`, `gemini`, or `kilo_gateway` |
| `base_url` | string | Yes | Base URL of the API endpoint. Must be a valid URL (including protocol) |
| `api_key` | string | Yes | API key for authentication. Supports `env:VAR_NAME` syntax to reference environment variables |
| `api_key` | string | Yes | API key for authentication. Supports `secret:NAME` and `env:VAR_NAME` syntax |
| `name` | string | No | Optional friendly name for the provider (displayed in logs and UI) |

> Note:
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/(configuration)/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "Configuration",
"pages": ["config", "sandbox", "permissions"]
"pages": ["config", "secrets", "sandbox", "permissions"]
}
2 changes: 1 addition & 1 deletion docs/content/docs/(configuration)/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ But NOT:

```
~/.spacebot/agents/{other_agent}/ # other agents' data
~/.spacebot/config.toml # instance config (contains API keys)
~/.spacebot/config.toml # instance config (secret references, not plaintext keys)
/etc/, /home/, /Users/ # system paths
```

Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/(configuration)/sandbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ passthrough_env = ["GH_TOKEN", "GITHUB_TOKEN", "NPM_TOKEN"]

Each listed variable is read from the parent process environment at subprocess spawn time and injected into the worker's environment. Variables not in the list are stripped.

When the secret store is available, `passthrough_env` is redundant -- credentials should be stored in the secret store, which injects tool secrets automatically. The field is additive and continues to work alongside the store.
When the [secret store](/docs/secrets) is available, `passthrough_env` is redundant -- credentials should be stored in the secret store, which injects tool secrets automatically. The field is additive and continues to work alongside the store.

## Durable Binaries

Expand Down Expand Up @@ -216,6 +216,7 @@ The sandbox is one layer in a defense-in-depth model:
| **Exec env var blocklist** | Blocks `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, etc. | Exec tool |
| **Leak detection** | Regex scan of all tool output for secret patterns | All tools via SpacebotHook |
| **Output scrubbing** | Exact-match redaction of known secret values | Worker output, status updates, OpenCode events |
| **[Secret store](/docs/secrets)** | Categorized credential storage, config resolution, tool secret injection | All agents |
| **Permissions system** | Application-level tool access control | All tools |

The sandbox and permissions system are complementary. The [permissions system](/docs/permissions) controls which tools an agent can use and what paths the LLM is allowed to access at the application level. The sandbox enforces filesystem boundaries at the kernel level for subprocesses that are allowed to run.
Loading