Docs

Templates and secrets

Make sandboxes from a template, and give every one of them the same variables and files.

For your agent

agental.io/docs/templates.md

A template is what a sandbox is made as and held to, under one name: its start, which is a size and a snapshot or a new machine, its variables and files, and its outbound rule. Every account has a template named default, which a sandbox is on when it is made without naming one and which cannot be deleted.

  • A sandbox is on one template at a time.
  • A sandbox can have variables and files of its own too, on top of its template's. Where both name the same variable or the same path, the sandbox's own is used.
  • A sandbox can have addresses of its own too, which add to what its template's outbound rule lets out.
  • There is nothing above the template: no secret applies to every sandbox of the account but through a template.

Make a template

POST/v1/templates
curl -X POST https://agental.io/v1/templates \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"coding-agent"}'

It starts empty, at the size default starts sandboxes at, on a new machine. Its name is used once in the account. GET /v1/templates lists them, default first, with the names of their variables and files and how many sandboxes are on each.

Give it secrets

PUT/v1/templates/{id}/secrets/variables/{name}
curl -X PUT https://agental.io/v1/templates/{id}/secrets/variables/DATABASE_URL \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value":"postgres://user:[email protected]/app"}'
PUT/v1/templates/{id}/secrets/files
curl -X PUT https://agental.io/v1/templates/{id}/secrets/files \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":".npmrc","text":"//registry.npmjs.org/:_authToken=…\n"}'
  • A variable is in the environment of every command. Its name is letters, digits and underscores, and does not start with a digit.
  • A file is written into the sandbox before every command, at a path relative to /opt/agental. A file can be up to 10 MB.
  • Each call sets one. An empty value is kept as an empty value: DELETE removes one.
  • GET …/secrets/variables and GET …/secrets/files give names and paths only. A value is read one at a time: GET …/secrets/variables/{name}, and GET …/secrets/files?path= for a file.
  • A file's path goes in the query string, as path, because it may hold slashes. It is kept written one way: ./.config//gh/hosts.yml is .config/gh/hosts.yml.
WarningWhoever can run a command in a sandbox can read these values, unless a variable is kept out of the sandbox. Give a template only what its sandboxes need.

Make a sandbox from it

POST/v1/sandboxes
curl -X POST https://agental.io/v1/sandboxes \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Idempotency-Key: fix-login-bug-1" \
  -H "Content-Type: application/json" \
  -d '{"name":"fix-login-bug","templateId":"tpl_0123456789abcdef"}'
  • Leave templateId out and the sandbox is on default.
  • The template's start says its size, what it starts from and how long it runs after each start. size, snapshotId and ttlSeconds in the request win over it. snapshotId: null asks for a new machine whatever the start says, and a snapshot named in the request with no size gives the snapshot's size.
  • Its commands have the template's variables and files from the first.
  • It is held to the template's outbound rule from its first boot, with any addresses of its own on top.
  • A fork starts on the same template as its source, with the same secrets and addresses of its own.

Set its start

PUT/v1/templates/{id}/start
curl -X PUT https://agental.io/v1/templates/{id}/start \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"size":"small","snapshotId":null,"ttlSeconds":14400}'

ttlSeconds is how long its sandboxes run after each start: null is the deployment's default, and left out it stays as it was. snapshotId left out is a new machine. It is for sandboxes made from now on. A sandbox already on the template keeps its size, its disk and its time limit. A snapshot needs a size with at least the disk it was taken on. A new size has to be one your plan makes: 402 size_not_on_plan if it is not. The size it starts as already is kept, so its snapshot alone can be changed.

Set what its sandboxes may reach

PUT/v1/templates/{id}/network
curl -X PUT https://agental.io/v1/templates/{id}/network \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"outbound":"blocked"}'

open, blocked, or allowlist with the addresses in allow. Every template is open until this is set, default included. A change raises the template's revision, as a change to its secrets does. Outbound rules says what a sandbox on it is then held to.

When a change reaches a sandbox

A sandbox has its template's variables and files, and its outbound rule, as they were when it last started. A change to a template does not reach a sandbox that is running: it takes it when it next starts, or now when you apply it.

POST/v1/templates/{id}/apply
curl -X POST https://agental.io/v1/templates/{id}/apply \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sandboxIds":["sbx_2a9a47e50daec64a"]}'
  • Leave sandboxIds out, or send {}, to bring every running sandbox on the template up to date.
  • Nothing is restarted. The next command gets the variables as they are now, and the files are written to the disk at once, and in any case before the next command.
  • Each is held to the template's outbound rule from when the request answers, its server told first. One whose server does not take the rule is listed in failed and left as it was. applied lists the rest.
  • It is the only call that changes a running sandbox because of its template. A stopped sandbox named is refused with 409 sandbox_not_running: it takes the change when it starts.
  • GET /v1/templates/{id}/sandboxes lists the sandboxes on a template with upToDate for each, and GET /v1/sandboxes/{id}/template says it for one.

