Injection-resilient, multi-tenant agentic RAG with non-custodial on-chain action-prep.
Orca answers questions over an ingested knowledge base with cited evidence, reads live
on-chain data, and prepares — never submits — transactions the user signs in their own
wallet. Boardwalk is the first tenant; the core is a generic
platform behind a TenantAdapter contract.
The design premise: an LLM will eventually obey instructions planted in retrieved content, so don't trust it not to. Safety is deterministic code, not model alignment — a taint gate that makes obedience not matter for the action path.
flowchart LR
W["Widget · Playground · Telegram bot"] --> R
subgraph serve ["Serving — TypeScript"]
R["Deterministic router"] --> L["Agent loop<br/>+ taint gate"]
end
subgraph ingest ["Ingestion — Python engine"]
C["Connectors<br/>docs_mdx · openapi"]
end
DB[("Neon Postgres<br/>pgvector + FTS · RLS")]
C --> DB
L -->|searchKnowledge| DB
L -->|"getLaunch — live read"| API["Boardwalk API"]
L -->|"prepare* — action tools"| TX["Unsigned tx bundle"]
TX --> U["User signs in their own wallet"]
Retrieved content carries a trust tier (tier-0 first-party docs → tier-2 uploads and external content). The moment a tool returns content at or above the taint tier, the turn is tainted — and while tainted, action tools are removed from the set the model is even offered. Taint is monotonic and enforced in code the model can't argue with. A live tainted turn:
router tier=tier2 reasons=["action-verb","live-lexicon"]
step 1 actionToolsActive=true → searchKnowledge → taintedAfter=true (tier-2 chunk retrieved)
step 2 actionToolsActive=false ← action tools left the model's reach
final tainted=true no signable action surfaced; answer grounded in tier-0 sources
Defense in depth behind the gate: spotlighted/datamarked untrusted chunks, schema-enforced field provenance (an action address must appear verbatim in the user's typed message), non-custodial by construction, an egress allowlist, RLS tenant isolation, and spend caps. The full threat model, prior art (CaMeL, spotlighting), and stated boundaries — including what is not defended — are in SECURITY.md.
Injection resistance, end to end against the live serving path (details and caveats in SECURITY.md and packages/bench/RESULTS.md):
| Measurement | Result |
|---|---|
Deterministic gate floor — model scripted to obey the injection (poison-asr.test.ts, per PR) |
action ASR 0 |
| AgentDojo attack wrappers × claim-to-attacker goal, ingested tier-2, wallet attached | action-hijack ASR 0/5, gate engaged 5/5 |
Live red-team (promptfoo) vs a running /v1/chat |
5/5 passed |
| Own-corpus PoisonedRAG-style sweep — injected instructions, up to 8/8 retrieval flooding | ASR 0 |
| Same sweep — injected facts with no true counter-source in the corpus | ASR ~1.0 — undefended, documented as the boundary |
Retrieval quality (BEIR, the production spine: hybrid RRF → voyage rerank-2.5):
| nDCG@10 | Orca hybrid | Orca + rerank | best published single-model first stage |
|---|---|---|---|
| SciFact | 0.802 | 0.807 | 0.784 — NV-Embed-v1 (7B) |
| FiQA-2018 | 0.565 | 0.629 | 0.631 — NV-Embed-v1 (7B) |
apps/api— Next.js route handlers (/v1/chat,/v1/search), the Telegram bot webhook,/playground(port 3000)apps/web— admin console (better-auth): tenant settings, knowledge, usage, feedback (port 3001)packages/core— router, agent loop + taint gate, tools, retrieval, guardrailspackages/db— Drizzle schema (the cross-language contract), RLS policy,withTenantpackages/tenant-boardwalk— the Boardwalk tenant adapter (system prompt + tools) + its evalspackages/widget— embeddable React chat widget + a dependency-free typed stream clientpackages/bench— BEIR benchmarks + the injection/poison-resistance harnesspackages/config— shared tsconfig, the zod env contract, the model catalogevals/redteam— live red-team (promptfoo + AgentDojo) with committed resultsengine/— Python (uv) connector framework:docs_mdx,openapi
Node 22 (.nvmrc), pnpm via corepack, Python via uv.
pnpm install
pnpm lint:ci # biome lint + format (what CI runs)
pnpm -r typecheck && pnpm -r test # TS (pglite in-memory Postgres; no services needed)
node scripts/lint-tenant-access.mjs # tenant tables only reachable via withTenant
cd engine && uv run pytest # Python, offlineLive paths need apps/api/.env.local + engine/.env (see .env.example):
an AI Gateway key and two Neon URLs — DATABASE_URL (serving) whose role must be
non-owner + NOBYPASSRLS (the owner role bypasses RLS, making tenant isolation a no-op),
and ADMIN_DATABASE_URL (owner role) used only for migrations and role setup, never for
serving. Boot fails loud if the serving role can bypass RLS.
pnpm --filter @orca/db exec tsx src/migrate.ts # ADMIN_DATABASE_URL: schema + RLS + serving role
pnpm --filter @orca/db exec tsx scripts/seed-dev.mts # dev tenant + knowledge base
cd engine && uv run engine ingest docs_mdx && uv run engine ingest openapi
pnpm --filter @orca/api dev # http://localhost:3000/playground