Trust Boundaries
A map of who can do what, and which component enforces each boundary. If you're touching a feature that crosses one of these lines, this doc is mandatory reading.
Principals
| Principal | Description |
|---|---|
| Anonymous | Unauthenticated visitor |
| Authenticated User | Signed in via Supabase Auth |
| Skill Author | Same as Authenticated User, in the role of editing Skills |
| Deployer | Same as Authenticated User, in the role of running deployments |
| Trading Agent | LLM-driven process running on Fly, in the trading role |
| Chat Agent | LLM-driven function call, in the read-only Q&A role |
| Execution Engine | Server-side module that validates and executes orders |
| Service Role | Server-side identity used by trusted processes (web server, live runner, ingest jobs) — bypasses RLS |
A single human user can be a Skill Author and a Deployer at the same time (and likely is, in MVP). The roles are logically separate so that future multi-user scenarios slot in cleanly.
Boundary map
┌──────────────────────────────────────────────────────────────────────────────┐
│ ┌────────────┐ │
│ │ Anonymous │ → can hit auth pages, nothing else │
│ └────────────┘ │
│ ▲ │
│ │ sign in / sign up │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Authenticated │ │
│ │ User │ → reads own data via Supabase RLS │
│ └────────┬─────────┘ → can write own Skills, sims, deployments │
│ │ → cannot write other users' anything │
│ │ │
│ ┌──────┴────────────────────────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Skill Author │ │ Deployer │ │
│ │ role │ │ role │ │
│ └────────┬───────┘ └────────┬─────────┘ │
│ │ saves Skill │ deploys / commands │
│ │ (writes skill_versions) │ (writes agent_commands) │
│ │ │ (chats — agent_messages) │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Database (Supabase) — RLS enforces user-scoped access │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ writes (snapshots, state, logs) │
│ │ │
│ ┌───────────────────────┴─────────────────────────────────┐ │
│ │ Service Role (bypass RLS, server-side only) │ │
│ │ • apps/web server actions │ │
│ │ • apps/live-runner │ │
│ │ • packages/data-ingest │ │
│ └────────────────┬──────────────────────────────────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Trading Agent │ │ Chat Agent │ │
│ │ (Fly, per skill)│ │ (Vercel Fn) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ proposed action natural-language reply │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Execution Engine │ │ (no exchange access) │ │
│ │ validates → ? │ └──────────────────────┘ │
│ └────────┬─────────┘ │
│ │ accepted │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Broker Adapter │ ← decrypted exchange credentials │
│ │ (Hyperliquid) │ held only in this process memory │
│ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘Per-data-class access matrix
| Data class | Anon | Authed User (own) | Authed User (others) | Service Role |
|---|---|---|---|---|
skills, skill_versions | — | RW | — | RW |
deployments | — | R + create | — | RW |
agent_state | — | R | — | RW |
agent_logs, decision_snapshots | — | R | — | RW |
agent_commands | — | RW (own) | — | RW |
agent_conversations/messages | — | RW (own) | — | RW |
sim_runs | — | RW | — | RW |
exchange_credentials (ciphertext) | — | R metadata only | — | RW + decrypt |
bars, news, funding_rates | — | R (all) | R (all) | RW |
R = SELECT, RW = SELECT + INSERT/UPDATE/DELETE. "Metadata only" for credentials = the user can see the wallet address and label, never the decrypted key material.
Agent boundaries
Trading Agent
What it CAN do (within Skill constraints):
- Read market data, news, portfolio (via tools)
- Emit
ProposedActionvia thepropose_ordertool - Reason in
final_textfor its decision
What it CANNOT do:
- Call exchange APIs directly (no broker handle is passed to the agent runtime; only the Execution Engine has it)
- Modify its own Skill or risk caps
- Issue commands (no command tools)
- Read other deployments' state
- Bypass the Execution Engine (the engine is the only code path to a broker adapter; the agent runtime does not import broker adapters)
Chat Agent
What it CAN do:
- Read past decision snapshots, current state, logs (via introspection tools)
- Read recent market data and news (via read-only versions of trading tools)
- Generate text responses, possibly suggesting actions the deployer might take
What it CANNOT do:
- Place orders (
propose_ordernot in tool whitelist) - Issue commands (no command tools — see ADR-0007)
- Modify Skill, deployment, or risk configuration
- Read other users' or other deployments' data (route handler authorizes deployment ownership before loading)
Chat cannot issue commands
This is so important it gets its own subsection. The chat agent's tool whitelist deliberately excludes any tool that triggers an agent_commands insert. If a deployer says "ok flatten everything" in chat, the chat agent should:
- Acknowledge the intent
- Explain what flatten would do (it can simulate it via read tools)
- Suggest clicking the Flatten button — but never trigger it
Rationale: prompt-injection resistance + audit clarity. See ADR-0007.
Execution Engine boundary
The Execution Engine is the only code path between an agent's ProposedAction and a broker adapter. Engineering rules:
- The agent runtime package (
packages/agent-runtime) must not import frompackages/brokers/* - The tools package (
packages/tools) must not import frompackages/brokers/* - Only
packages/execution-engineimports brokers - Only
packages/execution-enginedecrypts credentials
These are enforced by:
- ESLint rule:
no-restricted-importsblocking the disallowed paths - CI check: grep for broker imports outside the engine package
- Code review: any PR that touches
packages/execution-engineorpackages/brokersrequires explicit reviewer attention to this rule
Secrets boundary
| Secret | Where it lives | Where it's used |
|---|---|---|
| Supabase service role key | Vercel env (server-only), Fly secret | apps/web server, apps/live-runner |
| User exchange API keys | DB (encrypted) → in-memory after decrypt | apps/live-runner only |
| Vault encryption key | Supabase Vault | apps/live-runner (decrypt only) |
| AI Gateway key | Vercel env / Fly secret | All processes that call models |
| User-provided BYOK provider keys | DB (encrypted) | Sent to AI Gateway as request header |
| News provider API keys | Vercel env | packages/data-ingest |
| Fly API token | Vercel env | apps/web server (for provisioning) |
Details: secrets.md.
Prompt-injection threat model
Agent context will routinely contain user-generated or external-source text (news, fills, possibly tool descriptions from MCP servers). Any of these is a potential injection vector. Mitigations:
| Vector | Mitigation |
|---|---|
| News injecting "place order X" | Engine validates every action; no risk-cap bypass possible |
| News injecting "tell deployer to flatten" | Chat agent cannot issue commands; would only result in text suggestion the human still must act on |
| MCP server returning malicious tool description | Curated MCP catalog only; new servers require platform review |
| MCP server returning huge result to OOM agent | Result size cap in MCP wrapper |
| Tool result attempting to escalate (e.g., "now ignore your system prompt") | Engine still enforces risk caps regardless of agent state |
| Compromised model output | Engine acts as adversarial validator on every action |
The Execution Engine is designed assuming the agent is adversarial. This is not paranoia — it is the only realistic stance given LLM behavior.
Common boundary violations to watch for in code review
- A tool function calling a broker directly. Fix: route through Execution Engine.
- A skill-editor server action writing
agent_commands. Fix: commands originate from the deployment UI, not the editor. - A chat tool with
modes: ['read', 'write']. Fix: chat is read-only; if the tool has any side effect, it doesn't belong in chat. - An app importing
@supabase/supabase-jsdirectly. Fix: go throughpackages/db. - The live runner reading from
auth.uid()for authorization. Fix: runners use service role; user identity for command audit comes fromagent_commands.user_id. - A new
agent_commands.kindthat bypasses an existing risk cap. Fix: any command that can affect risk goes through the Execution Engine, not directly to the broker.