Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

BoundaryChoiceProperty it buysWhere
Identity at the edgeRust gateway (BFF) with Keycloak OIDC + PKCEsmall, memory-safe, auditable code at the most exposed hop; opaque sessionsParts I, III
Capability surfaceMCP 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 seesParts I, III, IV, V
Deterministic policyRust interpreter over a JSON specificationpolicy reviewed as data, validated at boot; exhaustive matchingPart III
Domain authorityElixir + Ash: declarative resources, actions, policies, AshStateMachineone definition of who may do what, enforced identically for UI, REST and AIPart V
Secret handlingRust with purpose-typed keys, zeroisation, SQLCiphermisuse of one key for another purpose does not compile; bounded secret lifetimesPart IV
Agent loop and railsPython, NeMo Agent Toolkit + NeMo Guardrailsa mature ReAct runtime with native tool calling and railsParts I, III
Durable runtimeTypeScript, LangGraph.js, SQLite, one Node processsmall enough to read end to end; explicit graph, checkpoints in one filePart II
InvariantsPostgreSQL constraints, unique indexes, row locks, triggersguarantees that hold even if application code is wrongParts I, III, V
ObservabilityOpenTelemetry → MLflowone trace per request, joined across the agent and railsParts 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 in agent/config.yml as the bearer token for the MCP server. With approvals enabled, approval.py reads HITL_APPROVAL_SECRET and 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.