Deployments & rollback
read as.mdEvery deploy of a Guuey agent — from Studio, guuey deploy, or
guuey agent apply — produces a build: an immutable
record that holds everything the agent runs from. This page explains what
that record contains, why the hash an apply hands back really is the hash of
what serves, and what the platform guarantees (and refuses to fake) when you
roll forward or back. For the commands and CI wiring, see
Agents as code; for what the pod itself looks like, see
Concepts: hosting.
The deployment snapshot
Section titled “The deployment snapshot”A build’s payload is its snapshot: your guuey.json, re-serialized into
one canonical string with the system prompt inlined byte-exact (a
{ "file": "prompts/system.md" } reference is resolved on your side and the
file’s bytes are embedded, so the snapshot is self-contained — no lookup back
into your repo). If your app uses Guuey’s generative-UI binding, that entry
is injected before the snapshot freezes, so the pod receives it the same way
it receives everything else.
Canonical form is what makes hashes meaningful. The platform doesn’t store your bytes as-sent; it parses the document against the schema and re-serializes it, which emits keys in a fixed order. The same logical document therefore always produces the same string — and the same sha256 — regardless of how your editor formatted it. Two consequences you can rely on:
- Idempotency on the reconcile path. An apply (
guuey agent apply) or rollback whose assembled snapshot byte-matches the newest active build queues nothing and does not roll the pod — a no-op CI run is genuinely a no-op.guuey deployand Studio are simple triggers: they always queue a new build, even for identical bytes. - A stable drift gate.
deployedContentHash.snapshotin the reconcile response hashes the exact persisted string; theagentDefandsystemPrompthashes cover your submitted bytes verbatim, so your pipeline can compare them againstsha256sumof the reviewed files and fail red on any mismatch.
Builds and their lifecycle
Section titled “Builds and their lifecycle”Builds are numbered per app, monotonically: each new deploy takes the
highest existing build number plus one. A build moves through queued →
(building for code-mode) → deploying → live; when a newer build goes
live, its predecessor is stamped superseded. failed and superseded are
terminal — no workload behind them, and they never block the next deploy.
The newest active build (queued, building, deploying, or live) is the
reference point: it is what “unchanged” and rollback compare against, and
during a rollout the old live row and the new in-flight row briefly coexist
until the handover completes.
GET /v1/apps/:id/deployments keeps the full history — every build’s
status, plus the snapshot hash and provenance for builds that came through
agent apply or a rollback (Studio and guuey deploy builds carry null
for both) — and each build has its own
…/deployments/:n/status endpoint your pipeline can poll to liveness.
Row-driven serving: the hash IS what serves
Section titled “Row-driven serving: the hash IS what serves”The deployment record is not a log of what happened — it is the input to
what runs. The pod boots by reading its snapshot from its build’s record;
there is no side channel through which configuration reaches a serving
agent. The runtime image is pinned on the record by immutable image
digest, never a mutable tag. For no-code apps the platform advances that
digest when it ships a runtime update — auto-update is the default, and
--runtime-auto-update off pins the app to the runtime captured at its last
deploy — always by rewriting the record first, so the record still names
what runs (runtime updates). Code-mode
images are per-build and change only with a new build.
That is why the snapshot hash in a reconcile or rollback response is a guarantee rather than a receipt: nothing can change what serves without writing a new build record with a new hash. If the hash you got back matches the bytes you reviewed, the agent that answers your users is running those bytes — today and until the next build goes live.
Rolling redeploys
Section titled “Rolling redeploys”Agent pods are long-lived, so a redeploy is a rolling update, not a restart: the new build’s pod starts alongside the old one, and the old pod is only taken out of service after the new one is ready. In-flight conversations on the old pod are drained, not cut off.
“Ready” is a real check, not a liveness ping. Each pod exposes a readiness
check that reports not-ready when the pod cannot do its job — a required
colocated tool degraded, or the pod draining for shutdown. A pod that
receives its shutdown signal flips to not-ready immediately and stays that
way; within one probe window traffic stops routing to it, and any request
that still lands on it during the drain gets a structured 503 with a
Retry-After and succeeds on a live pod on retry (see
Concepts: hosting).
Combined with snapshot idempotency, this gives the operational property CI depends on: an apply only ever rolls the pod when the bytes changed, and when it does, users never see a gap.
Rollback: byte-exact or refused
Section titled “Rollback: byte-exact or refused”guuey agent rollback --to <n> re-serves a previous build’s exact bytes as
a new build — a new number, a fresh rollout, but the pinned build’s
persisted snapshot as its payload. The platform does not “rebuild from that
commit”; it re-serves the string that build’s pod booted from.
The guarantee is strict: before queuing, the stored snapshot is run back
through the same canonical assembly a fresh deploy uses, and the result must
be byte-identical to what was stored. If it no longer round-trips — the
canonical form has moved since that build (a schema default was added, a
generative-UI binding the build pre-dated) — or the target is a code-mode
build or a record without a snapshot, the rollback answers
409 ROLLBACK_NOT_EXACT and names the remedy (guuey agent apply from that
commit). Guuey refuses to serve an approximation and call it a rollback.
One boundary to know today: a build whose snapshot pre-dates the app’s
generative-UI binding — deployed in the window before the binding was
provisioned, typically a deploy fired right after app creation — is refused
with ROLLBACK_NOT_EXACT; re-apply from the repo instead. Studio and
guuey deploy builds otherwise round-trip, because every deploy path
persists the same canonical, binding-injected form.
Three more properties worth knowing:
- Every gate re-runs. A rollback passes the same checks as a fresh deploy — schema, lints, tier and size ceilings, build caps. Old bytes don’t grandfather past current limits.
- Build material only. Rollback never touches the app’s access policy
(auth mode, allowed origins, guest access). If the old manifest declared
different
app.access, apply from that commit instead. - Provenance travels with the bytes. The new record carries the pinned
build’s
repo/path/shaverbatim — the bytes are that commit’s — plus a separaterolledBackFrommarker naming the build it re-served.
Provenance and the drift gate
Section titled “Provenance and the drift gate”Provenance (repo, path, sha) is an audit label, not a verified
fact: it is asserted by the caller at apply time and read back on the live
build and every historical one. Guuey does not check out your repo to
confirm it. The hashes are the truth — the drift gate exists so your
pipeline proves, cryptographically, that the persisted bytes are the
reviewed bytes, and provenance then tells a human which commit to look at.
Builds deployed outside CI (Studio, guuey deploy) simply carry no
provenance.
Schema versions: refuse, never guess
Section titled “Schema versions: refuse, never guess”guuey.json carries a root schema version ("1" today), bumped only when
the document shape changes in a way an older reader cannot interpret. The
CLI and the platform compare against the same constant, published in
@guuey/config, and both sides refuse rather than best-effort parse a
document from the future:
- The CLI stops before any network call with
SCHEMA_TOO_NEWand tells you to upgrade@guuey/cli. - The reconcile and deploy routes answer
400 SCHEMA_UNSUPPORTEDwhen the document is newer than the platform serves.
An older schema is accepted only through a real migration to the current one; none exists yet (there has only been one version), so today the rule is simply “equal, or refused”. The point of the stance is that a document is never silently reinterpreted: what a build means is fixed by its schema version, which is exactly what makes byte-exact rollback a meaningful promise.