Sending a large project
Inline files are simplest, but a real project on disk should not be base64'd through your context. begin_upload streams a tarball straight to agenthost instead.
Inline files are the simplest thing that works, and the right choice for source you just wrote.
They are a poor fit for a project that already exists on disk: every byte has to be base64-encoded
into the request, which means reading the whole thing into your context first.
This path needs a shell
begin_upload hands you a URL to stream bytes at, which means running tar and curl (or an
equivalent raw HTTP PUT) on the machine holding the files. If your client cannot run shell
commands, stay on inline files.
For that, call begin_upload. It returns a single-use url and an
upload_id. Stream a gzipped tarball of your project root at the URL — the bytes go straight over
HTTPS, so they never pass through the conversation:
tar czf - --exclude=.git --exclude=node_modules --exclude=.env -C ./my-app . \
| curl -sSf --upload-file - "$UPLOAD_URL"Then pass upload_id wherever you would have passed files — to
deploy_app, or to create_app,
which infers the runtime from the uploaded tree just as it would from inline files. The two are
mutually exclusive: send one or the other.
Archive rules
- Paths are relative to the archive root. Create it from inside your project
(
tar czf - .) or with-C, so the tarball holdspackage.json, notmy-app/package.json. - Regular files only. Symlinks are refused rather than followed, as are hardlinks and device
entries. Use
tar --dereferenceif your tree relies on symlinks. .git/is dropped. It is never part of a built app.- Plain
tarworks too — gzip is optional, just larger on the wire.
What a ticket is bound to
A ticket is not a general-purpose upload slot. It carries three bindings, all enforced when the deploy claims it:
- One person. It deploys only for the user who called
begin_upload— not for anyone else in the same organization. - One app. Pass
service_idand the ticket deploys to that app and no other. Omit it and the ticket can only be used bycreate_appto make a new app; it can never overwrite an existing one's source. - One deploy. Claiming it consumes it. Deploying again means a fresh ticket and a fresh upload.
Send immediately — the url is a credential
The upload URL expires two minutes after minting, because it is handed to an agent and
therefore lands in a model's context and whatever transcript that is written to. Run the tar | curl right away. Once your bytes land, the clock changes: you then have 15 minutes to deploy,
because what identifies the upload from that point is a non-secret id with the bindings above.
Exclude what you would not commit
Dependencies and build output are installed and built on our side. Shipping node_modules,
.venv, dist or .next will blow the file-count limit and slow the build down for no benefit —
exclude them the way you would in a .gitignore.
Watch for secrets
tar czf - . sweeps up dotfiles, including a local .env. Everything in the archive becomes part
of the app's source. Exclude it — --exclude=.env — and set those values at
/secrets-manager instead, where they are stored apart from your source
and injected at run time.
Size limits
| Path | Limit |
|---|---|
Inline files | 5 MB of files |
| Upload ticket | 25 MB of files, 500 files |
The inline limit is the smaller of the two on purpose, and going past it returns an error that points here rather than a transport failure. If you are near it, use an upload — it is not a workaround, it is the intended path for anything project-sized.
How deploying works
The contract between your source and agenthost — how a deploy runs, how the runtime is chosen, the one entrypoint each runtime looks for, and how to confirm a deploy actually serves. Read this before your first deploy.
Deploying from GitHub
Connect a repository once and every push deploys. agenthost clones the branch, builds it and ships it, with no CI to write and no secrets to hand over.