Jobs & secrets

Fleet

#Jobs, agent runs and secrets

A gateway also runs work after you have gone. A job is a command, or an agent and a prompt, run in a fresh sandbox per run: the gateway places it, starts it, waits with a timeout, keeps its output (1 MiB per stream) and the files you name (8 MiB each), terminates the sandbox, and retries a run that failed. prompts makes a batch, one run per prompt; parallelism bounds how many run at once. A run that does not fit the tenant's quota waits for room.

Jobs and secrets exist only on a gateway (fleet.md); a plain sandboxd answers their endpoints 404. The endpoints are in api/v1.md.

#A first agent run

sandbox-cli secret set ANTHROPIC_API_KEY < key.txt      # sealed on the gateway, never shown again
sandbox-cli agent-run claude "fix the failing test" --secret ANTHROPIC_API_KEY --wait

agent-run is a job of one run: an agent, a prompt, and the same options a job has but no batch. It prints the job's id; with --wait it waits, prints the run's output and exits with the agent's exit code. job run --wait waits for a whole job and exits 1 unless it succeeded.

An agent runs in its verified headless mode (claude, codex, gemini, opencode, cline). Your saved login does not come along — a run's is copied in when it starts, from the gateway or your machine, and a job has none — so the agent authenticates with an API key you keep as a secret: ANTHROPIC_API_KEY for claude, OPENAI_API_KEY for codex, GEMINI_API_KEY for gemini, the provider's for opencode and cline. The agent must be in the image or installable from it.

#Jobs and batches

cat > job.yaml <<'YAML'
agent: claude
prompts: ["review internal/api", "review internal/gateway"]
parallelism: 2
retries: 1
timeout_secs: 1800
secrets: [ANTHROPIC_API_KEY]
keep: {files: [/sandbox/home/review.md]}
notify: https://hooks.example.com/sandbox    # POSTed {job, run, state}; never output
YAML
sandbox-cli job run -f job.yaml
sandbox-cli job ls · job get ID · job output ID 0 [--file PATH] · job cancel ID

A job spec is YAML or JSON (-f - reads it from stdin):

