What is served depends on which runtime the tenant is on. The
/v1/teams/** surface is
mounted, and of its 32 operations a tenant on the durable runtime is served 22, a tenant
on the frozen hermes runtime is served 13, and 10 are refused for both. A refusal is
501 not_configured naming the missing dependency for the runtime that tenant is on, in
error.details.missing.Which runtime is one column — company_containers.sidecar_url — so the decision is per
tenant. Every response carries provider; every refusal carries error.details.runtime.
The table under What answers today is the exact list, taken from the
router. Nothing on this page is a plan; it is either served or it is named as refused.No tenant can be put on the durable runtime by a request, and that is deliberate. The
only function that writes that column to the Worker, registerVettaRuntime(), is reachable
from exactly one production path — naive up, when a teams: block declares
runtime.durable(...) and the operator exports NAIVE_DURABLE_CREDENTIAL_<TEAM> out of
band — and POST …/migrate is still 501, because registering overwrites the tenant’s legacy
coordinates and the way back is not symmetric. So a tenant naive up placed answers
provider: "durable" and gets the 22 durable operations; every other tenant answers
provider: "hermes", and moving one is an operator act rather than a call. See
the durable runtime.Declaring a team
naive.config.ts
Three things you cannot write
These are compile errors, not runtime failures — you find out in your editor.brain: acme.view({ can: [] }) — an empty ability list — is how an agent gets no brain
access. There is no second concept for “none”, and no field anywhere in which a second
brain can be named: acme.partition() and acme.view() are methods on the brain value
itself.
Addressing: (company, tenant, team)
A team is not addressable on its own. Every runtime operation names three things:- the company — taken from the API key you present, never from the URL;
- the tenant user — the end-user this work is for,
--tenant <tenantUserId>; - the team — the key you declared under
teams:.
--tenant is required on every subcommand except list. There is no default and no
fallback to a “default” user: submitting one customer’s work under another customer’s
ledger is not a mistake worth making convenient. --tenant @self resolves the credential’s
own subject explicitly.
The REST form is /v1/teams/{team}/tenants/{tenantUserId}/….
One deployment currently serves one company. The durable runtime’s control plane is
addressed per
(company, tenant), but the deployment this API talks to is provisioned per
company rather than shared. Read every “tenant” answer below as scoped inside one company’s
deployment.submit is the work verb
There is one verb for handing work to a team, and it is submit. It is the same operation
on every surface — one action id, one fence, one audit row.
--brief <file> (or - for stdin) exists because prose on argv is mangled by every
shell — quoting, !, newlines. Use it for anything longer than a sentence.
naive tasks create, naive ceo run and naive objectives create all map to submit.
They keep working; see Orchestration.
Watching a run
watch requires --run. It emits NDJSON — one object per line — and is resumable:
--cursor <seq> is sent as Last-Event-ID, so a dropped connection resumes at a
position, not at a timestamp. --fail-on-terminal exits 3 when the final status is
unverified, failed or blocked, which is what you want in CI.
Tool arguments are printed as args_digest rather than as values. --show-args prints
them and requires an interactive terminal, so a CI log cannot accidentally capture
arguments that carried a secret.
Approvals
Approvals are the one write on this surface that is fully served today.--because is required. The reason is recorded on the decision, so an approval that
was granted always carries why, and a later reader is not left inferring it.
POST …/approvals/{id}/decide is a second address for one write path, not a second
write path: it calls the same execute/deny code as
/v1/users/{user_id}/approvals/{id}/approve, so the human-resolver rule and the
no-self-approval rule hold identically. An agent cannot resolve its own approval through
this address either.
See Approvals for the resolution rules.
What answers today
Measured against the mounted router, not against a plan. Served for every tenant, on either runtimeboard and board/{card} answer from different stores depending on the runtime — the
durable runtime’s own board for a durable tenant, the legacy mirror for a hermes one — which
is why every response names its provider.
Served on the durable runtime; 501 on hermes, naming the legacy equivalent
Refused for both, each naming what is absent
A refusal is a
501 with error.details.missing as a list — if you fix one
prerequisite there is a second, and you are told about it now rather than after another
round trip. It is deliberately not 404 (which would say your address is wrong), not 403
(which would say you are not allowed), and never 200 with an empty list (which would say
this tenant has no work).
Where a refusal names a legacy route, it is telling you what that tenant has — not
recommending you switch to it. Reaching for naive tasks create to make a durable operation
appear to work writes into the frozen runtime, which is a different execution model with a
different governance path.
A refusal that came from the runtime itself is carried through unchanged: the status is
preserved and error.details.runtime_said holds the runtime’s own sentence, because whoever
runs the cutover needs the runtime’s words rather than this API’s paraphrase.
Next steps
- Orchestration — the frozen legacy runtime, and the mapping off it
- Approvals — how a parked action is resolved
- Brain — the partition a team binds
- Infrastructure as code — the rest of
naive.config.ts