Company4 min read

defineProject: strict, honest agent infrastructure as code

naive.config.ts gets a strict entrypoint. defineProject refuses any declared field nothing consumes, kits: becomes the canonical declare-now-instantiate-later block, and every team runs durable by default — hermes only when you say so.

Read the docs →

/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
/defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject /defineproject
Company
TL;DR
  • defineProject is the new naive.config.ts entrypoint: declaring a field nothing consumes is a named refusal at define time, never a green apply that enforces nothing.
  • defineConfig stays forever as the lenient alias nothing breaks, and naive up now tells lenient configs exactly what they declared that nothing reads.
  • kits: is the canonical spelling of declare-now-instantiate-later; the top-level agents: block is a deprecated alias that compiles to byte-for-byte the same output.
  • Every team runs on the durable runtime unless you say otherwise; runtime.hermes() is an explicit, per-team (or per-member, via runsOn) choice.
  • The legacy surface runtime.pool(), systems:, agentProfiles — keeps working indefinitely, with one honest deprecation line on naive up.

Infrastructure as code has one job: what the file says is what the platform does. The naive.config.ts DSL grew fast enough that this stopped being fully true — you could declare a field, apply green, and have nothing anywhere enforce it. Today's release closes that gap. It is not a new surface; it is the existing one made honest.

The problem: a green apply that enforces nothing

A declarative DSL accumulates fields faster than consumers land for them. That is normal. What is not acceptable is the failure shape it produces: you write agent({ time: … }), the config validates, naive up applies, and the calendar bound you think you declared is enforced nowhere — dropped before it ever reaches the wire. Nothing fails, so nothing tells you.

We went through the DSL field by field and sorted every declarable into three bins:

  • Wired — consumed and enforced. can, limits, governance (narrowing only), brain bindings, environment, model, concurrency, maxSteps, team review rubrics and spend/hardSpend walls.
  • Consumed by one runtime — real on the durable lane, reported honestly on the other. A hermes-led team declaring agent({ model }) is told model (runtime.hermes) in its unconsumed list, not pretended into force.
  • Consumed by nothing yetagent().time, agent().secrets, agent().skills, team().time, runtime.durable({ workspace }), company.residency, modules:. These are the bin the old behavior hid.

defineProject: refusal over silence

defineProject is the new entrypoint, and the whole difference is what happens to that third bin. Declare a field nothing consumes and the config refuses at define time — when the file is evaluated, before anything is applied:

Error: declared_unconsumed: company.residency is declared and NOT YET ENFORCED —
nothing consumes it, so it would apply green and enforce nothing. Remove the field
(reinstate it when a consumer lands), or keep this config on defineConfig, the lenient
legacy alias.

defineConfig stays, permanently, as the lenient alias — every existing config keeps compiling and keeps producing the same output. And lenient no longer means silent: naive up now prints a per-team unconsumed list for everything declared that nothing reads, so both entrypoints tell the truth; they differ only in whether the truth is a warning or a refusal.

The list is one shared value in the codebase. Wiring a field deletes its row, which removes the refusal, the warning, and the honesty marker in the same edit — strictness can only shrink as consumers land, never lag behind them.

Kits: declare now, instantiate later

The top-level agents: block always had a slightly wrong name. It never provisioned anyone — it declared a role, and your code stamped instances later. That concept already has a noun on this platform: an account kit. So the block is now spelled kits::

import { defineProject, kit, skills } from "@usenaive-sdk/iac";
 
export default defineProject({
  project: "acme",
  kits: {
    sdr: kit({
      instructions: "Research prospects and draft outreach for one customer.",
      can: [skills.search, skills.email],
      limits: { budget: "$50/mo" },
    }),
  },
});
// your app, at run time — one governed instance per child project
const instance = await naive.forProject("acme").forChild(childId).provision("sdr");

kits: and agents: are one namespace: both compile to byte-for-byte the same canonical output, agents: gets a rename notice from naive up (a notice, never a refusal), and the same name in both blocks is refused at define time. Agents declared inside a team are different — they are the team's roster, materialized by naive up itself, not instantiated later.

Durable by default, hermes by choice

