Skip to main content
@usenaive-sdk/iac is the build-time, declarative half of the SDK. You author a naive.config.ts, and naive up sets it up. The split is declaration vs execution: defineProject(...) runs when the config file is evaluated — it validates and refuses, and makes no API call — and naive up is the execution step that plans and applies what the config declares. A refusal from defineProject therefore surfaces anywhere the file is evaluated (naive up, naive up --plan, or your own build), before anything is applied. The entrypoint is defineProject — strict: declaring a field nothing consumes is a named refusal at define time (declared_unconsumed: <path> …) instead of a green apply that enforces nothing. defineConfig remains as a permanent @deprecated lenient alias with the behavior every existing config was written against; naive up prints what such a config declared that nothing reads. Nothing needs migrating — switching is never urgent, only strictly better — but new configs should use defineProject. See Strict mode for the exact list and a real refusal, and the migration guide for moving an existing config over. naive up issues no per-tenant resources — no KYB, no card issuing, no carrier call until forUser(id).provision(role). It is not side-effect free, though, and two of its effects are company-scoped and durable:
  • company.brains is provisioned. Each declared brain becomes a knowledge base, and the declared default is promoted. naive up reports each one as created or converged. These survive naive down — a brain is the company’s accumulated knowledge, and tearing down a deployment does not mean discarding it.
  • declared teams: are materialized and placed. naive up stamps out each team’s agents and reports per-team placement (placed / converged / refused) and per-agent provision_status. A refused team makes naive up exit non-zero.
Read result.teams[].placement and result.brains rather than inferring either from the exit code alone.

The shape

A config has two halves:
  • infrastructure — set up once for the whole project: cloud (web, database, storage) and business (the real-world programs agents draw from).
  • agents — a blueprint per role. naive.forUser(id).provision(role) stamps out a fresh, isolated, governed copy per user.
Two more blocks are additive: company: { brain | brains, residency, governance } and teams: { … } declare the durable-runtime surface (see Teams). Declaring either turns on define-time validation for those blocks only. Write brain: brain({ … }) for one, brains: brains({ default, declared }) for several — writing both is company_brain_and_brains at define time (the brain has the whole shape). runtime.pool() (below) is the legacy runtime; on the team surface it is runtime.hermes()pool is optional: omitted means the company’s one hosted-hermes container, and a given name must match a pool declared under the config’s top-level runtime: block (an unknown name is hermes_pool_unknown at define time). The current runtime is runtime.durable({ … }) — which is also what a team with no runtime: line gets, so choosing hermes is always an explicit act. One member can be pinned on its own with agent({ runsOn }); see Choosing a runtime. Every existing config keeps compiling.

More than one project

project: is the project this config applies to — the same layer naive projects manages, and a plain string is one project. An organization that needs several declares them under projects:, and naive up plans and applies each in turn:
A block declared on a project replaces the root block of the same name; a block it omits is inherited. Omit projects: entirely and nothing changes — the single-string form is exactly what it always was.

Strict mode — what defineProject refuses

defineProject is defineConfig plus one pass: every field on the accept-and-drop list that the config declares is refused at define time. The list is the set of fields the DSL accepts but nothing yet consumes — declared, dropped (or carried and read by nothing), and enforced nowhere. Under the lenient defineConfig they apply green and naive up warns; under defineProject they refuse before anything is applied:
The refused-until-wired fields today: agent().time, agent().secrets, agent().skills, team().time, runtime.durable({ workspace }), company.residency, and the top-level modules: block. Wiring a field deletes its row from the one shared list, which removes the refusal in the same edit — strictness only shrinks as consumers land. Separate from strictness, fields that ARE consumed — but only by one runtime — are not refused on the other: a hermes-led team declaring agent({ model }) or team({ review }) applies, and the plan/apply reports each such field in the team’s unconsumed list (e.g. review (runtime.hermes)). See Model, rubric and spend.

Builders

The agent block

How it’s enforced (not just declared)

When you provision(role), the blueprint is translated into a dedicated AccountKit for that agent and the agent is bound to it:
  • can → an allowlist: only those skills are enabled; the governance gateway refuses anything else with 403.
  • limits.approveescalate-only human-in-the-loop gates on the listed skills (a matching action parks for approval; a non-match never removes a built-in default).
  • limits.budget → a real combined cost ceiling at the gateway. It caps BOTH real-world spend (card limits, top-ups, trading notional) AND platform usage (LLM, search, compute, hosted runtime), summed for the window. A hard cap returns 403 budget_exceeded (race-safe — reserved under a lock before execution); a soft cap routes to human approval; the alert threshold emits a budget.alert event. A multi-agent system shares one cap across all its agents. If a hard cap (or the company’s credit balance) is exhausted, the agent’s hosted runtime is auto-stopped so you stop incurring cost.
