All docs
Docs · Reference
The capability inlet
a node asks, your stack answers
Work you dispatched to a node calls back for something only this chassis should do. The call arrives as one HTTP request carrying the run's grant. The chassis decides it, runs the stack that declares the capability, and answers with what that stack left.
Capabilities says what a capability is, how a stack declares one and how a node calls it. This page is the wire and the run.
The request
POST /v1/cap/crm.lookup HTTP/1.1
Host: desk.example.com
Txco-Run-Grant: rg1.…
Txco-Caller-Rid: CfbLw96hdZm715moMuCqf
Content-Type: application/json
{"input": {"email": "ada@example.com"}} | The path | /v1/cap/<name> on the web listener, ahead of every hostname route. The tenant is the grant’s, not the host’s. |
Txco-Run-Grant | The run grant’s token: the whole authorization. No session, no basic auth. |
Txco-Caller-Rid | Optional: the calling run’s request id on the node, stamped as @cap.caller.rid. |
| The body | A JSON object; input is what the capability is asked. An empty body is an empty input. At most --op-payload-max. |
A node’s cap://<name> op makes this request. Anything else that holds a
live run grant’s token may make it too: the token is what is checked.
One call is two runs
_grant/0 decide it @src == "grant", @grant.kind == "capability"
<stack>/<n> do it @src == "cap", @cap.name, @cap.input - The decision is a run of the tenant’s
_grantstack, as for a secret (the grant inlet): the chassis proposes, a rule may allow, refuse or hold. A call that fails a hard check — a forged or expired token, an ended grant — reaches no rule. - The work is a run of the stack that declares the capability
(
CAPS/<name>.yaml), entered at its start,<stack>/0, or at the scope its declaration’sentrynames. - Both are ordinary runs: admitted, traced and metered. The work run’s
request id is the answer’s
X-Request-ID; the node records it ascap.rid.
The envelope
Everything under @cap is stamped by the chassis from the run grant’s own
row, after the token verified and the call was allowed. It is read-only.
Only @cap.input is the caller’s.
| fact | holds |
|---|---|
@cap.name | The capability |
@cap.input | The call’s input, a JSON object |
@cap.run, @cap.generation | The run, and which minting of it |
@cap.grant | The run grant’s id |
@cap.principal.id, .kind | Whom the work acts for |
@cap.stack, @cap.workspace | The stack that minted the grant, and the workspace it was minted for |
@cap.trace | The trace the grant was minted in |
@cap.caller.rid | The node’s request id, when the caller sent one |
@cap.impl.stack, @cap.impl.to | The declaring stack, and where the run entered |
The envelope never holds the grant’s token.
The stack answers at the top level of the envelope:
| the run leaves | the answer is |
|---|---|
._cap.output | {"ok": true, "output": <that value>} |
._cap.error = {code, message} | {"ok": false, "error": {code, message, status: 422}}. It wins over an output. |
| neither | {"ok": false, "error": {"code": "no_capability", "status": 404}} |
The answers
Always JSON. The decision is answered with its own status, before anything runs:
| status | body | when |
|---|---|---|
401 | {"ok":false,"error":{"code":"unauthorized"}} | No token, or not a live grant’s |
403 | {"ok":false,"error":{"code":"denied"}} | Refused: the allowlist, the standing grant, the budget, a rule |
409 | {"ok":false,"error":{"code":"held"}} | A rule held it for a person |
404 | {"ok":false,"error":{"code":"no_capability"}} | Not a capability name |
503 | {"ok":false,"error":{"code":"unavailable"}} | The chassis could not decide, or could not read the declaration |
A refusal says only that it was refused. The trace of the _grant run has
the reason.
An allowed call answers 200 at once — headers sent before the
capability’s run starts — and the body when the run ends. Until then the
chassis writes one space every ten seconds. A model call can take a minute,
and a proxy on the way that waits a fixed time for response headers, or
drops an idle connection, would otherwise cut it. So what the run came to
is in the body, with the status it would have carried:
| body | when |
|---|---|
{"ok":true,"output":…} | The stack answered |
{"ok":false,"error":{"code":…,"message":…,"status":422}} | The stack’s own error |
{"ok":false,"error":{"code":"no_capability","status":404}} | No rule answered |
{"ok":false,"error":{"code":"timeout","status":504}} | The run passed its ceiling: the declaration’s timeout, or --op-timeout-max |
{"ok":false,"error":{"code":"unavailable","status":503}} | The run failed, or the tenant was refused admission |
A client reads error.status as the status. The leading whitespace is
valid JSON. The node’s cap:// does both, and maps the codes to its own
grammar (txco_cap_denied, txco_cap_held, …; cap://).
The _cap stack
A capability no active stack declares is not refused outright: the call
goes to the tenant’s _cap stack, at _cap/0, the way a cron tick goes to _cron/0. It is the place for a rule that answers by pattern, or routes a
family of names somewhere:
# OPS/_cap/100/legacy.txcl — every report.* capability is answered by one stack
WHEN @src == "cap" && @cap.name =~ /^report\./
SET @route.stack = "reports",
@route.to = "reports/0"
EXEC "txco://route" - A declaration wins.
_capsees only the names nothing declares. - A tenant with no
_capstack answers nothing for those names, and the caller getsno_capability. - Prefer a declaration. A declared capability is in the catalogue, is checked at deploy,
and names its one answering stack; a pattern in
_capis none of those. - The decision came first either way.
_capruns only what_grantallowed, for a name the run grant and a standing grant both carry.
What ends a run, whatever it was doing
| the run | the answer is |
|---|---|
| Takes longer than its ceiling | timeout (504, in the body) |
| Errors | unavailable (503, in the body) |
| Is refused admission: rate, concurrency, a suspended tenant | unavailable (503, in the body) |
| The caller goes away | The run is cancelled |
Keeping a record
The two traces — the _grant run and the capability’s run — are the
record of a call: txco trace. To keep one you can read back, write it
where the decision is made, as the grant inlet shows; @grant.input is there for a capability call, so the record can say
what was asked as well as what was decided.