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.mdA 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
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
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"}'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:
DELETEremoves one. GET …/secrets/variablesandGET …/secrets/filesgive names and paths only. A value is read one at a time:GET …/secrets/variables/{name}, andGET …/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.ymlis.config/gh/hosts.yml.
Make a sandbox from it
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
templateIdout and the sandbox is ondefault. - The template's start says its size, what it starts from and how long it runs after each start.
size,snapshotIdandttlSecondsin the request win over it.snapshotId: nullasks 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
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
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.
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
sandboxIdsout, 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
failedand left as it was.appliedlists 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}/sandboxeslists the sandboxes on a template withupToDatefor each, andGET /v1/sandboxes/{id}/templatesays it for one.
A sandbox's own secrets
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}andfiles. - 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.
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"]}'{
"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 withhidden: true, its hosts and its stand-in.GET …/secrets/variableshas every name innamesand the ones kept out inhidden.GET /v1/templates/{id}has the same list ashidden, and so doesGET /v1/sandboxes/{id}/templatefor 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/policysayshiddenVariables: false, a variable sent withhostsis refused with501 hidden_variables_unavailable, and one already kept out stays out.
Narrow its hosts
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 andhosts. 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 andhosts. - The change reaches sandboxes as a new value would.
What its commands see
$ 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
.npmrcor~/.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, insideAuthorization: Basictoo, sogit pushover 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 underexample.comand notexample.comitself. - 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.ukand*.github.ioare. Name the hosts themselves, or a wildcard under a domain someone owns. GitHub's downloads are such a case:githubusercontent.comis on the list, so*.githubusercontent.comis refused, andraw.githubusercontent.comandobjects.githubusercontent.comare each named. - A host that cannot be one is refused with
400 invalid_host, andmessagesays 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
Hostheader 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
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
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
| Answer | Meaning |
|---|---|
404 not_found | No template of yours has that id, or the sandbox and the template are of different accounts. |
409 name_taken | The account has a template of that name, or the name is default. |
409 default_template | default is not deleted or renamed. |
409 template_in_use | Sandboxes are on the template. message says how many. |
409 not_on_template | apply named a sandbox that is not on the template. Nothing was applied. |
409 sandbox_not_running | apply named a sandbox that is not running. Nothing was applied. |
409 too_many_addresses | The template's outbound rule and a sandbox's own addresses would come to more than 100. Nothing was changed. |
409 not_hidden | PUT …/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_widened | PUT …/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_small | The start's snapshot was taken on a size with more disk. |
400 invalid_host | A host cannot be one, or there are none or more than 10. message says which and why. Nothing was changed. |
501 hidden_variables_unavailable | A 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]