A sandbox's own secrets

PUT/v1/sandboxes/{id}/secrets/variables/{name}
curl -X PUT https://agental.io/v1/sandboxes/{id}/secrets/variables/API_TOKEN \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value":"a token for this sandbox only"}'
  • The same calls as a template's, under /v1/sandboxes/{id}/secrets: variables, variables/{name} and files.
  • One is used from the sandbox's next command, without a restart and without apply.
  • Remove one and the sandbox has its template's again, where the template has one of that name.

Keep a variable out of the sandbox

Set a variable with hosts and it is kept out of the sandbox. Its commands see a stand-in in place of the value, and the server the sandbox runs on puts the value in only on HTTPS requests to those hosts. The value is never on the sandbox's disk, so nothing that runs there can read it, root included.

PUT/v1/templates/{id}/secrets/variables/{name}
curl
curl -X PUT https://agental.io/v1/templates/{id}/secrets/variables/GITHUB_TOKEN \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"value":"ghp_…","hosts":["github.com","api.github.com"]}'
json
{
  "name": "GITHUB_TOKEN",
  "hidden": true,
  "hosts": ["github.com", "api.github.com"],
  "standIn": "agental_hidden_7hq2xk9mw4rz3st8vn1bq5c0dle6fy2p"
}
  • The answer has the hosts and the stand-in, and not the value. From now on nothing gives the value back, not the API and not the console, to you or to anyone. To change it, set it again.
  • Set again with a new value, it keeps its stand-in and takes the hosts sent this time. Set again without hosts, it is plain from then on, and its commands read its value.
  • GET …/secrets/variables/{name} answers with hidden: true, its hosts and its stand-in. GET …/secrets/variables has every name in names and the ones kept out in hidden. GET /v1/templates/{id} has the same list as hidden, and so does GET /v1/sandboxes/{id}/template for the variables a sandbox has from its template.
  • A sandbox's own variable is kept out the same way, under /v1/sandboxes/{id}/secrets. Where a sandbox has its own variable of a name its template has too, its own is used, kept out or not.
  • It reaches sandboxes as any other secret does: a template's at a sandbox's next start or when you apply it, and a sandbox's own from its next command. A fork has its source's own secrets, with the same stand-ins.
  • It is free on every plan. Where GET /v1/policy says hiddenVariables: false, a variable sent with hosts is refused with 501 hidden_variables_unavailable, and one already kept out stays out.

Narrow its hosts

PUT/v1/templates/{id}/secrets/variables/{name}/hosts
curl
curl -X PUT https://agental.io/v1/templates/{id}/secrets/variables/GITHUB_TOKEN/hosts \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"hosts":["api.github.com"]}'
  • The hosts that are sent take the place of the ones it had. Its value and its stand-in stay as they are, so the value is not sent again.
  • Each host has to be one it is sent to now, or a name under one of its wildcards. A new host is refused with 409 hosts_widened: only someone who has the value may send it somewhere new, so set it again with its value and hosts. A key that may change secrets cannot send a value it was never given to a host of its choosing.
  • A plain variable is refused with 409 not_hidden. To keep one out, set it with its value and hosts.
  • The change reaches sandboxes as a new value would.

What its commands see

shell
$ echo "$GITHUB_TOKEN"
agental_hidden_7hq2xk9mw4rz3st8vn1bq5c0dle6fy2p
  • The stand-in: agental_hidden_ and 32 lowercase letters and digits. It is the same in every sandbox on the template, and stays the same when the value is replaced.
  • Use it where the key would go: in a header, in a URL, or in a file such as .npmrc or ~/.git-credentials. On an HTTPS request to one of the variable's hosts, the server puts the value in place of the stand-in in the request's headers and its URL, inside Authorization: Basic too, so git push over HTTPS works.
  • In the answer, it puts the stand-in back in place of the value, in the headers and the body, so the value does not come into the sandbox that way either.
  • A file that holds the stand-in keeps working when the value is replaced, and an agent that has been running for hours sends the new value with its next request, without a restart.
  • The value is never on the sandbox's disk, so a snapshot or a fork holds only the stand-in, and so does an archived sandbox.
  • Sent anywhere else, the stand-in is worth nothing.

