Discovery and the capability closure
The capabilities of /api are discoverable: the manifest tells you which operations this server version defines, the deployment-wide capabilities tell you which domains this server has assembled, and the per-session capability closure tells you which operations are allowed right now, in a given session, under the current authorisation. All three endpoints are read-only, require authentication and return no navigable URL.
The three discovery endpoints
Section titled “The three discovery endpoints”| Operation | Path | Question it answers | Response highlights |
|---|---|---|---|
discovery.manifest |
GET /api/manifest |
What this server version defines | The whole manifest (format: tansr-api-manifest-v1, revision, schemaHash, families, operations, capabilities, platformApi) plus runtime.installed (whether each domain is assembled; for the session domain the preferred family name or null) |
discovery.capabilities |
GET /api/capabilities |
What this server has assembled | { contract, manifestRevision, schemaHash, domains }; each domain gives installed, contract (family name), status and schemaHash, and the session domain adds family: { preferred, available } |
discovery.session.capabilities |
GET /api/sessions/:id/capabilities |
What can be done in this session right now | The capability closure (next section); the response header tansr-closure-id equals closureId in the body |
Shared rules:
GETonly; any other method is 405method_not_allowed(withallow: GET).- No query string and no request body; sending either is 400
invalid_request. - Responses carry
cache-control: no-store: rediscover on every start or reconnect, never cache to disk. tansr-domainisdiscoveryon all three, andtansr-schema-hashis the aggregate manifestschemaHash; on responses of other domains that header is the digest of the family’s source file and is not comparable with the aggregate value.- The bodies contain no navigation fields such as URLs,
hreforlinks. Paths are instantiated only on the client, from manifest templates plus path parameters.
The per-session capability closure
Section titled “The per-session capability closure”The capability closure is a deterministic snapshot the server takes of operation availability for “this session, this user, right now”:
{ "contract": "unified-v1", "closureId": "<sha256 hex>", "authorizationRevision": "<string or null>", "domains": { "session": { "installed": true, "revision": "<family revision>" }, "archive": { "installed": false, "revision": null } }, "operations": { "session.message.send": "enabled", "session.audio.speak": "disabled", "archive.binding.create": "unavailable" }}operationscovers 77 operations (the 81 of manifest revision 7 minus the 3 discovery endpoints and the deployment-widesession.capabilities, i.e.GET /api/capabilities/sessions), each in one of three states:enabled: inside the closure, may be called;disabled: the capability exists on this server but the current authorisation or session state does not allow it (for example the session has ended, a required capability is not granted, the archive binding has not reached a writable epoch);unavailable: the capability does not exist at the boundary of this server or this session (domain not assembled, audio / input source not wired, and so on).
closureId = sha256(UTF8('tansr.unified.closure.v1') ‖ 0x00 ‖ canonical({ authorizationRevision, domains, operations })), wherecanonicalis the strict JSON encoding of@tansr/protocol/canonical. A client may recompute it locally as a check, but it never needs to derive the closure itself: the closure content is always what the server answered.- Session not found, or not owned by the current user: 404
not_found, with no distinction between the two cases; session domain not assembled or closure source not wired:capability_unavailable(detail.reason: not_installed); closure source temporarily unavailable: 503upstream_unavailable(withretry-after).
Using tansr-closure-id as a precondition on writes
Section titled “Using tansr-closure-id as a precondition on writes”Once you hold a closure, send the request header tansr-closure-id: <closureId> on session-scoped write operations. Before delegating to the domain handler the server performs two checks:
| Check | Response when it fails |
|---|---|
Does the closureId in the header equal the current closure? |
412 precondition_failed, retryAction: rediscover, detail.domainCode: closure_stale; the response header tansr-closure-id carries the current value |
Is the operation enabled in the current closure? |
disabled: 403 capability_unavailable; unavailable: 404 capability_unavailable; in both cases detail is { reason: 'outside_closure', operation, state, closureId } |
Three points to note:
- The header only makes sense on session-scoped writes. On
GET/HEAD, or on a non-session path (for examplePOST /api/executors/connections), it is 400invalid_request(detail.header: tansr-closure-id). - Without the header the server skips the closure check, and the domain handler remains the final authority: it rejects unauthorised requests according to its own subcontract. The closure lets a client receive a uniformly shaped rejection earlier; it never relaxes any check.
- There is exactly one correct reaction to a 412: call
GET /api/sessions/:id/capabilitiesagain and retry with the newclosureId. Do not guess, do not keep the old value, do not fall back to an old prefix.
Revision drift
Section titled “Revision drift”Every /api response carries tansr-manifest-revision. Official clients lock the revision and schemaHash they were generated against at build time (for example @tansr/api-client/api exposes them as client.locked, with the runtime observation in client.observed()). If at runtime the server’s revision differs from the locked one:
- Log it, do not switch logic. Revision advances are additive (new operations, new fields); an older client keeps calling according to its own catalogue, and operations it lacks show up as
unavailablein the closure or answercapability_unavailable/not_foundwhen called. - Do not change the parser because of it, and do not “try” the old prefix: that is exactly the silent downgrade forbidden by rule one.
- When you need new operations, upgrade the client to the version aligned with that revision (
@tansr/api-clienthas been aligned with revision 7 since 0.3.0; the./apientry is unchanged in 0.4.0).
@tansr/api-client/api offers an onContract callback through which the { manifestRevision, schemaHash, domain } of every response passes; it is a good hook for your diagnostics log.
Was this page helpful?
Thanks for your feedback.