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-GrantThe run grant’s token: the whole authorization. No session, no basic auth.
Txco-Caller-RidOptional: the calling run’s request id on the node, stamped as @cap.caller.rid.
The bodyA 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 _grant stack, 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’s entry names.
  • 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 as cap.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.

factholds
@cap.nameThe capability
@cap.inputThe call’s input, a JSON object
@cap.run, @cap.generationThe run, and which minting of it
@cap.grantThe run grant’s id
@cap.principal.id, .kindWhom the work acts for
@cap.stack, @cap.workspaceThe stack that minted the grant, and the workspace it was minted for
@cap.traceThe trace the grant was minted in
@cap.caller.ridThe node’s request id, when the caller sent one
@cap.impl.stack, @cap.impl.toThe 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 leavesthe 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:

statusbodywhen
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:

bodywhen
{"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. _cap sees only the names nothing declares.
  • A tenant with no _cap stack answers nothing for those names, and the caller gets no_capability.
  • Prefer a declaration. A declared capability is in the catalogue, is checked at deploy, and names its one answering stack; a pattern in _cap is none of those.
  • The decision came first either way. _cap runs only what _grant allowed, for a name the run grant and a standing grant both carry.

What ends a run, whatever it was doing

the runthe answer is
Takes longer than its ceilingtimeout (504, in the body)
Errorsunavailable (503, in the body)
Is refused admission: rate, concurrency, a suspended tenantunavailable (503, in the body)
The caller goes awayThe 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.

Edit this page · View as markdown