All docs

Docs · Reference

Grants

what a principal, and one piece of its work, may hold

A stack dispatches work somewhere else: a command in a workspace, acting for a principal. That work may need a secret. Grants say what it may be handed by this chassis, and sandboxes say what it holds.

   principal
       │
  standing grants        may ever be handed GITHUB_PAT, DB_DSN, …      txco://grant/put
       │
   run grant             may open the sandboxes github, postgres       txco://delegate/mint
       │
  ┌────┴──────────────────────────────┐
  │ exec WITH grant, sandbox = github │ the chassis opens it, then starts the command
  │ txco sandbox github -- gh pr list │ the program asks its chassis to open it
  └────┬──────────────────────────────┘
       │ one request per secret, decided by the tenant's _grant stack
       ▼
   PROGRAM     GH_TOKEN=…      authority stops here
ToRead
See it wholeA complete example
Say what a program may holdSandboxes
Say what a principal may ever be handedStanding grants
Say what one piece of work may openRun grants
Start a command inside a sandboxOpening a sandbox for a command
Let a program open one itselftxco sandbox
Decide which requests are allowedWhat decides a request
Let dispatched work ask this chassis to do somethingCapabilities
Turn it onTurning it on

Who ends up holding what

The secret goes to the program, in cleartext, as the value of a variable the sandbox names. It goes nowhere else.

holdsnever holds
The rule that dispatched the workThe grant’s id, the sandboxes’ namesThe token, the secret
The envelope and the traceIds, the sandbox, the variable, the request, the proposal, the verdictThe secret, the token
The commandWhat its sandboxes set, and — where it may open more — its tokenA secret it chose by name: there is no way to ask for one

This is for work the chassis does not run itself: a program in a workspace that has to hold the connection. A rule that needs a secret for its own call does not need a grant: it names the secret with secrets.env.* or secrets.headers.*, and the chassis uses it where it is stored (workspaces, secret store).

A complete example

A stack deploys by pushing a repository from a workspace. The push needs a token, which the program running git must hold.

Once, in the stack’s tree — what a deploy step holds, under which name:

# OPS/release/SANDBOXES/github.yaml
description: what a deploy step holds
env:
  GH_TOKEN: secret:GITHUB_PAT

Once, when the principal is set up:

# `service:release` may ever be handed GITHUB_PAT
EXEC "txco://grant/put"
  WITH principal = "service:release", kind = "secret", name = "GITHUB_PAT"

Once, by the operator, since the secret’s own policy starts closed:

txco auth tenant secrets policy GITHUB_PAT --pull reviewed

On every deploy:

# 3400: what this deploy may open, and where it runs
WHEN ._deploy.id != ""
  EXEC "txco://delegate/mint"
    WITH principal  = "service:release",
         run        = ._deploy.id,
         allow      = ["github"],
         workspace  = "build",
         node_class = "reviewed",
         ttl        = 600,
         budget     = 3,
         into       = "_delegate"

# 3410: run it inside the sandbox
WHEN ._delegate.id != ""
  EXEC "workspace://build/exec"
    WITH grant   = ._delegate.id,
         sandbox = "github",
         command = "git push origin main",
         into    = "_push"

git runs with GH_TOKEN in its environment and nothing else of the grant’s. The rule sees ._push.exit and ._push.stdout; the trace shows that GITHUB_PAT was asked for through github, and allowed, by which run, and never its value.

The same, for a program that decides on its own when it needs the token — an agent loop, a script with many steps:

    WITH grant   = ._delegate.id,
         command = "./release.sh"
# inside release.sh
"$TXCO_BIN" sandbox github -- git push origin main

To refuse the deploy’s token outside working hours, or to keep a record of every release, add a rule to the tenant’s _grant stack (the grant inlet). To stop a deploy that is still running, revoke its grant: its next request is refused.

There are two kinds of grant, and the second can only narrow the first.

Standing grantRun grant
SaysThis principal may ever be handed thisThis piece of work may open these sandboxes, now
LastsUntil revokedMinutes: it expires, and ends with the work
Written bytxco://grant/puttxco://delegate/mint
HoldsOne principal, one resource, its verbsIts sandboxes, a budget, an expiry, a workspace

A grant names one of two kinds of resource:

kindnamed byverbthe principal may
capabilitylowercase words joined by dots: crm.lookupinvokecall it
secreta secret’s name: CRM_KEYreleasebe handed the secret itself

A name is exact. There are no wildcards and no families: crm.lookup does not grant crm.update.

Sandboxes

A sandbox is what a program may hold: a small file the stack deploys, one per name, under the reserved SANDBOXES/ directory beside OUTLETS/ and DATASETS/.

