This is a Naive → Naive guide. The “vendor” you are migrating from is a previous version of
Naive. The URL you arrived at is the one every deprecated route, CLI group and dashboard banner
links to (
Link: <…>; rel="deprecation"), so bookmark it rather than the section index.runtime.pool()), a fixed CEO agent per company, a kanban board at /v1/tasks,
long-running goals at /v1/objectives, worker records at /v1/employees, schedules at
/v1/cron, and per-agent notes at /v1/memory. It runs real production tenants today and it
does that job.
It is now frozen. Frozen means something narrower and more useful than “deprecated”:
- Every route, CLI command and DSL symbol keeps answering, unchanged. No response shape
changes, nothing is deleted, and there is no
Sunsetdate on any of it — aSunsetheader would be a promise the platform has not made. Existing configs keep working. - It accepts no new capabilities. Anything built after the freeze lands on the durable runtime only. The gap will widen; it will not close.
- Its policy is declarative, not enforced in-process. In the legacy container the agent profile’s policy is a description the container is asked to respect. Enforcement happens at the gateway the container calls back through. The durable runtime decides at the tool-call boundary instead, which is why the two runtimes are different products rather than two deployments of one.
The legacy runtime answers to eight names in Naive’s own history — orchestration, warm pool,
instance pool, Hermes, sidecar, container runtime, hosted runtime, and just the
runtime. They are the same thing. A deprecation notice that named only one would leave seven
ways to keep talking about it, so the register lists all eight.
Read this before you plan the work
Concept map
Before / after: the core path
Declaring the runtime
- There is exactly one lead, structurally.
leadis its own slot, not an entry inagentsthat happens to be calledceo. You cannot declare two, and you cannot declare none. edgesis typed against the agents you declared. A typo in an edge is a compile error, not a run that silently never delegates.- The brain is a value you hand out.
acme.partition(…)andacme.view(…)are methods on the brain, so a team or agent binds the brain it was given. runtime.pool()is untouched. It keeps its name, its type and its output. Adopting the new block does not require deleting the old one.- The entrypoint is
defineProject. The strict spelling ofdefineConfig— a declared field nothing consumes refuses at define time instead of applying green. Its own two-line migration is a separate guide.
defineConfig returns your config by identity when it declares neither company nor teams.
The twenty define-time refusals the new blocks add run only over the new blocks, so adding a
teams: key cannot change how an existing config compiles.Sending work
Reading what happened
status and verification_status) that the same rows already carry:
Because the board status is a function of two columns rather than a column of its own,
?status= filters in memory on this build and the response says so with filtered_in_memory: true.
naive teams watch emits NDJSON — one object per line, resumable. It polls the run’s event
ledger; there is no SSE or WebSocket for runs on this build, and the events endpoint takes no
after cursor, so the cursor is client-side and the cost grows with ledger length rather than
with new events.Minimal viable migration
You do not have to move a tenant to start. In order, smallest first:1
Read your existing board through the new address
naive teams board <team> --tenant <tu> serves the same cards from the same tables as
/v1/tasks. Nothing is copied and nothing is migrated — this is a second address for
one collection, not a second collection. If the two disagree, that is a bug worth reporting.2
Declare the team beside the system you already have
Add a
teams: block to naive.config.ts and leave systems: in place. They coexist; the
new define-time refusals do not run over the legacy block.3
Move approvals to the new address
POST /v1/teams/{team}/tenants/{tenant}/approvals/{id}/decide is the one write that is live,
and it calls the same executeApproval / denyApproval the legacy route calls. One write
path, two addresses. Your existing approval integration keeps working either way.4
Read the honesty report before you plan anything larger
naive teams plan <team> --tenant <tu> reports digests, the fence, caps and attestation
parity for one (team, tenant). Fields it cannot compute are null beside a
*_unavailable_because sibling naming the reason — never a plausible-looking value.5
Stop here — an EXISTING tenant still cannot be cut over, and the blocker is not submit
POST …/submit is now served for a tenant on the durable runtime, so the step that
used to say “wait for submit” is done. And a tenant can now be put on that runtime — but
only a new one. Which runtime a tenant is on is company_containers.sidecar_url, and
the only function that points it at the Worker, registerVettaRuntime(), has exactly one
production caller: services/placement.ts, reached from naive up when a teams: block
declares runtime.durable(...) and the operator exports
NAIVE_DURABLE_CREDENTIAL_<TEAM> out of band. It is on no tenant-addressable route, and
POST …/migrate and POST …/rollback are both still 501.That path refuses precisely the case this guide is about. placeTenant permits only a
tenant with no company_containers row (placing destroys nothing) or one already
on the durable runtime (converge by comparing). A tenant with a live Hermes container — i.e.
every tenant you are reading this page to migrate — is refused, because registering
would null instance_id, severing the only pointer to the running ECS task: a live task on
a live volume, referenced by nothing, reaped by nothing, billed to nobody, holding the
tenant’s /data. A config edit must never be able to cause that.So the cutover of an existing tenant remains an operator act, taken per tenant, with the
credential in hand and a runbook that stands the new runtime up before anything is deleted.
Ask for one rather than waiting for migrate to start answering.Consolidate further once you’re on teams
Gain #1 — one decision function, so one answer
Legacy policy is declarative inside the container and enforced at the gateway the container calls back through, which means “may this agent do X” has two answers that can disagree. The durable runtime decides at the tool-call boundary instead.POST /v1/policy/explain answers the question directly, without performing the action: it calls
the same isPrimitiveEnabled / capabilityAllowed / resolveApprovalRequirement the gate calls,
and reports the layer and the config path that decided.
Three vocabularies, and they are not the same set — read the one belonging to the surface you
called. The closed decision engine answers
allow | deny | freeze. POST /v1/policy/explain
answers allow | deny | park — it never says approve, because “needs approval” is a deferral,
not a permission. The MCP tool envelope adds attest and unavailable on top of those, because
a model needs to distinguish an outage from a decision. Do not map one onto another by name
alone.Gain #2 — the board is addressed per tenant
Legacy orchestration addresses work as (company, agent). Durable teams address it as (company, tenant, team), so one declared team serves many customers without one customer’s cards appearing on another’s board.Gain #3 — a team can hold a brain
A legacy agent’s context is/v1/memory — per-agent notes with no lifecycle. A team binds a
partition of a company brain, and an agent binds a view of it with an explicit ability
list. A company may declare several brains and name which one a team or an agent binds. See
The brain replaces memory and
The brain.
What does not map yet
submitand eleven other writes answer501. Listed in full at the top of this page. The refusal names its missing dependencies; it does not fabricate a run id.GET /v1/teams— the company-scoped team list — answers501. A declared team’s name is stored under a spelling the control plane’s vocabulary gate has retired, and every package that could host an accessor is at its recorded ceiling. Raising the ceiling would make a gate more permissive, and renaming the stored key needs a migration. Until one of those happens, list your teams from your ownnaive.config.ts, which is the declaration of record anyway.- The
teamsprimitive is not registered, so there is noteamsslug to gate on and none is invented./v1/teams/**is gated ontasksinstead — the primitive whose rows it serves — applied where the tenant is resolved rather than on the mount. A tenant whose kit disablestasksis refused at/v1/users/{id}/tasksand at the team board serving the same rows, both with403 primitive_disabled_by_kit.GET /v1/teamsis the exception: it is company-scoped, resolves no tenant, reads no kit, and answers501either way. - No
Sunsetdate exists on any legacy row. The freeze ends when the last Hermes container stops running, which is a condition, not a date. naive planat project scope is not implemented. Its central field is the manifest digest, anddefineConfigcomputes none in this build.- The
agent-profiles→agentsCLI rename has not happened, and neither have thepolicy,grants,limits,spendandconnectionsCLI groups. Their REST routes are mounted; the CLI is not built against them yet.
Where to go next
- Orchestration overview — the frozen surface, with the deprecation register that produced this page’s URL
- Runtimes —
runtime.durable()besideruntime.pool() - The brain replaces memory — the other half of this move
- Runtime & brain governance MCP tools — the same surface for a model, and how a refusal is made legible to one