Field
command: [argv…] what each run starts. Exactly one of command and agent.
agent, prompt an agent in its headless mode, and what to ask it.
prompts: […] a batch: one run per prompt, every one to succeed.
parallelism how many runs may be in flight at once (default 1).
completions how many runs must succeed (default 1; a batch's is its number of prompts).
retries how many more attempts a run that failed or timed out gets.
timeout_secs bounds each attempt from the moment its command starts (default 3600).
name, image a name for people (jobs are found by id); the image, else the node's default.
labels: {key: value} your own metadata, bounded as a sandbox's are: up to 32 keys of lowercase letters, digits and . _ / -, values up to 256 printable bytes. Returned with the job; Studio's Jobs filters by them.
env: {NAME: value} set in each run; a reserved name is refused. A GET shows names, not values.
secrets: [NAME, VAR=NAME] the tenant's secrets, each set in the run's environment: VAR=NAME sets secret NAME as VAR, and a bare NAME sets it under its own name. A variable is held to what env may set, once.
network, resources, from_snapshot as for a sandbox: the egress policy, {cpus, memory_mb, disk_mb}, a snapshot to start from.
keep: {output, files} what is kept once the sandbox is gone: stdout and stderr (default true, 1 MiB per stream), and the absolute guest paths in files, read back when the command ends (8 MiB each).
notify a URL told when each run ends and when the job does (below).

A job is running while any run is queued or running, then succeeded, failed or cancelled. Each run is queued, running, succeeded, failed, timed_out or cancelled; a failed or timed-out attempt with retries left goes back to queued, and the run's attempts share its index.

job get ID shows each run's state, exit code, attempts and sandbox; job output ID N prints what run N kept of stdout (and stderr to stderr), and --file PATH one of its kept files. job cancel ID cancels it: runs not started never are, and running ones' sandboxes are terminated.

A finished job, and what it kept, is kept for a day (--job-retention), in --jobs-dir (jobs/ beside the state file). A restarted gateway picks its jobs up again: a running command is followed where it runs.

#Secrets

sandbox-cli secret set ANTHROPIC_API_KEY < key.txt   # the value from stdin
sandbox-cli secret ls                                # names and when each was set; never a value
sandbox-cli secret rm ANTHROPIC_API_KEY

A secret goes into a run's environment by name, only for jobs that name it; the API never returns it. Secrets need the gateway started with --secrets-key-file (32 random bytes, mode 0600), which seals the tenants' secrets and jobs' environments at rest; without it there are no secrets, and a job or service that names one is refused with 501. Setting or removing one needs a key with the secrets:write scope. The name is yours to choose: letters, digits and . _ -, starting with a letter or an underscore, at most 128, not a reserved variable; the value is at most 64 KiB, with no NUL.

A secret and the variable it fills are told apart. A spec says which variable each secret is set in, so one tenant can keep a key per purpose and give any of them to the variable a program reads:

sandbox-cli secret set openai-prod < prod.txt
sandbox-cli secret set openai-staging < staging.txt
sandbox-cli job agent-run codex "…" --secret OPENAI_API_KEY=openai-staging
secrets:
  - OPENAI_API_KEY=openai-prod   # secret openai-prod, as OPENAI_API_KEY
  - ANTHROPIC_API_KEY            # secret ANTHROPIC_API_KEY, under its own name

The variable is checked when the spec is sent, as an env name is: a valid name, not reserved (LD_PRELOAD=… is refused), not set twice, and not also in env. A bare name must itself be a variable name, so openai-prod alone is refused with the VAR=openai-prod to write instead. The job's record keeps the mapping, which is names only.

Secrets are the tenant's: any key of the tenant may name one in a job or a service, and so read it from inside its sandboxes. A tenant is the unit that shares secrets, and users with no tenant all share the default one; an organisation is a tenant, with secrets of its own. A service names them the same way (secrets: [NAME, VAR=NAME], services.md).

What a secret in a sandbox's environment is protected from, and what it is not, is in security/secrets.md.

#Private images

A job, a service or a sandbox whose image is private is pulled with the organization's login for its registry, kept beside its secrets:

sandbox-cli registry login ghcr.io -u alice < token.txt   # read:packages on ghcr.io
sandbox-cli registry ls
sandbox-cli registry logout ghcr.io

The token is sealed and never shown again, and is not a variable in any sandbox: it reaches only the node's pull. Another organization's create of the same image gets no login and is refused by the registry, even once the image is on the node. Studio sets them on its Secrets page; the routes are in api/v1.md.

#Notify webhooks

A job's notify URL is POSTed {"event": "run.finished" | "job.finished", "job", "name"?, "job_state", "run"?, "state"?, "exit_code"?} when a run ends and when the job does — never output or a value.

The address rules exist because the gateway makes the request, from inside the operator's network:

  • https to a public address only, checked when the gateway connects, after the name is resolved — so a name that resolves somewhere else by the time of the post is still caught, and a user cannot make the gateway call its own loopback, the nodes' network or a metadata address.
  • --notify-allow-private lifts this for hook receivers on a private network: the gateway then also posts to loopback, private and link-local addresses, and over http to loopback.

#Who may do what

Scope
start a job or an agent run, cancel one sandbox:create
list jobs, read a job, its output and kept files; list secrets sandbox:read
set and remove secrets secrets:write

Another user's job is not found. A running job whose owner holds no active key at all in that tenant is cancelled as DELETE /v1/jobs/{id} would: its running sandboxes are terminated, its queued runs never start, and the job and each run say cancelled: the owner's access was revoked. Every run also checks its owner before it makes a sandbox (operations.md).

#From an SDK and Studio

The Python and TypeScript SDKs have create_job/createJob, agent_run/agentRun, jobs, job, cancel_job/cancelJob, job_output/jobOutput, job_file/jobFile, set_secret/setSecret, secrets and delete_secret/deleteSecret (sdk/README.md). In Studio, Jobs submits and cancels jobs, filters them by state and label, and shows each run's kept output and files, and Secrets lists names and sets values in a password field that is never shown again (studio.md).

Edit on GitHubdocs/jobs.md