# OPS/<stack>/SANDBOXES/<name>.yaml
description: what a deploy step holds     # optional
env:
  GH_TOKEN: secret:GITHUB_PAT             # VARIABLE: reference
  GH_HOST:  secret:GH_ENTERPRISE_HOST
The nameThe file’s stem: [a-z][a-z0-9_-]*, at most 64. It appears in allow, in WITH sandbox, on the command line.
envEach entry sets one variable to one secret. A variable is [A-Za-z_][A-Za-z0-9_]*; a reference is secret:<NAME>. One secret may fill several variables. A name starting TXCO_ is the chassis’s own. At most 32.
capabilitiesA list of capability names a run whose grant names this sandbox may call on this chassis: crm.lookup, mail.send. Nothing is handed over for one; each call is decided when it is made. At most 32.
descriptionOptional, for whoever reads the file.
env or capabilitiesAt least one. A sandbox may set variables, name capabilities, or both.
Anything elseA deploy error. Only what the chassis enforces is declared.
  • It deploys with the stack. txco apply uploads it, txco lint and the activation gate parse it, and a file that does not parse refuses the deploy. A canary slot declares its own, as it does its outlets.
  • The program never names a secret. It names a sandbox; the stack said what that sets when the file deployed. Nothing in the chassis hands a program a secret by name.
  • A run grant copies its sandboxes at mint. Editing a file changes new mints, not live grants. Every request still reads the grant’s row, so revoking it, closing it or spending its budget bites as before.
  • A sandbox opens whole or not at all. Each secret it names is decided on its own, in the trace on its own, charged on its own. One refused, and the program is handed nothing; what was already released is discarded and what was charged stays charged.
  • The _grant stack sees which sandbox and which variable (@grant.sandbox, @grant.env) beside the secret’s name, so a rule may allow a secret through one sandbox and refuse it through another.

files: and network: are not declared yet: a sandbox holds only what the chassis enforces.

Standing grants

# research may be handed the CRM key, and may call crm.lookup
WHEN ._setup.ok == true
  EXEC "txco://grant/put"
    WITH principal = "service:research", kind = "secret", name = "CRM_KEY"
# → _grant = {id, principal, kind, name, verbs, created_by, created_at, created}
opWITHresult
grant/putprincipal, kind, name, verbs?the grant, and created
grant/listprincipal, kind?, include_revoked?{principal, count, items[]}, newest first. The owning stack only.
grant/listkind, name{kind, name, count, items[]}: who holds this resource. Any stack in the tenant.
grant/revokeid (with principal to pin whose it must be), or principal, kind, namethe grant, and revoked

All three answer at into (default _grant).

  • put is safe to retry. Putting a grant that already holds returns it with created: false.
  • The principal must already exist. It needs a user record, a username or a credential first. A grant cannot name a principal that another stack could claim later.
  • The resource need not exist. A grant may be written before the secret it names is stored.
  • A grant has no conditions. No roles, no inheritance, no time limit. It is a row a person can read aloud.
  • A disabled user holds nothing while disabled. Nothing is revoked, so enabling the user restores every grant.
  • Pass principal with an id that came from a request, for the reason credentials do.
  • A principal may hold 1024 live grants.

A standing grant decides nothing on its own. It is the outer bound. A run grant names what one piece of work may open, and each request is decided when it is made.

Run grants

To delegate is to hand part of what a principal may be handed to one piece of its work. The record of it is a run grant. Mint one when you dispatch the work:

# 3400: this task may open two sandboxes
WHEN ._task.id != ""
  EXEC "txco://delegate/mint"
    WITH principal = "service:research",
         run       = ._task.id,
         allow     = ["crm", "warehouse"],
         workspace = "bench",
         ttl       = 900,
         budget    = 20
# → _delegate = {id, principal, run, generation, workspace, workspace_id,
#                node_class, allow, sandboxes, budget, spent, depth,
#                minted_by, issued_at, expires_at, live}
opWITHresult
delegate/mintprincipal, run, allow, and the optional params belowthe grant
delegate/getidthe grant, what it has spent, and whether it is live
delegate/revokeidthe grant, and revoked
delegate/closeid, reason? (default done)the grant, and closed

All four answer at into (default _delegate). Only the stack that minted a grant may read, revoke or close it.

mint parammeaningdefault
runThe name of the work: a task id, a job id. Letters, digits and . _ @ + : / -.required
allowThe sandboxes the work may open, by name: each a SANDBOXES/<name>.yaml of this stack. 1 to 16.required
workspaceA workspace of the calling stack, by name: where the work is sent. It need not exist yet.none
node_classreviewed or unreviewed: what is allowed to run thereunreviewed
ttlSeconds the grant lasts--run-grant-ttl-default
budgetRequests the work may make: one per secret opened--run-grant-budget-default
generationA counter of your own, such as a lease. It must be later than any the run has had.the next one
parentThe id of a run grant to narrow fromnone