Which hosts

  • 1 to 10 for each variable: hostnames, as api.github.com, and wildcards, as *.example.com, which cover every name under example.com and not example.com itself.
  • Lowercase, with no scheme, port, path or IP address. An international name is written in its xn-- form.
  • A name on the Public Suffix List, its private section included, is refused: it is not one host but a place where anyone can have one. So is a wildcard directly over one, as *.com, *.co.uk and *.github.io are. Name the hosts themselves, or a wildcard under a domain someone owns. GitHub's downloads are such a case: githubusercontent.com is on the list, so *.githubusercontent.com is refused, and raw.githubusercontent.com and objects.githubusercontent.com are each named.
  • A host that cannot be one is refused with 400 invalid_host, and message says which and why.
  • Hosts do not open the outbound rule: a template that blocks everything still blocks the hosts listed here, and one held to an allowlist reaches them only if their addresses are on it.

Only over HTTPS, and only to its hosts

  • The value is put in only on HTTPS to port 443. A request over plain HTTP carries the stand-in out as it is.
  • The server opens only the connections to a variable's hosts, with a certificate authority made for that sandbox alone, which the sandbox trusts. Every other HTTPS connection passes through untouched and is never decrypted.
  • It checks the real host's certificate against the public certificate authorities, and puts nothing in when that fails.
  • A request whose Host header names another host than the one its connection was opened for gets nothing put in.

Tools that trust the sandbox's authority

The image puts each sandbox's authority in the system's store of certificates, and names it in NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE and DENO_CERT. Node.js, Python, curl, git, Go and Rust tools trust it with nothing to set, and Java does through the system's keystore, which ca-certificates-java keeps.

A tool that ships its own fixed list of authorities and reads none of these fails TLS to the variable's hosts. Tell it to use the system's certificates if it can, or keep that variable plain.

What a kept-out variable does not do

  • It does not change a request's body. A stand-in in JSON or a form is sent as it is: put the key in a header or the URL.
  • It does not change WebSocket frames after the upgrade. The upgrade request's headers are changed as any request's are.
  • It does not carry HTTP/2 or HTTP/3 to a variable's hosts. The server speaks HTTP/1.1 to them, so a client that would use HTTP/2 uses HTTP/1.1, and in a sandbox that holds a kept-out variable HTTP/3 falls back to TCP.
  • It does not get past a tool that checks a key's shape, by its length or its first characters, before it sends it. Such a tool may refuse the stand-in: keep that variable plain.

Put a sandbox on another template

PUT/v1/sandboxes/{id}/template
curl -X PUT https://agental.io/v1/sandboxes/{id}/template \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"templateId":"tpl_0123456789abcdef"}'

It has that template's variables and files from its next command, running or not, and is up to date with it. It is held to that template's outbound rule at once if it is running, and from its next start if it is stopped. Its size, its disk, and its own secrets and addresses stay as they are.

Delete a template

DELETE/v1/templates/{id}

A template that sandboxes are on is refused with 409 template_in_use, and no sandbox is moved off it: put them on another template first. default is never deleted.

What it does not do

  • A file removed from a template, or from a sandbox's own, is no longer written. A copy already written to a sandbox's disk stays there until it is deleted there.

Errors

AnswerMeaning
404 not_foundNo template of yours has that id, or the sandbox and the template are of different accounts.
409 name_takenThe account has a template of that name, or the name is default.
409 default_templatedefault is not deleted or renamed.
409 template_in_useSandboxes are on the template. message says how many.
409 not_on_templateapply named a sandbox that is not on the template. Nothing was applied.
409 sandbox_not_runningapply named a sandbox that is not running. Nothing was applied.
409 too_many_addressesThe template's outbound rule and a sandbox's own addresses would come to more than 100. Nothing was changed.
409 not_hiddenPUT …/hosts named a plain variable, whose commands read its value. To keep it out, set it with its value and hosts. Nothing was changed.
409 hosts_widenedPUT …/hosts named a host the variable is not sent to now. To send it somewhere new, set it again with its value and hosts. Nothing was changed.
400 size_too_smallThe start's snapshot was taken on a size with more disk.
400 invalid_hostA host cannot be one, or there are none or more than 10. message says which and why. Nothing was changed.
501 hidden_variables_unavailableA variable cannot be kept out of its sandbox here, as GET /v1/policy says with hiddenVariables. Set it without hosts for its commands to read it. Nothing was changed.

The full machine-readable reference is OpenAPI 3.1. Every page as Markdown, for an agent: llms.txt. Questions: [email protected]