Deploy from an agent
Let a connected AI agent publish an app in one tool call — from the files in the call itself, from a source archive it uploads, or from a commit in the repository your app is linked to — change a single file in an app that is already running, how the source travels, how you follow the build, and what your organization's deployment governance does to it.
An agent that can write your app but not publish it leaves you in a terminal for the last step. The platform connector closes that: with the deploy scope, a connected agent starts the deployment in one tool call and follows the build to a URL that answers.
Before that call, the agent can check its work: validate the manifest and preview what the deploy would apply with the read-scoped preflight tools, and after the deploy lands, run the app’s checks. See Check before you deploy for the whole flow.
There are three ways to give that call its source, and which one fits depends on where the agent runs — not on what you are deploying. All three end in the same deployment, with the same governance, the same limits and the same operation to follow.
This guide is about what that call does — and, just as importantly, what it does not do. It never approves anything.
What you need
- The platform connector connected and verified.
- A grant that includes the deploy scope. It is not part of the read defaults,
so you approve it deliberately: request
scopes = ["platform:deployments:execute"]in the client’s MCP configuration, start a new authorization, and approve it on the Dibbla consent page. - Source that is ready to deploy: a
Dockerfileat its root, or adibbla.yamlmanifest. The same rules asdibbla deployapply, because it is the same deployment path. - For the repository source below: the app connected to a GitHub repository in the Console. That connection is made by an organization admin, once, and the agent cannot make or change it.
Three sources, one deployment
| Your agent runs | Give the call | Why |
|---|---|---|
| In a chat with no shell at all — ChatGPT, or any hosted assistant | files | It wrote the code in the conversation; there is no disk to pack |
| Anywhere, when the app already runs and one file changes | files with mode: "patch" | Only what changed travels; the rest of the app is left alone |
| On your machine or anywhere it can reach the internet freely | archive | The source is on a disk it can read, and it can upload |
| In a cloud sandbox with restricted network access | source (repo and ref) | It can already push to GitHub; it does not need to reach us with bytes |
Send exactly one of the three. Sending two, or none, is refused with a sentence saying so rather than a guess.
Deploying the files themselves: no filesystem at all
A hosted assistant has no terminal, no disk and nothing to upload. It has the
files it just wrote. So it sends them: files is a list of
{ file, data, encoding } with mode: "replace", and the tool call is the
whole deployment.
{
"alias": "my-app",
"mode": "replace",
"files": [
{
"file": "Dockerfile",
"data": "FROM nginx:alpine\nCOPY index.html /usr/share/nginx/html/\n"
},
{ "file": "index.html", "data": "<!doctype html><h1>Hello</h1>" }
],
"idempotency_key": "1a2b3c4d-…"
}
encoding is utf-8 when you leave it out, which is what source text is;
base64 is there for the occasional binary asset. Paths are relative to the
source directory — Dockerfile, app/main.go — never absolute and never
climbing out of it.
There is nothing to upload afterwards: the answer is an operation id, and the build is already on its way.
You do not write any of that yourself. You ask:
Build me a tiny web page that says “Hello from Dibbla”, and publish it to my Dibbla app
my-app. Deploy it by sending the files in the call — you have no shell here.
The inline tree is capped at 200 files and 2 MB of decoded content. That
is generous for source — every template Dibbla ships is a few hundred
kilobytes of it — and deliberately far too small for a build. Over the cap,
the call is refused with a sentence naming the limit and pointing you at the
archive path or dibbla deploy. Build output and
dependencies do not belong in a deployment at all: exclude them, do not pack
them into a tool call.
A cloud coding session typically reaches package registries and GitHub, and nothing else — while its MCP traffic goes through its provider’s servers and always arrives here. So an upload URL on our origin is unreachable from that sandbox unless you add our domains to its allowed list. Deploying from a commit removes that problem instead of working around it: the agent pushes a branch, names it, and the platform fetches the source itself.
Deploying an archive: one call, not a sequence
The agent calls platform_deployment_start once, with the application alias
and the identity of the archive it has on disk — the exact byte size and the
SHA-256 of the tar.gz. The call returns immediately with:
- an operation id (
deployment:…) that outlives the conversation, and - a structured upload plan: where the client process sends the archive bytes, and in what size chunks.
The client then pushes the archive itself. The deployment starts on its own when the last chunk lands. Nothing about the build happens inside the tool call, and the archive never passes through the model — it is a byte stream on a separate, principal-bound endpoint, not a tool argument.
No field anywhere in the connector takes an archive’s bytes. A build archive
in a model’s context window is expensive, lossy and unnecessary; the upload
plan exists so the client process moves the bytes and the model only reads the
outcome. The inline files path above is a different thing — source the agent
wrote in this turn, small and bounded — and it is never where a tar.gz goes.
Resuming, and never deploying twice
Every call carries an idempotency key you choose once per intended deployment. Calling again with the same key returns the same operation and the same upload intent, so an upload that was interrupted resumes from the committed offset. It does not create a second deployment. The same key with a different archive is refused as a conflict rather than silently adopted.
Deploying a commit: nothing to upload
When the agent has pushed its work, it calls the same tool with the repository and a ref instead of an archive — the branch it just pushed, a tag, or a commit SHA. No bytes move from the agent at all.
Two things happen inside that call:
- The ref is pinned to a commit. The answer names the exact
commit_shabeing deployed, so a branch that moves afterwards — someone else’s push, or the agent’s own next commit — does not change what this deployment publishes. - The source is fetched by Dibbla, with Dibbla’s own credential for the connected repository. A private repository works exactly like a public one, and no token is ever handed to the agent, written into the operation, or visible to the model.
An app may only deploy from the repository it is connected to. A ref that does not exist, a repository the app is not connected to, and a repository in another organization all answer the same way — the deployment is refused as “no such repository and ref for this application”, because telling those cases apart would say which repositories exist.
The rest is identical to the archive path: the same idempotency key rule (the same key returns the same operation, so a retry is not a second deployment), the same operation to follow, the same governance, the same limits.
Changing one file in a running app
Publishing an app and fixing a line in one are different jobs. Sending the whole tree again to change one file is expensive, and it quietly overwrites whatever anyone else deployed while the agent was working.
So files has a second mode. With mode: "patch", the agent sends only the
files that change — each as its whole new content — and delete for paths
that should go. Everything it does not send stays exactly as it is, down to
the file mode, and the deployment is one commit on top of the revision your app
is running.
{
"alias": "my-app",
"mode": "patch",
"files": [{ "file": "index.html", "data": "<!doctype html><h1>Hej</h1>" }],
"delete": ["old-banner.html"],
"base_sha": "9f1c0b7a4d2e6f8091a3b5c7d9e1f3a5b7c9d1e3",
"idempotency_key": "5e6f7a8b-…"
}
The loop the agent runs is read → change → patch, and none of it needs a terminal, a download or the Console:
platform_files(action: read) returns the file as it is running, and therevisionit was read at.- The agent changes the file in its own context.
platform_deployment_startwithmode: "patch", that file, and that revision asbase_sha.
You ask for it in one sentence:
In my Dibbla app
my-app, the heading on the start page says “Hello” — change it to “Hej” and deploy it. Patch just that file; don’t re-send the whole app.
base_sha is a lock you can choose to hold
base_sha is the revision the agent read the files at. If the app has been
deployed since — by a colleague, by CI, by another agent — the patch is
refused, and the refusal names the revision that is current now so the agent
can read the file again and re-send. Nothing is merged and nothing is guessed.
It is optional. Left out, the patch applies to whatever revision is current at the moment it arrives, which is what you want when you are the only one deploying.
A patch sends the complete new content of each file it changes. There is no line-based diff, no hunk header and no context lines anywhere in the connector — a model that has to count lines gets them wrong, and matching a diff that does not quite fit is a failure we would rather not have. Files are the unit; a file that is 400 lines long travels as 400 lines.
Going back to an earlier version is not a separate tool. The history is
already there: platform_deployments lists one commit per deploy, and
platform_files reads any of them with a ref. Read the old content at the
revision you want and patch it back.
A delete path that is not in the revision being patched is refused rather
than ignored — an agent that believes your app holds a file it does not should
find that out before it deploys, not after.
Starting from the app that is already running
Both sources above assume the agent already has the code. Often it does not:
you ask a connected agent to change something in an app that has been running
for months, and it has never seen that source. dibbla clone is how a human
answers that — and an agent has no terminal to run it in.
If the change is small, you do not need this section at all: read the file with
platform_files and send it back with mode: "patch", as
Changing one file in a running app
describes. What follows is for the case where the agent wants the whole app on
a filesystem.
If the agent only needs to see the code — to answer a question, or to find
the file it is about to change — it does not download anything at all:
platform_files finds, searches and reads the deployed source directly, and
Connect an AI agent
describes those three actions. What follows is for the case where the whole app
has to land on a filesystem, because the agent will edit a checkout and deploy
it.
So the same source is a download. With the file-read scope, the agent asks for the application rather than for a file it once uploaded:
platform_fileswithaction: prepare_downloadand the app’salias(and optionally arefto start from a particular deploy rather than the latest one — a revision fromplatform_deployments, or the one a finished deploy reported as the revision it built). The answer is a principal-bound intent that declares the archive’s exactcontent_lengthandcontent_sha256.- The client process fetches the bytes itself from the intent’s
downloadfield, with the same access token it uses for MCP, and verifies them against thatcontent_sha256. An interrupted fetch continues with aRange: bytes=<the last byte it actually kept>-; the client’s own count is what a resume is based on, not the server’s, and it never starts over. A client with no filesystem hands the console link to you instead, exactly as an upload does. - The agent unpacks the
.tar.gz, changes what you asked it to change, and deploys the result through the archive path above.
That is the whole loop — clone, change, deploy — with no terminal in it. The download is the same transfer plane as the upload, so the same rules hold: no bytes and no base64 pass through the model, the link is not a presigned storage URL and carries no credential of its own, and an app in another organization or one you cannot reach is simply absent rather than refused.
The source comes from the Dibbla-managed repository written by your deploys, so it is the code that is running — not any uncommitted work on your laptop and not a fork on GitHub. An app that has never been deployed with version control enabled has no archive to hand out.
Following the build
The deployment is a durable operation, so it is followed with the same tools as every other long-running thing on the connector:
| Question | Tool |
|---|---|
| What phase is it in? | platform_operation (view: status, the default) |
| What is the build doing? | platform_operation (view: logs) |
| What happened, step by step? | platform_operation (view: events) |
| What is the app’s URL? | platform_operation (view: output) |
| Stop it before it reaches the cluster | platform_operation_cancel |
Build logs are paged and bounded — an agent reads a window of them, not a
whole BuildKit transcript. When the deployment succeeds, the output carries
the application’s URL. When it fails, the output carries the same build error
dibbla deploy would have printed: the failing step, the parsed compiler
diagnostics and a bounded log tail. A missing Dockerfile, an invalid
manifest or a port that is not a port are reported as what to fix, not as
“the deploy failed”.
A deployment can be cancelled while it is queued or still preparing. Once it has begun changing the cluster it is past the point of cancellation, and the connector reports a conflict rather than rewriting what already happened.
The deploy lands — unless your organization asks for review
By default a deploy from an agent goes straight out. The one call rolls the app out and the output carries its URL, for a brand-new app and for a change to one that is already running. Nothing waits for a person.
Review is something an organization turns on for itself, and when it is on the agent does not get to choose and cannot work around it.
| Effective model | What the one call does |
|---|---|
immediate (the default) | The deployment rolls out. The output carries the app’s URL. |
admin_four_eyes, member_four_eyes, admin_checkpoint | A pending deployment is created. Nothing is live until a person approves. |
Turning approval on
An owner or admin opens Console → Organization settings → Deployments, picks a review policy and saves. It applies to every app in the organization from that moment; a single app can be given its own policy on the app’s own settings. Switching back to immediate rollout is the same two clicks.
Two of the review policies need someone other than the author to approve, so in a one-person organization they cannot be satisfied — the Console says so next to them before you pick one.
Organizations that required admin four-eyes without ever having chosen it were moved to immediate rollout in September 2026: the requirement was written for them when the setting was introduced, not picked by anyone. The organizations affected were notified, and the Deployments settings page says so. Nothing was removed — pick a review policy there and approvals are back.
When the deployment came from a repository, the pending deployment in the Console names the repository, the ref and the commit SHA alongside the exact diff. The reviewer decides about a commit they can go and read, rather than about an opaque candidate.
When review is required, the operation’s output says so explicitly: it names the effective model, the pending deployment, and the Console link where an authorized person decides. The connector has no approve, deny, apply, close or retry tool — that is not an omission to be fixed later, it is the boundary. And the rule underneath is not enforced by the connector at all: Dibbla refuses to let the author of a deployment be the person who approves it, whatever role the author holds. Asking the agent to approve its own work therefore fails the same way it would fail for you in the Console.
A brand-new application — an alias that does not exist yet — is created directly. Governance describes how changes to an existing app are released.
The limits that still apply
Everything that bounds dibbla deploy bounds this, in the same words:
- Plan and trial limits. An organization at its app limit, or past the end of its trial, is refused before a single byte is uploaded, with the same error the CLI prints.
- Your role. A grant is a delegation, not a promotion. If your role in the organization cannot deploy, neither can your agent.
- Source size. The archive is capped at 50 MB, and a repository is held to
the same cap once packed. Exclude build output and dependencies with a
.dibblaignore. An inlinefilestree is capped much lower — 200 files and 2 MB decoded — because it travels through the model. Apatchobeys the same cap on what it sends; the app it patches may be any size. - Integrity and tenancy. The upload is verified against the SHA-256 you declared; an archive that does not match is refused and nothing is deployed. An archive belonging to another organization resolves to nothing.
- Resource defaults. A deployment that does not name
cpuormemorygets your organization’s configured defaults, exactly as the CLI does.
Troubleshooting
| What you see | What it means | What to do |
|---|---|---|
| The deploy tool is missing from the tool list | The grant does not include the deploy scope | Reconnect and approve the deploy scope explicitly |
| The operation stays queued and never builds | The archive has not finished uploading | Read the operation’s status; it carries the offset to resume the upload from |
IDEMPOTENCY_KEY_CONFLICT | That key already names a different deployment | Reuse a key only to resume the identical deploy; pick a new one otherwise |
| The output says a pending deployment was created | This app requires review | Open the Console link and have an authorized colleague decide |
| The archive was refused for its digest | The uploaded bytes are not the ones that were declared | Recompute the SHA-256 of the exact tar.gz and start a new deploy |
| The build failed with a compiler error | The build is the same build the CLI runs | Read the parsed diagnostics in the operation’s output and fix the source |
SOURCE_NOT_FOUND for a repo and ref | The ref does not exist, or the app is not connected to that repository | Push the branch, check the spelling, and check the app’s repository connection in the Console |
| The inline files were refused for the limit | The tree is over 200 files or 2 MB decoded | Deploy the same source as an archive, or with dibbla deploy from a machine that has it |
BASE_SHA_STALE on a patch | The app was deployed after the agent read the files | Read the files again at the revision the refusal names, and send the patch based on that one |
| A patch says there is no source to patch | The app has never been deployed with version control, or the alias is not yours | Deploy the complete tree once with mode: "replace"; patches apply to what a previous deploy stored |
SOURCE_UNAVAILABLE | This Dibbla installation could not reach the repository | Nothing was deployed and nothing in your source is wrong; retry, and tell us if it persists |