Technology choices follow boundaries
Five projects, four implementation languages and three agent runtimes. That is not a lack of standardisation. Each choice serves the boundary its project is about. The ideas carry over to other stacks. The point of this chapter is which property each choice was bought for, so you can buy the same property elsewhere.
| Boundary | Choice | Property it buys | Where |
|---|---|---|---|
| Identity at the edge | Rust gateway (BFF) with Keycloak OIDC + PKCE | small, memory-safe, auditable code at the most exposed hop; opaque sessions | Parts I, III |
| Capability surface | MCP servers in a separate process (Rust rmcp; AshAI in Elixir) | the tool list is the capability; the credentials for the data (database, keys) stay in the tool service, and the agent runtime holds only a service credential that the model never sees | Parts I, III, IV, V |
| Deterministic policy | Rust interpreter over a JSON specification | policy reviewed as data, validated at boot; exhaustive matching | Part III |
| Domain authority | Elixir + Ash: declarative resources, actions, policies, AshStateMachine | one definition of who may do what, enforced identically for UI, REST and AI | Part V |
| Secret handling | Rust with purpose-typed keys, zeroisation, SQLCipher | misuse of one key for another purpose does not compile; bounded secret lifetimes | Part IV |
| Agent loop and rails | Python, NeMo Agent Toolkit + NeMo Guardrails | a mature ReAct runtime with native tool calling and rails | Parts I, III |
| Durable runtime | TypeScript, LangGraph.js, SQLite, one Node process | small enough to read end to end; explicit graph, checkpoints in one file | Part II |
| Invariants | PostgreSQL constraints, unique indexes, row locks, triggers | guarantees that hold even if application code is wrong | Parts I, III, V |
| Observability | OpenTelemetry → MLflow | one trace per request, joined across the agent and rails | Parts I, III |
Keeping credentials from the model is not keeping them from the runtime
It is tempting to read "the model holds no credentials" as "the agent process holds no credentials". They are different claims, and only the first is true in these projects:
- In Part I, the trusted NAT runtime holds
MCP_API_KEY, configured inagent/config.ymlas the bearer token for the MCP server. With approvals enabled,approval.pyreadsHITL_APPROVAL_SECRETand signs approval tokens with it. Part III works the same way, with approvals always on. - In Parts IV and V, the MCP client that the agent runs in holds an API key
(Arktos's
X-API-KEY, a Tauros agent key). Whoever holds that key acts as that wallet owner or that agent.
So the rule these systems follow is narrower and more precise:
- Secrets stay out of model-visible context and tool arguments. No prompt, tool description, tool result or argument carries a credential. Part I's trace processor also redacts credential headers before export.
- Trusted runtime components hold the credentials their responsibility needs. The agent runtime authenticates to the tool service. The approval signer signs. The tool service holds the database or key material.
- Prompt injection and runtime compromise are different threats. An injection changes what the model says and requests. The capability surface, policies and point-of-mutation checks contain it. A compromise of the runtime process or its environment changes what trusted code does. An attacker there can call tools with the runtime's credential. In Parts I and III they can also mint approval tokens: the HMAC secret is shared by the signer and the verifier.
- Backend verification has a defined trust model. The MCP server recomputes state, checks policy and enforces single use. A forged token therefore cannot apply an impossible or stale change. But the server accepts any correctly signed token as a human decision. Recomputation does not make a compromised signer harmless. Containing that threat takes ordinary engineering: a small signer, network segmentation, secret management, or an asymmetric or separately hosted signer. (Approvals — the trust model.)
Patterns behind the choices
Put the strongest guarantee in the lowest layer that can hold it. A type that cannot be constructed (Arktos's key purposes) beats a runtime check. A database constraint (Tauros's one approval per revision) beats an application check. An application check beats a prompt instruction. Every project places at least one guarantee as low as it can go and tests it by bypassing the layers above.
Make the authority-bearing code small and boring. The gateway, the MCP servers, the rules engine and the mutation paths are short, deterministic and heavily tested. The probabilistic parts (model, prompts, rails) are allowed to be fuzzy because nothing authoritative depends on them.
Use the framework's abstractions, but model your own concepts. Sophos needed runs although LangGraph has threads and checkpoints. Tauros's invoice revisions are a domain concept, not an ORM version table. A framework identifier is not a product concept.
Local-first is a property of data flows, not of where the model runs. Sophos lesson R8 enumerates the network paths that remain after the model is local. Apply the same audit to any "self-hosted" claim, including Arktos's custody model.
What the choices do not imply
- They are not endorsements of a single stack for production. Each project documents what it would need to change before production.
- Rust does not make a component secure. In Arktos, for example, the server listens on plain HTTP and expects TLS from the deployment.
- MCP is a protocol, not a security model. Its safety comes from what you expose and how callers are authenticated.