A team that says nothing about its runtime runs on the durable runtime — the current lane, with governance decided in-process at the tool-call boundary. Choosing the frozen hosted-container lane is always an explicit act:

teams: {
  research: team({           // no runtime: line — this is runtime.durable()
    lead: agent({ instructions: "Plan the week's briefs." }),
    agents: { analyst: agent({ instructions: "Write one brief." }) },
  }),
  support: team({
    runtime: runtime.hermes(),   // explicit — the frozen hosted lane
    lead: agent({ instructions: "Triage the ticket and delegate." }),
    agents: { tier1: agent({ instructions: "Answer the ticket." }) },
    edges: [["lead", "tier1"]],
  }),
}

One member can be pinned elsewhere with agent({ runsOn }) — a hermes team can keep a durable specialist. The reverse is refused at define time (a hermes agent is a profile inside the company's single container, and a durable-led team owns no container to put it in), and so is an edges: pair whose ends sit on different runtimes: delegation is a shared board, the two runtimes do not share one, and a card that applies and never arrives is exactly the silent failure this release exists to remove.

One more honesty fix while we were in there: runtime.hermes({ pool }) looked like routing and never was. pool is optional — omitted means the company's one hosted container — and a given name must match a pool your config's runtime: block declares (hermes_pool_unknown otherwise). A valid name is recorded on the team's agent rows as metadata.config_pool; it does not change which container is claimed.

Nothing breaks

The legacy surface — runtime.pool() declarations, agent-level runtime: "<pool>" strings, systems:, agentProfiles — keeps compiling and keeps applying, indefinitely. What it gains is one deprecation line on naive up naming exactly what was found, and the same per-team unconsumed reporting everything else gets. When you want the path over, the migration guides walk it route by route — including the two-line defineConfig → defineProject switch.

The full DSL reference is in the IaC docs, and declaring teams — runtimes, brains, governance layers, spend walls — in the Teams docs. Rename one call, run naive up --plan, and read what your config has been declaring all along.

Frequently Asked Questions
What is defineProject?+
defineProject is the strict entrypoint for naive.config.ts in @usenaive-sdk/iac. It runs everything defineConfig runs — the same define-time validation and named refusals — plus one extra pass: any declared field that nothing on the platform consumes is refused at define time with a declared_unconsumed error naming the exact path, instead of being silently dropped by a successful apply.
Do I have to migrate off defineConfig?+
No. defineConfig is a permanent lenient alias — it keeps today's behavior byte for byte and is never removed. Switching is never urgent, only strictly better: you rename one call, run naive up --plan, and either it passes or it names each field to remove. There is no sunset date.
What is the difference between kits: and agents: in the config?+
They are one namespace with two spellings. kits: is the canonical one — an account kit is a role declared now and instantiated later, per child project, via naive.forProject(p).forChild(id).provision(kitName). The top-level agents: block has always meant exactly that, so it stays as a deprecated alias that compiles to identical output; naive up prints a rename notice, and declaring one name in both blocks is refused (kit_and_agent_duplicate).
Which runtime does a team run on?+
The durable runtime, unless you say otherwise. A team that declares no runtime: line gets runtime.durable(); runtime.hermes() puts a team on the frozen hosted-container lane, and one member can be pinned elsewhere with agent({ runsOn }). Mixed rosters are validated at define time: a durable-led team cannot carry a hermes member, and an edge between two runtimes is refused rather than accepted and never delivered.
What does pool mean on runtime.hermes()?+
It is a record, not a routing mechanism. pool is optional — omitted means the company's one hosted-hermes container. When given, the name must match a pool declared under the config's top-level runtime: block (an unknown name is hermes_pool_unknown at define time), and a valid name is recorded on the team's agent rows as metadata.config_pool. It does not change which container is claimed.
Is the legacy surface going away?+
No. runtime.pool() declarations, agent-level runtime strings, systems:, and agentProfiles all keep compiling and applying indefinitely. What changed is honesty: naive up prints one deprecation line naming what it found, and per-team unconsumed lists so nothing you declared is silently dropped. The migration guides cover the path over when you want it.
NT
Naïve Team

Building the agent-native backend.

Keep reading