In the result, allow is every secret the sandboxes name, and sandboxes is each sandbox’s variable names — never a value.

There is no token in the result. The token that travels with the work is signed by the chassis when it hands the work over, and never enters an envelope or a trace. A rule holds the grant’s id, which names the grant and authorizes nothing.

A run grant only narrows

RuleRefused as
Every sandbox is one the stack declaresno_sandbox; the message names what the stack has
Every secret the sandboxes name is one the principal holds a standing grant forexceeds_standing; the message names what is missing
A grant narrowed from a parent names only secrets the parent names, whatever it calls its sandboxesexceeds_parent
Its budget comes out of the parent’s, all of it, at mintexceeds_parent
It expires no later than the parent. A longer ttl is shortened, not refused.
It is narrowed at most 4 timesdepth
Only the stack that minted the parent narrows itnot_owner

Work that a run dispatches needs a run name of its own, such as task-42/lookup. The principal may differ from the parent’s: that is how one principal hands part of its work to another.

What ends a run grant

EventThe row shows
It expiresexpires_at in the past
delegate/close: the work endedclosed_at, and your close_reason
delegate/revokerevoked_at
The same run is minted againclosed_at, close_reason: "superseded"
Its budget is spentspent.calls equals budget.calls
  • Ending a grant ends every grant narrowed from it.
  • Every request reads the row, so each of these takes effect on the work’s next request.
  • Minting a run again replaces its grant. The newest generation is the only live one, so a stale copy of the work is refused. Two runs on one workspace each keep their own grant.
  • Nothing extends a grant. For longer work, mint again.
  • Ending a grant recalls nothing. It stops what the work may still ask for. A secret the work was already handed stays where it is until you rotate it.

The node class

node_class says what is allowed to run where the work is sent.

classwhat runs there
unreviewedAnything, including code written a moment ago
reviewedOnly what was reviewed before it was installed

It is a promise by whoever mints the grant. The chassis cannot prove what runs on a machine. A secret’s pull policy is checked against it.

Opening a sandbox for a command

# 3410: run the work inside its sandboxes
WHEN ._delegate.id != ""
  EXEC "workspace://bench/exec"
    WITH grant   = ._delegate.id,
         sandbox = ["crm", "warehouse"],
         command = "./build.sh"

grant hands one command the run grant; sandbox — one name or a list — says which of its sandboxes the chassis opens before the command starts. The command starts with every variable they set, or does not start.

The command also gets what it needs to present itself at run time:

variableholdson
TXCO_RUN_GRANTThe grant’s token. txco sandbox presents it, and so does a command that dispatches a run to a node.Every provider
TXCO_RUNThe run’s nameEvery provider
TXCO_GRANT_SOCKWhere txco sandbox reaches the chassis. Set where the grant personality is on.The chassis’s own machine (the local provider)
TXCO_BINThe txco binary, for "$TXCO_BIN" sandbox …The chassis’s own machine
  • The values travel in the exec. Opening happens here, in the chassis, so the push form works on any provider — the fleet’s included — as secrets.env.* does. A provider whose commands run on another machine gets the sandboxes’ variables, the run’s name and the token; the socket and the binary it could not use there.
  • The token is made here and goes nowhere else. It is signed when the command starts, put in the command’s environment, and removed — with every value a sandbox set — from the command’s output and from every error before anything reaches the envelope or the trace.
  • Only exec takes a grant. attach, connect and the other verbs refuse it, and so does exec with stream.
  • The grant must be this stack’s, and for this workspace. A rule cannot hand a command another stack’s grant, or carry a grant to a workspace it was not minted for.
  • What a sandbox sets wins over env and secrets.env.* of the same name. Two sandboxes that set one variable are refused.
workspace.error.codemeaning
grant_refusedA sandbox was refused (the message names it, never why: the trace has that), or the grant does not name it, has ended, is another stack’s, or was minted for another workspace. Or the node has no master key to sign with. The message says which.
grant_unavailableThis node opened no identity store
bad_requestgrant is not a run grant’s id; sandbox is not a name or a list of them, or is given without grant; grant names no sandbox on a provider whose commands cannot reach this chassis (nothing would be handed over); or grant is combined with stream or a verb that runs no command

txco sandbox

"$TXCO_BIN" sandbox github -- gh pr list
"$TXCO_BIN" sandbox github postgres -- ./migrate

txco sandbox asks the chassis to open each named sandbox, with the token this command was started with, then becomes the program. It is for a program that decides on its own when it needs what: an agent loop, a script with many steps. It needs the grant personality on the chassis.

  • The -- is required. A program’s name is as good a sandbox name as any.
  • The program gets what the sandboxes set and none of the grant’s variables, so it cannot open more.
  • Exit codes and signals are the program’s own once it starts.
