Skip to main content
Every substrate a region touches — secrets, queue, inference, storage, runtime — is behind a driver interface in @usenaive/region-core. Services never import a provider SDK directly; they call a driver resolved from a NAIVE_*_DRIVER env var. The interfaces are 1:1 with the cloud implementations they abstract, so the cloud code paths port to a Tower without behavior change.

The five selectors

Defaults come from resolveDriverSelection(): with NAIVE_BUILD=tower the base is the Tower set, otherwise the cloud set. Any individual NAIVE_*_DRIVER value overrides its base. (There are also kv and cron drivers — both Postgres on a Tower — used for the relay cursor and scheduled sweeps.)
The runtime pool is a selectable driver, not a fixed dependency. The cloud default is the ECS warm pool; a Tower runs local (docker-compose service instances). Neither is privileged by the code.

NAIVE_SECRETS_DRIVER — the Credential Vault

The SecretsDriver is envelope encryption. Regardless of driver the stored value layout is identical — nonce(12) || tag(16) || ciphertext, AES-256-GCM, with an encryption context of exactly { company_id, tenant_user_id } — so vault_entries rows are portable between substrates. generateDataKey / decryptDataKey / reEncryptDataKey are the required methods; decryptDataKey must reject on a mismatched context. reveal is optional.
Reveal is absent on vaultd (Tower). reveal? is an optional method on SecretsDriver; the vaultd driver does not implement it and vaultd compiles the method out entirely (towerbuild). The vault reveal route therefore returns 501 on a Tower — plaintext never leaves the box. kms and file implement reveal; vaultd does not. See Security model and Vault encryption.

NAIVE_QUEUE_DRIVER — durable queues

sqs (cloud) and pg (Tower) implement the same QueueDriver: create/delete, send (with FIFO group id + content dedup + delay), receive with a visibility timeout, delete, purge, and attributes. The pg driver implements pgmq semantics on plain SQL (SKIP LOCKED, read_ct, archive) rather than requiring the pgmq extension — a documented deviation, since FIFO group-key withholding needs custom SQL either way. It runs on stock postgres:16, does FIFO group-withholding + content dedup, and moves a message to the DLQ at maxReceive = 5.

NAIVE_INFERENCE_DRIVER — chat completions

An OpenAI-compatible InferenceDriver (non-streaming, streaming, and listModels). Both the llm primitive and the agent proxy (/v1/proxy/*) go through the same router (packages/api/src/services/llm.tsdrivers.ts), so a box has one inference source, not two.

The three box inference modes

On a Tower the mode (persisted per box in boxes.inference_mode, chosen in the dashboard Settings → Inference or the onboarding wizard) decides where model calls actually go. The effective mode resolves as env override (NAIVE_INFERENCE_MODE / NAIVE_LLM_FORWARD / NAIVE_INFERENCE_DRIVER) → the persisted choice → cloud. If a box is set to cloud but no forward is wired, it degrades to the local provider driver (BYO key) rather than failing — so cloud is safe as a default.
Honest hardware feasibility for local models. Running a model on the box needs real VRAM. A 4 GB Jetson Nano cannot serve a useful LLM — use cloud (credits) or byo. A Jetson Orin (16–64 GB unified memory) can serve a small quantized model (≈3–8B, AWQ/GGUF) at modest speed (expect seconds-per-response, not instant) — fine for light tasks, not for large-context or high-throughput work. Bigger models want a discrete NVIDIA GPU on an x86 box. Enable local inference with the opt-in vllm compose profile (docker compose … --profile vllm up -d) after setting NAIVE_INFERENCE_DRIVER=vllm
  • VLLM_MODEL (and a JetPack/ARM64 VLLM_IMAGE on Jetson).

NAIVE_STORAGE_DRIVER — object storage

StorageDriver = put / get / presigned URL / delete.
  • s3 (cloud) — AWS S3, real presigned URLs.
  • fs (Tower) — files under NAIVE_STORAGE_FS_ROOT (e.g. /data/storage). Fetch URLs are HMAC-signed and served by /storage/local/*key; it guards against path traversal.

NAIVE_RUNTIME_DRIVER — the runtime pool

RuntimePoolDriver is the spec’s “supervisor”: claim/release/get/list of runtime instances for tenants.
  • ecs (cloud) — the ECS warm pool (hot-claim an idle task).
  • local (Tower) — the box’s compose agent-runtime service (the real Hermes agent-container). It boots in POOL_MODE=idle; a claim HOT-CLAIMS it in place by POSTing /provision (identical contract to the ECS path — no restart), then records the binding so sidecarFetch/getContainerForSubject resolve it. Point the driver at it with NAIVE_LOCAL_RUNTIME_URL (default http://agent-runtime:3100) + CONTAINER_AUTH_TOKEN.
On a Tower each runtime is started with a per-operator NAIVE_RUNTIME_TOKEN and addresses the egress gateway as its only route off-box. The agent-container image is built for arm64 and amd64 (scripts/build-agent-container.sh).
Local inference. Set HERMES_LOCAL_MODEL (+ NAIVE_VLLM_URL) to point the box’s Hermes runtime at a local vLLM OpenAI-compatible endpoint — the CEO’s own reasoning then stays on-box, complementing the auto inference driver that keeps the llm primitive local too.

The GovernorClient contract

Drivers move bytes; they never make the trust decision. That decision is a wire call to the closed governor, and the only way to make it is GovernorClient / the governorCall() wrapper in @usenaive/region-core (pinned in PROTOCOLS.md §1).
  • Transport — localhost HTTP/1.1 + JSON to GOVERNOR_URL (default http://governor:8700), authenticated with a shared GOVERNOR_TOKEN bearer, ajv-validated on both ends.
  • Fail-closed is baked in. governorCall() bounds every call (500 ms connect / 2 s total). Governor unreachable or timeout throws GovernorUnavailableError; there is no verdict caching and no call site can default to library behavior. The API maps it to 403 enforcement_unavailable; the gateway maps it to deny-all.
  • A Verdict carries decision, a target of live | sandbox (whether an allowed action routes to real rails or the sandbox adapter), and — on freeze — an approval_id for a persisted pending row.

Writing a driver

  1. Implement the interface from @usenaive/region-core (e.g. SecretsDriver, QueueDriver). Match the exact method shapes; for secrets, preserve the nonce(12) || tag(16) || ciphertext layout and reject on context mismatch so vault_entries rows stay portable.
  2. Register it behind the selector. The api resolves drivers in packages/api/src/drivers.ts via lazy getters keyed on resolveDriverSelection(process.env); add your branch there (and export a create…Driver(opts) factory from your package, as the bundled drivers do).
  3. Never talk to the governor yourself except through GovernorClient — enforcement stays in the closed engine, and the fail-closed guarantee depends on the single wrapper.
  4. Add a conformance capability. Tag any leg that needs a real third-party key with hasCapability() so a missing key is reported BLOCKED, never mocked (see Conformance).
  5. Ship a README with the five-section template (Purpose / Consumers / Configuration / Run standalone / Place in the topology) — the docs gate checks it.
The set of selectors is the source of truth in packages/core/src/drivers.ts (DRIVER_ENV_VARS). A CI gate greps this page for every one: NAIVE_SECRETS_DRIVER, NAIVE_QUEUE_DRIVER, NAIVE_INFERENCE_DRIVER, NAIVE_STORAGE_DRIVER, NAIVE_RUNTIME_DRIVER.