Docs / Reference
Security model
The credentials AgentServe issues and what each can do, how secrets are stored, headers, webhook signing, rate limits and what is not production-ready.
AgentServe lets anyone deploy without an account, and lets a human take over the result later. This page lists the credentials that make that work, what each one grants, how the server stores them, and where the current version falls short.
Credentials#
| Credential | Format | Who gets it | What it can do |
|---|---|---|---|
| Manage token | ast_anon_ + 32 URL-safe characters | Whoever creates the stack, once,
in the create response. The CLI and MCP server keep it in .agentserve/stack.json. | Read, redeploy, extend, and delete one stack; read its logs and events. It cannot claim the stack. |
| Claim code | CLM-XXXX-XXXX-XXXX | The creator of an anonymous stack, once, inside
claim_url. | Take ownership of the stack, once. Anyone holding it can claim, unless the stack has a
claim_email. |
| API key | as_live_ + 32 URL-safe characters | The account holder, shown once at
sign-up (or when an account is created on the claim page) and on each rotation at /account. | Create
stacks in the account, manage every stack the account owns, claim stacks with a claim code, read
/v0/me. |
| Session cookie | agentserve_session | The browser, after sign-in, sign-up or a claim. | Use the dashboard: view, restart, revoke and delete the account's projects; rotate the API key. |
| Webhook secret | whsec_ + 32 URL-safe characters | The creator, once, when the
manifest sets notify.webhook. | Nothing on the server. The receiver uses it to verify signatures. |
Manage tokens after a claim#
Claiming does not revoke the manage token: the agent that deployed the stack can keep redeploying it, which is
usually what the human wants. The owner can cut the agent off from the project's settings page in the dashboard
(Revoke agent token). That clears the token's hash, emits stack.agent_revoked, and from then
on only the owner's API key and session can manage the stack. Deleting a stack also clears its token and claim
code.
Claim codes#
- 12 characters drawn with
secrets.choicefrom a 31-character alphabet without0,O,1,IorL, so codes survive being read aloud. That is about 59 bits. - Input is normalized before lookup: any case, any spaces or dashes, with or without the
CLMprefix. - Single use. The claim is an atomic update that clears the code's hash, so two concurrent claims can't both succeed.
claim_email(manifest, form field or--claim-email) restricts claiming to the account with that email, compared case-insensitively. AgentServe does not send email: the field restricts, it doesn't notify.- Claim pages are served with
Referrer-Policy: no-referrer, so the code in the URL isn't leaked to other sites.
Storage#
| Secret | Stored as |
|---|---|
| Manage tokens, claim codes, API keys | HMAC-SHA256 of the value, keyed with the server secret. The plaintext is never stored, so lost values can't be recovered, only replaced. |
| Passwords | scrypt with a random 16-byte salt, N=2^14, r=8, p=1, stored as
scrypt$<salt>$<hash>. Compared in constant time. At least 8 characters. |
| Webhook secrets | Plaintext in the database, because the server needs them to sign every delivery. |
| Sessions | Not stored. The cookie is <user id>.<issued at>.<signature>,
the signature being HMAC-SHA256 with the server secret, truncated to 32 hex characters. |
Rotating the server secret invalidates every token, claim code, API key and session at once.
Sessions#
The cookie is HttpOnly, SameSite=Lax and Secure. It is valid for 30 days from sign-in. Signing out deletes the cookie in the
browser, but there is no server-side session list: a copied cookie stays valid until it expires or the server secret
changes. After sign-in, the next redirect is only followed for paths starting with a single
/.
HTTP headers#
Every response from the control plane (API, claim pages, dashboard) carries:
| Header | Value |
|---|---|
Referrer-Policy | no-referrer |
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
Content-Security-Policy | frame-ancestors 'none' |
Responses from deployed services are passed through without these headers; the service controls its own. The
proxy adds X-AgentServe-Stack: <stack id> to every service response and, while the stack is
unclaimed, X-AgentServe-Unclaimed: deployed by an AI agent, unverified. Each service has its own
hostname, so service pages can't read the control plane's cookies, which are scoped to the control-plane host.
Uploads#
- Uploads larger than the size limit are rejected before extraction.
- Archive entries whose path resolves outside the extraction directory reject the whole upload. Symlinks and hard links that point outside it, and device files, are skipped.
- A service's
pathmust resolve inside the upload.
Webhook signing#
Each delivery carries X-AgentServe-Signature: t=<unix>,v1=<hex>, where v1
is the HMAC-SHA256 of "<t>.<raw body>" keyed with the stack's webhook secret. Receivers
should compare in constant time and reject timestamps outside a few minutes. See
Events for a verification snippet.
The webhook URL is taken from the manifest as is. The server will POST to any URL it can reach, including addresses on its own network.
Rate limits#
| What | Limit per client IP |
|---|---|
| Anonymous stack creation | 30 per hour |
POST /v0/claim | 10 per minute |
| Sign-in, sign-up and claim-form submissions (combined) | 10 per minute |
| Unclaimed stacks building or live | 3 at a time |
Rate limits are sliding windows. Details in Limits.
Not production-ready yet#
The current version is an early MVP. Don't deploy anything you can't afford to lose or expose.
- No isolation. Services run as plain processes under the server's user. A deployed service can read and write anything that user can, including the database, the server secret and other stacks' files, and can reach the network without limits. A real deployment needs containers or microVMs (Firecracker, gVisor) with CPU, memory and network limits.
- Build commands are arbitrary shell.
build,startand npm install scripts run with the same privileges as the services. - Single node. One process, one SQLite database, local disk. No replication, no failover. Rate limits live in memory.
- No managed add-ons. No Postgres or Redis; services keep state in
$DATA_DIR. - No email.
claim_emailonly restricts who can claim; nobody is notified. Account emails are never verified, and there is no password reset. Events go tonotify.webhook. - Basic abuse controls. A per-IP cap, the TTL and in-memory rate limits. Public anonymous hosting also needs phishing and malware scanning and a way to report a stack.
- No resource quotas. Disk use, memory and CPU per stack are not limited.