exit, before the program startsmeaning
77A sandbox was refused
69The chassis could not be asked, or could not decide
65A value holds a NUL byte, which no variable can
126, 127The program could not be started, or was not found
2The command line, or two sandboxes that set one variable

Whatever a command prints is in the trace

A command that prints a secret it was handed puts it in the envelope and the trace. The chassis removes the grant’s own token, and every value it opened a sandbox with, from a command’s output. It cannot know what else the command read.

What decides a request

Each secret a sandbox names is one request.

 1  HARD CHECKS        the grant is genuine, live, current, and names the sandbox   no ──▶ refused
 2  THE PROPOSAL       the chassis's own answer
 3  THE _grant STACK   the tenant's rules may change it
 4  THE VERDICT        refuse, or charge the budget and hand over
CheckKind
The token is genuine and unexpiredHard
The run grant is live, and the newest of its runHard
The tenant is liveHard
The run grant names the sandboxHard
The secret existsHard
The run grant names the secretProposal
The principal holds a standing grant to release itProposal
The secret’s pull policy admits this nodeProposal
Budget remainsProposal
  • A hard check is the chassis’s alone. No rule changes it.
  • The proposal is to allow when every proposal check passes.
  • The tenant’s _grant stack has the last word. A rule may allow what the chassis proposed to refuse, or refuse what it proposed to allow. The grant inlet says how.
  • With no _grant rules, the proposal stands.
  • Everything fails closed. A rule that errors, takes too long or is refused admission is a refusal.

The pull policy

A secret says for itself whether work may be handed it.

pullmeaning
noneNo work may be handed it. The default. A rule that names the secret still uses it as before.
reviewedWork on a reviewed node may
anyWork on any node may
txco auth tenant secrets policy DB_DSN --pull reviewed
txco auth tenant secrets list        # the PULL column

A secret that was handed over cannot be recalled. Revoking a grant stops the next request. Only rotating the secret ends a past one. Mark a secret any only when its loss is tolerable.

Turning it on

The push form — exec WITH grant, sandbox — needs nothing turned on beyond a workspace provider. For txco sandbox, add the grant personality: the Unix socket a program on this machine reaches the chassis on.

txco dev --allow-local-workspace --grant
  • The ops need nothing. grant/* and delegate/* work on every node.
  • Listed and unable to listen is fatal at boot. A node that was meant to answer and silently does not is worse than one that will not start.

Which stack may grant

The creator-stack rule covers both kinds: only the stack that manages a principal may grant to it or mint for it, and a canary slot or a channel counts as its stack.

Errors

Failures land as <into>.error.{code, message} and the run continues:

code (txco_grant_… / txco_delegate_…)meaning
invalid_arga bad or missing param; the message says which
not_foundno such principal or grant in this tenant
not_owneranother stack manages this principal, or minted this grant; the message names it
user_disabledthe principal is a disabled user
too_manythe principal already holds 1024 live grants
no_sandboxallow names a sandbox this stack does not declare; the message names what it has
exceeds_standinga sandbox names a secret the principal holds no standing grant for
exceeds_parenta sandbox names a secret the parent does not, or the budget is more than the parent has left
parent_not_livethe parent grant has expired, ended or been revoked
depththe grant would be narrowed more than 4 times
stale_generationthe run already has this generation or a later one
no_tenant · no_stackthe request has no tenant, or the op was not dispatched by a rule
disabledthis node could not open the identity database at boot
storethe database failed; the message has the detail

Flags

flagdefaultmeaning
--run-grant-ttl-default600Seconds a run grant lasts when mint gives no ttl
--run-grant-ttl-max3600Ceiling on ttl. At most 86400.
--run-grant-budget-default50Requests a run grant may make when mint gives no budget
--run-grant-budget-max1000Ceiling on budget. At most 1000000.
--grant-decide-timeout5sHow long the _grant stack may take to decide one request
--grant-refusal-window5sHow long a refusal is answered from memory. 0 turns it off.
--grant-socketunder the system’s temp dirThe socket of the grant personality. At most 100 bytes.

A refusal is remembered for a few seconds, so a program that asks again and again cannot run the _grant stack without end. A change that would now allow the request takes up to --grant-refusal-window to be seen by a run that was just refused. A release is never remembered.

Where it lives

In the auth database, beside users and credentials. On a fleet every node sees the same grants, so a grant minted on one node is honored, and revoked, on all of them. The sandboxes are stack files, in the runtime database with the rest of the stack.

See examples/grants-hello for all of it in one stack.

Edit this page · View as markdown