So an agent whose can omits skills.trading returns 403 on forUser(id).trading.*, and a budget of $1 (hard) makes a $250 card return 403 budget_exceeded — both verified end-to-end.
limits.budget binds on both transports; can and limits.approve do not, quite. The combined ceiling is enforced inside the agent-profile context, which is bound around every HTTP request and around every MCP tool call, so a capped agent is capped whichever way it calls. The capability allow-list and the approval requirement are decided by the governor, and on MCP the governor is consulted by 28 of the 271 tools — the money and identity verbs. A can rule about, say, search or image generation is enforced on the HTTP route and not on the equivalent MCP tool. Detail: the governance gateway.
For fine-grained control you can still drop to the lower-level agentTemplate({ identity, wallet, comms, policy }) + policy({ allow, deny, approvals, autoApprove, network }) builders under agentProfiles — the agent({...}) DSL compiles down to exactly that.

Account kits in the config

kits: is the canonical spelling of “declare now, instantiate later”. A kit is a role your config registers once; nothing runs until your own code stamps an instance of it for one child project — the same account-kit rows the SDK already manages.
kit() takes the same fields as agent() (including runsOn — a kit may pin the runtime its instances run on), and compiles to byte-for-byte the same canonical output. Relationship to agents: — the top-level agents: block has always meant exactly this, so it stays as a deprecated alias with identical behavior: both spellings compile to the same output, and naive up prints a rename notice for agents:. Declaring the same name in both blocks is refused at define time (kit_and_agent_duplicate). Rename the block to kits:; nothing else changes.

Multi-agent systems

A system(...) composes a parent/manager agent (which holds the shared budget) with sub-agents, each in its own isolated runtime. Constraint: root must be the agent role named "ceo". The hosted runtime runs a single Hermes gateway pinned to the ceo profile, and dashboard / naive ceo run dispatch through it. A root under any other name would be created and governed but never receive CEO work — naive up rejects that with invalid_input. Give the CEO whatever title/skills you want; only the role name is fixed.
Instantiate at run-time:
Spend aggregates against the parent’s cap at the gateway; revoke(parent) kills the whole system.

How naive up sets up infrastructure

naive up is a plan → apply lifecycle backed by a Naïve-managed executor (no IaC toolchain for you to operate). Each resource maps to a managed backend, which Naïve provisions and keeps reconciled for you:
  • cloud.bucket → a private object-storage bucket.
  • cloud.postgres → a managed Postgres database (created async; the pooled DATABASE_URL is returned as an output once healthy).
  • cloud.webweb hosting. naive up uploads the resource’s dir and triggers a production deployment; env is auto-injected from sibling outputs (the Postgres DATABASE_URL, the bucket name).
  • business.* → the shared real-world programs (entity/email/phone/cards); each agent draws its own instance from them at provision() time.
Resources provision in dependency order (db + storage before web), are advanced idempotently, and reconciled in the background — exactly like agent provisioning.
Apply is asynchronous. A real apply can take minutes (container provisioning, profile staffing), so the API accepts it as a job — 202 { job_id } — and the CLI polls GET /v1/jobs/:id until the job completes, then prints the same apply report a synchronous run would. Only one apply runs per company at a time; a second naive up while one is in flight answers 409 duplicate_request with the running job to poll. (Direct API callers: send Prefer: respond-async, or ?async=true, to opt in; a plain POST /v1/deployments/:project/apply stays synchronous.)

What naive up prints beside the plan

Three notices ride on the plan and the apply output, so a lenient config still hears about everything strict mode would have refused:
  • Per-team unconsumed lists — one warning line per team that declared fields nothing on its runtime reads: ⚠ team "support" DECLARED AND UNREAD: …. Do not treat those fields as in force.
  • A legacy-surface deprecation notice when the config still uses runtime.pool() declarations, agent-level runtime: "<pool>" strings, or systems:. All three keep working indefinitely; new configs author defineProject with teams: on runtime.durable().
  • A rename notice when the config declares agents: — a deprecated spelling, not a deprecated surface: the block compiles to exactly what kits: compiles to.
Credit-gated. Infrastructure scales with what you can pay for: when your credits are exhausted you can’t provision more, and running infrastructure is shut down (web disabled, runtime/compute stopped; database/storage data persists).