Use secrets and signed container images
Two features for modules that call paid services and run many container images: write-only secrets delivered to
one stage, and container image sets approved by who signed them instead of by a list of digests. Both need
requires.core >= 2.5. The contracts are manifest.md and
sandbox.md; this page shows the steps.
Give one stage an API key
Section titled “Give one stage an API key”-
Declare the secret and list it on the stage that needs it. Nothing else receives it: not the other stages, not services, probes or
doctor, not the coordinator side.[requires]core = ">=2.5,<3"[[secrets]]name = "llm_api_key"description = "API key for the model provider"[[stages]]name = "attempt"determinism = "none"secrets = ["llm_api_key"][sandbox]net = { mode = "egress-allowlist", allow = ["api.provider.example"] } # where the key may go, and nowhere else -
Read it in the runner:
from oarbank_sdk import secretskey = secrets.get("llm_api_key") # SecretMissing if this job's stage does not list itThe value comes from
OARBANK_SECRETS_FILE, an owner-only file in the job’s work directory that the agent deletes with it. Never log it, write it into the result or artifacts, or put it on a tool’s command line. The agent replaces exact copies of the value in the logs it sends with[secret:llm_api_key], but that is a safety net: an encoded or split copy passes through. -
The owner sets the value, for the module or for one node (a node’s own value wins):
oarbank secret set my-module llm_api_key # reads the value from stdin or a no-echo promptoarbank secret set my-module llm_api_key --node gpu-box-2oarbank secret list my-module # set or not, fingerprints, never the valueUntil a node has a value, the stage’s jobs wait there (
SECRETS_NOT_SETinoarbank explain). -
Run
oarbank-sdk conform. The kit hands each declared secret a random value (orconformance.jsonsecrets), delivers it only to runner specs of the stages that list it, and fails any run whose output, result, artifacts or files contain it, and any coordinator verb whose answer does (a spec reaches every stage).
A coordinator verb that truly needs the value declares coordinator.permissions = ["secrets:read:self"] and calls
ctx.host.secret("llm_api_key"); it gets the module’s value, never a node’s.
Approve a set of task images by signature
Section titled “Approve a set of task images by signature”-
Sign each task image with a cosign key pair (
cosign generate-key-pair;cosign sign --key cosign.key <image>@<digest>), and put the public key in the bundle:[[sandbox.container_sets]]name = "tasks"registry = "ghcr.io"repository = "example/swe-tasks/" # every repository below this prefixplatform = "linux/amd64"key = "keys/tasks.pub"[[stages]]name = "attempt"requires = { pools = { containers = 1 } }The operator approves the prefix and the key’s fingerprint once. Pushing and signing more images needs no new module version.
-
List each job’s images when you enqueue it, by digest:
from oarbank_sdk import effects as fxfx.job(key, spec, stage="attempt", images=["ghcr.io/example/swe-tasks/django-1234@sha256:..."]) -
Run them from the runner through the broker:
from oarbank_sdk import brokerr = broker.run("ghcr.io/example/swe-tasks/django-1234@sha256:...", ["pytest", "-x"], platform="linux/amd64",mounts=[broker.Mount("repo", "/repo")])The agent verifies the signature before it pulls. An image outside the set, unsigned, or not listed by the job is refused with
image_not_approved. -
To approve images by list rather than one signature each, publish a signed index instead and name it in the set (
index = "ghcr.io/example/swe-tasks-index:current"): an OCI artifact whose layeroarbank_sdk.images.index_document(registry, repository, seq, digests)writes, pushed withoras pushand signed withcosign sign --key. Raiseseqwith every new index; nodes never accept an older one. -
Test it with
oarbank-sdk conform: name a few members and an OCI image layout inconformance.json(oarbank_sdk.imagetestwrites signed test images without cosign or a registry). The kit verifies the members and checks that an image outside the set and an unsigned image inside it are refused.
Show it on your module’s pages
Section titled “Show it on your module’s pages”A page can say whether each secret is set, and since when, without ever seeing a value (the secrets host query,
UI contract 1.2), and list the images your jobs ran (images). taskbench’s overview
(ui/pages/overview.json) does both and links to the module’s Secrets
tab, where the owner sets the value:
{"type": "table", "requires": "1.2", "fallback": "placeholder", "source": {"query": "secrets"}, "columns": [{"key": "name", "type": "code"}, {"key": "set", "type": "bool"}, {"key": "fingerprint", "type": "code"}, {"key": "changed_at", "label": "changed", "type": "relative_time"}, {"key": "changed_by", "label": "by"}]},{"type": "link", "requires": "1.2", "fallback": "drop", "text": "Set it on the Secrets tab", "to": {"tab": "secrets"}}The fingerprint is the keyed one the Secrets tab shows. See Build your module’s GUI.
GPUs in containers
Section titled “GPUs in containers”A stage whose containers need the GPU reserves the agent’s gpu pool and declares it:
[runner]gpu = { use = "exclusive", in_container = true }
[[stages]]name = "attempt"requires = { pools = { containers = 1, gpu = 1 } }and asks for it per run with broker.run(..., gpus="all"). Linux nodes with a CDI spec for their GPU (nvidia-ctk cdi generate), Windows nodes whose WSL containers session sees a GPU and Macs with krunkit offer the pool; the APIs a GPU
container gets on each node are in its report, and apis_any picks among them (GPU placement). On
Windows, use a glibc-based image for GPU containers.
Source: docs/secrets-and-images.md in the oarbank-sdk repository