Skip to main content
Some operations on /v1/teams are declared, addressable and refused. Which ones depends on the tenant’s runtime, so this page describes the contract and sends you to Durable Runtime API for the current membership and counts.
The lists live on one page, and it is not this one. This page used to carry its own tables of “the refused reads (5)” and “the refused writes (12)”. Those numbers were the measurement of a build in which every tenant was a hermes tenant, and they went stale the moment a durable tenant could be served operations a hermes tenant is refused — including six that this page still listed as refused.They are not restated here. naive-docs/api-reference/runtime/overview.mdx carries the split, and ci/runtime-surface-counts.test.ts derives its numbers from the router and fails the build when the prose disagrees. A count that no gate reads is a count that drifts, which is exactly what happened here.

The contract

Every refusal on this surface has the same shape:
501
Three properties you can rely on:
  1. missing is a list, not a string. Most of these have more than one prerequisite. A caller that fixes one and retries is told about the second one now rather than after another round trip.
  2. The request is authorised before it is refused. A bad credential still gets 401; a credential for the wrong tenant still gets the tenant error. The 501 is not an oracle for whether a resource exists.
  3. details.runtime names which runtime the refusal is about. The same operation refuses for different reasons on the two runtimes, and a refusal that did not say which one was wrong in both directions: it told a durable tenant its dispatcher did not exist, and told a hermes tenant about a manifest store hermes has never had.

The three refusals, and they are different facts

The second row is the one worth reading carefully. error.details.missing names which verb the runtime already implements, so “this runtime cannot do that” and “this address cannot reach it” are distinguishable. Where the refusal came from the runtime itself, its own sentence is carried through verbatim in error.details.runtime_said rather than being translated.

🔴 The exception: the reads that are empty without refusing

Property 3 of the old contract — 501, never 200 with an empty body” — is not true of this whole surface, and pretending otherwise is worse than the gap.These reads were written before either runtime was a choice, and they answer from naive’s task mirror on both. A tenant placed on the durable runtime has no rows in that mirror — it is written by the legacy close path — so these return a well-formed 200 with nothing in it, and nothing in the response says why:These are not refusals and cannot be caught as ones. {"items": []} here is a claim about the build wearing the shape of a claim about your tenant — the precise inversion the contract above exists to prevent.Two of the consequences are worth naming, because a reader will otherwise plan around them:
  • GET …/tenants/{id} and GET …/board disagree. The header counts mirror rows; the board is fetched from the runtime. Read the board for the board.
  • GET …/runs/{id}/events is empty, so any poller built on it yields nothing. The transcript is not missing — it is served as SSE at GET …/runs/{id}/stream, which is a durable lane.
Branch on provider, which every response that has a durable lane carries.

Why writes are not quietly forwarded

The obvious shortcut for a write a tenant’s runtime cannot serve is to forward it to the other one. It is not taken, for two reasons:
  1. The legacy runtime is frozen. A second write path into a frozen runtime is how a frozen runtime stops being frozen.
  2. The response would lie. POST …/submit returns manifest_digest and required_attestations. The legacy runtime has neither. A 200 carrying a fabricated digest is worse than a 501, because the digest travels.
If you want the legacy behaviour, call the legacy address directly. It is deprecated but fully working, and calling it explicitly means your code records which runtime it actually used.

Checking programmatically

error.details.missing and error.details.runtime are machine-readable and stable enough to log. Do not parse error.message.
A 501 is a statement about one tenant, not about the API. The same request against a tenant on the other runtime may be served. That was not true when this page was first written, which is why the check above no longer reads a 501 as “not wired in this build”.