This document describes the threat model, trust boundaries, and security controls for CorvidAgent. It is intended for operators, contributors, and security reviewers.
CorvidAgent runs as a local sandbox -- on your machine, in a VM, or on a private server. It is NOT a public web service. External communication channels are AlgoChat (Algorand blockchain messaging), Telegram (long-polling bridge), and Discord (WebSocket gateway bridge).
┌──────────────────────────────────────────────────────┐
│ Your Machine / VM / Sandbox │
│ │
│ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Dashboard│<-->│ CorvidAgent API │ │
│ │ (browser)│ │ localhost:3000 │ │
│ └──────────┘ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ AlgoChat Telegram Discord │
└──────────────┼──────────────┼──────────────┼─────────┘
│ │ │
┌─────────▼──────┐ ┌───▼────┐ ┌──────▼───┐
│ Algorand Node │ │Telegram│ │ Discord │
│(on-chain ident)│ │ API │ │ Gateway │
└────────────────┘ └────────┘ └──────────┘
The API server, agent sessions, database, and wallet keystore all reside within the same trust boundary. All outbound connections are initiated by the server -- there are no inbound connections except HTTP/WebSocket requests to the API port.
| Asset | Storage | Protection |
|---|---|---|
| Wallet private keys | Encrypted keystore (~/.corvid-agent/keystore/) |
AES-256-GCM encryption at rest |
| Agent mnemonic phrases | Keystore JSON files | Encrypted with WALLET_ENCRYPTION_KEY or server mnemonic |
API keys (API_KEY, DASHBOARD_API_KEY, ADMIN_API_KEY) |
.env file |
File permissions; timing-safe comparison at runtime |
| Database | corvid-agent.db (SQLite) |
WAL mode, parameterized queries, tenant-scoped access |
| Credit ledger | credits + credit_transactions tables |
Atomic mutations with TOCTOU-safe WHERE guards |
| Provider API keys (Anthropic, OpenAI, GitHub) | .env file |
Never logged, never sent to agents, file-permission protected |
| User sessions and conversation history | SQLite sessions + messages tables |
Scoped by session ID; tenant isolation in multi-tenant mode |
| AlgoChat encryption keys | Derived via X25519 key agreement | ChaCha20-Poly1305 authenticated encryption; keys never stored plaintext |
Malicious external users. Send prompt injections via AlgoChat messages, Telegram messages, Discord messages, or GitHub issue/PR comments. Goal: trick an agent into executing unauthorized commands, leaking secrets, or modifying protected files.
Compromised agents. An agent session that has been jailbroken through prompt injection or model exploitation. May attempt to read protected files, escalate to admin privileges, exfiltrate environment variables, or modify spending controls.
Network attackers. Attempt to intercept API traffic between the dashboard and server, perform MITM on non-TLS connections, or eavesdrop on bridge traffic. Mitigated by localhost-only default binding and optional TLS termination via reverse proxy.
Supply chain attackers. Compromised npm dependencies introduced through bun install. Could inject malicious code into the runtime. Mitigated by lockfile pinning (bun.lock) and dependency review on updates.
Rogue tenants. In multi-tenant mode, a tenant attempts to access another tenant's sessions, agents, credits, or configuration. Mitigated by tenant-scoped database queries enforced via db-filter.
| Surface | Entry Point | Protections |
|---|---|---|
| HTTP API | Port 3000 | API key auth (timing-safe), sliding-window rate limiting, CORS origin enforcement, content-length guard |
| WebSocket | /ws upgrade |
Bearer auth on upgrade handshake, rate limiting, 401 rejection on failed auth |
| AlgoChat | Algorand blockchain | Cryptographic identity (Ed25519 signatures), owner address allowlist, PSK encryption (X25519 + ChaCha20-Poly1305) |
| Telegram bridge | Long-polling | Bot token authentication, per-user session isolation |
| Discord bridge | WebSocket gateway | Bot token authentication, per-user session isolation |
| MCP tools | Agent sessions | Protected file enforcement (basename + path matching + symlink resolution), bash command scanning |
| Database | Local SQLite file | WAL mode, foreign keys, parameterized queries only (no string interpolation), tenant-scoped queries via db-filter |
| Agent sessions | Claude Agent SDK | Prompt injection scanner (heuristic, <10ms), spending caps, credit system with TOCTOU-safe atomic mutations |
| GitHub integration | Webhooks + polling | Social engineering scanner on comments, external URL detection, new-domain blocking in diff validation |
| Directory browsing | /api/browse-dirs |
Sandboxed to home directory + registered project roots + ALLOWED_BROWSE_ROOTS; path traversal blocked |
┌─────────────────────────────────────────────────────────────┐
│ TRUSTED ZONE (operator-controlled) │
│ │
│ .env corvid-agent.db wallet keystore │
│ (secrets) (all app data) (encrypted keys) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Server Process │ │
│ │ │ │
│ │ ┌───────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ API Layer │ │ Bridges │ │ Agent Sessions │ │ │
│ │ │ (authed) │ │ (TG/DC) │ │ (sandboxed SDK) │ │ │
│ │ └───────────┘ └──────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
├─────────────────────── TRUST BOUNDARY ──────────────────────┤
│ │
│ SEMI-TRUSTED (authenticated but untrusted content) │
│ - AlgoChat messages (signed, possibly adversarial content) │
│ - Telegram/Discord user messages (authed, adversarial) │
│ - GitHub webhook payloads (verified, adversarial content) │
│ - Dashboard requests (API-key authed) │
│ │
├─────────────────────── TRUST BOUNDARY ──────────────────────┤
│ │
│ UNTRUSTED │
│ - Unauthenticated HTTP requests │
│ - Raw network traffic │
│ - npm dependencies (pinned but not audited per-commit) │
│ - AI model outputs (may contain injected instructions) │
└─────────────────────────────────────────────────────────────┘
- Identity: Every message is signed by an Algorand private key. You prove who you are cryptographically -- no passwords, no JWTs.
- Owner commands:
/stop,/approve,/deny,/mode,/work,/agent,/council-- restricted to addresses inALGOCHAT_OWNER_ADDRESSES. - Allowlist: Optional -- restrict which addresses can message your agents.
- Encryption: Messages are encrypted with X25519 key agreement and ChaCha20-Poly1305 authenticated encryption (via ts-algochat PSK mode).
- All non-health HTTP routes require
Authorization: Bearer <key>whenAPI_KEYis set. - WebSocket upgrades require a valid key via query parameter (
?key=) or Bearer header. - Key comparison uses constant-time comparison to prevent timing side-channels.
- Localhost mode (default):
BIND_HOST=127.0.0.1-- no API key required. Only local processes can reach the API. - Network mode: Set
API_KEYin.env. The server refuses to start ifBIND_HOSTis non-localhost and noAPI_KEYis set.
When binding to a non-localhost address, the server auto-generates a strong ADMIN_API_KEY if one is not already set. This key is required for administrative operations (settings, key rotation, tenant management).
API keys can be rotated via POST /api/settings/api-key/rotate. A 24-hour grace period allows the old key to continue working, preventing lockouts during rolling deployments.
- Dashboard auth guard: Validates API key on all dashboard-facing routes.
- Admin role guard: Restricts sensitive paths (settings, key management, user administration) to admin-level keys.
- Tenant role guard: In multi-tenant mode, enforces that requests can only access resources belonging to their tenant.
Configure ALLOWED_ORIGINS (comma-separated) to restrict browser access. Defaults to * on localhost.
The agent has real ALGO in its wallet. These safeguards prevent runaway spending:
| Protection | Where | Default |
|---|---|---|
| Daily ALGO cap | server/db/spending.ts |
10 ALGO/day |
| Per-message cost check | AlgoChat bridge | Checks before send |
| Credit system | server/db/credits.ts |
Guest-only; owners bypass credits |
Configure via DAILY_ALGO_LIMIT_MICRO in .env.
- Atomic mutations: All credit operations (grants, deductions) are wrapped in SQLite transactions.
- TOCTOU prevention: Deductions use
UPDATE ... WHERE balance >= costguards to prevent race conditions between balance checks and mutations. - Grant validation: Only positive integer amounts are accepted. Negative grants are rejected at the validation layer.
- Per-turn tracking: Each agent turn records its credit cost in the
credit_transactionstable for auditability. - Per-message deduction: AlgoChat and bridge messages deduct credits before sending, failing gracefully if the balance is insufficient.
- Agent sub-wallets are encrypted at rest (
server/lib/crypto.ts) using AES-256-GCM with PBKDF2 key derivation (600,000 iterations). - Encryption key is derived from
WALLET_ENCRYPTION_KEYenv var. On localnet, a default key is used for development convenience. - On testnet/mainnet,
WALLET_ENCRYPTION_KEYmust be explicitly configured (>= 32 chars). There is no fallback. - Persistent keystore in
~/.corvid-agent/keystore/survives database rebuilds. File permissions are enforced (0o600). - Mnemonic phrases are never logged, never exposed via API, and never included in agent session context.
- Wallet operations (sign, send) happen server-side only -- agents request transactions through MCP tools, never handling raw keys.
- In-memory mnemonic cache re-encrypts with ephemeral keys (5-minute TTL) so plaintext never lingers in the heap.
- Key rotation is supported via
bun run migrate:keysfor re-encrypting all mnemonics with a new passphrase.
If you previously used ALLOW_PLAINTEXT_KEYS=true on mainnet, this escape hatch has been removed as of #924. All mainnet deployments now require:
-
Set
WALLET_ENCRYPTION_KEYwith a strong passphrase (>= 32 chars):# Generate a 256-bit hex key openssl rand -hex 32 -
Remove
ALLOW_PLAINTEXT_KEYSfrom your environment — it is deprecated and ignored. -
Use secret management for
ALGOCHAT_MNEMONICandWALLET_ENCRYPTION_KEY:- Docker secrets: mount as files in
/run/secrets/ - HashiCorp Vault: inject via
vault agent - AWS Secrets Manager: inject via ECS task definition or Lambda env
- systemd: use
LoadCredential=directive
- Docker secrets: mount as files in
-
Rotate encryption keys if you suspect compromise:
WALLET_ENCRYPTION_KEY="current-key" \ WALLET_ENCRYPTION_KEY_NEW="$(openssl rand -hex 32)" \ bun run migrate:keys
-
Verify configuration without making changes:
bun run migrate:keys --check
All inbound messages from external sources (AlgoChat, Telegram, Discord, GitHub) pass through a heuristic prompt injection scanner before reaching agent sessions.
The scanner evaluates six pattern categories:
- Role override attempts -- "ignore previous instructions", "you are now", system prompt leaks
- Encoding evasion -- Base64-encoded payloads, Unicode homoglyphs, zero-width characters
- Tool abuse instructions -- Attempts to instruct the agent to call specific tools or access files
- Data exfiltration prompts -- Requests to output environment variables, API keys, or file contents
- Social engineering -- Urgency markers, authority claims, impersonation of operators
- Delimiter injection -- Fake XML/JSON boundaries, system message markers
| Level | Meaning | Action |
|---|---|---|
| LOW | Single weak signal | Log only |
| MEDIUM | Multiple weak signals or one strong signal | Log and flag in audit trail |
| HIGH | Strong injection indicators | Block message, notify operator |
| CRITICAL | Clear, unambiguous injection attempt | Block message, kill session if in-progress, notify operator |
- 3 MEDIUM detections from the same source within a sliding window escalate to HIGH.
- 2 HIGH detections from the same source escalate to CRITICAL.
GitHub issue and PR comments are additionally scanned for social engineering patterns: suggested code containing fetch() to new domains, requests to add API keys, or instructions disguised as helpful contributions. Violations block PR creation in the work task pipeline.
Scanner executes in under 10ms per message and does not block the event loop.
A sliding-window rate limiter tracks requests per IP address (or per wallet address for AlgoChat). Requests exceeding the window limit receive HTTP 429 responses with Retry-After headers.
Sensitive endpoints have tighter rate limits than read-only routes:
- Mutation endpoints (POST, PUT, DELETE): Lower limits to prevent abuse.
- Read endpoints (GET): Higher limits for dashboard responsiveness.
- Authentication endpoints (key rotation, login): Strictest limits to prevent brute-force.
/api/health-- Always exempt (monitoring probes).- Webhook endpoints -- Exempt from IP-based limiting (validated by payload signature).
- Loopback IPs (
127.0.0.1,::1) -- Exempt when running in localhost mode, since only local processes can reach the API.
- Rotate immediately via
POST /api/settings/api-key/rotate(the old key remains valid for 24 hours). - Review the audit log for unauthorized requests made with the compromised key.
- If the admin key was compromised, restart the server with a new
ADMIN_API_KEYin.env. - Revoke any active sessions created during the compromise window.
- Kill the session immediately via the dashboard or
POST /api/sessions/:id/stop. - Review the session's message history and audit log for tool calls.
- Check for file modifications: inspect git status in any worktrees the session had access to.
- Verify protected files are unmodified (
spending.ts,sdk-process.ts,manager.ts,schema.ts, etc.). - If the agent created a work task, inspect the branch diff before any merge.
- Check the
credit_transactionstable for unexpected deductions or grants. - Review the daily ALGO spending in
spending.tslogs againstDAILY_ALGO_LIMIT_MICRO. - Temporarily set
DAILY_ALGO_LIMIT_MICRO=0to halt all ALGO spending. - Audit AlgoChat transaction history on the Algorand explorer for the agent's address.
- Isolate the affected tenant by disabling their API key.
- Review
db-filterquery logs to identify any queries that bypassed tenant scoping. - Check the
tenant_memberstable for unauthorized role assignments. - Audit session ownership to confirm no sessions were accessed across tenant boundaries.
- Rotate all API keys for affected tenants.
- Pin all dependencies in
bun.lock(already enforced). - Diff
bun.lockagainst the last known-good commit to identify changed packages. - Run
bun audit(or equivalent) to check for known vulnerabilities. - If a compromised package is identified, roll back to the previous lockfile and restart.
- Review server logs for unexpected outbound network connections.
If you run CorvidAgent on a public server instead of localhost:
- Set
API_KEYin.env-- the server enforces Bearer auth on all routes when this is set. The server will refuse to start if bound to a non-localhost address without anAPI_KEY. - Set
BIND_HOST=0.0.0.0for Docker or VM deployments where you need external access. - Add a reverse proxy (nginx/caddy) for TLS termination. Without TLS, API keys and session data transit in cleartext.
- Set
ALGOCHAT_OWNER_ADDRESSESto restrict admin commands to your Algorand address(es). - Use the AlgoChat allowlist to control who can message your agents.
- Set
ALLOWED_ORIGINSto restrict CORS to your domain(s). - Set
ALLOWED_BROWSE_ROOTSto limit directory browsing scope. - Review
DAILY_ALGO_LIMIT_MICROand credit grants for appropriate spending bounds.
Why localhost-only by default? Binding to
127.0.0.1ensures only local processes can reach the API without needing an API key, which is the safest default for single-machine deployments.
The deploy/ directory has example configs for systemd, Docker, and macOS launchd.
If you discover a security vulnerability, do not open a public issue.
Instead, please report it privately via GitHub Security Advisories or email the maintainers directly.
Include:
- A description of the vulnerability and its potential impact
- Steps to reproduce
- Any suggested fix (optional but appreciated)
We aim to acknowledge reports within 48 hours and provide a fix or mitigation plan within 7 days.