Skip to content
Early preview — you found Guuey before launch · official launch soon

Agents as code

read as .md

Your agent is a guuey.json and a prompt file. Check them into your repo, review changes in pull requests, and let CI converge the hosted agent on merge — the same way you would apply a Kubernetes manifest or a Terraform plan. Guuey’s side is one idempotent call: it converges the app’s access policy and deploys the artifacts, never rolls the pod when nothing changed, answers with the sha256 of what it persisted, and records which commit built it so you can read it back later.

guuey.json is the whole manifest. The agent section is what the pod runs (framework, model, prompt, MCP servers — see the CLI page); the app.access block declares the app record’s access policy, using the same field names as the platform API:

{
"schema": "1",
"appId": "7365f90e-…",
"agent": {
"framework": "claude-agent-sdk",
"model": "claude-sonnet-5",
"systemPrompt": { "file": "prompts/system.md" },
"mcpServers": {}
},
"app": {
"access": {
"userAuthMode": "byo",
"userAuthConfig": { "issuerUrl": "https://id.example.com", "audience": "console" },
"allowedDomains": ["https://console.example.com"],
"guestAccess": false
}
}
}
  • access is PUT-per-field: a field you don’t mention is never reset. "userAuthConfig": null is the explicit “clear the issuer binding”. Values are validated exactly like guuey apps update (userAuthModeanonymous | native_pool | byo; allowedDomains are bare domains or full origins, no wildcards).
  • Tier is not in the manifest. Plan/tier is the app’s, changed on the app’s billing page in the console (see Plans & billing); a CI credential never carries it. Live knobs that are not build material (maxPods, runtime updates) stay on guuey agent config — see Hosting for scaling and runtime-update semantics.
  • The agent.systemPrompt file reference is resolved by the CLI: it sends the file’s bytes alongside the document and Guuey inlines them byte-exact.

Built the agent in Studio first? guuey pull ejects the live definition into a local guuey.json and externalizes the prompt to prompts/system.md — check those in and manage the app from the repo from then on.

Run it from the directory that holds guuey.json:

Terminal window
guuey agent apply
✓ Applied — build #12 queued (provenance acme/agents@da49f6dcaabc).
snapshot: changed
config:
guestAccess: (unset) → false
sha256 agentDef: 5f2c…
sha256 prompt: 9a01…
sha256 snapshot: c7d4…
status: guuey deployments · /v1/apps/…/deployments/12/status

What one apply does, in order:

  1. Validates the document with the same schema and lints as guuey deploy (secrets in headers, reserved MCP names, tool-gate grammar, …).
  2. Computes the plan: does the assembled snapshot byte-match the newest active build, and does every declared access field already hold?
  3. If nothing changed → Unchanged — build #N (live) already serves these bytes. Nothing queued. The pod is not rolled. This is what makes a no-op CI run safe.
  4. Otherwise converges the differing access fields (fail-closed: a bad value is rejected before anything is written) and queues a new build.

Flags:

Flag Meaning
--dry-run Plan only. Prints the diff + hashes; exit 2 when there is drift, 0 when in sync, 1 on error. Nothing is written. Trigger-side gates (tier size ceilings, build caps) are enforced on the real apply.
--wait After applying, poll the build until it is live (or failed / superseded).
--provenance <p> auto (default): GITHUB_REPOSITORY/GITHUB_SHA when set, else git remote get-url origin + git rev-parse HEAD, path = cwd relative to the repo root. none: don’t record. <org/repo>@<sha>[:<path>]: explicit.
--json Emit the response verbatim (see below).
--app-id <id> Target another app — e.g. one manifest applied to a staging and a production app.

The --json response is the contract your pipeline gates on:

{
"applied": true,
"unchanged": false,
"dryRun": false,
"appId": "7365f90e-…",
"buildNumber": 12,
"status": "queued",
"deployedContentHash": {
"agentDef": "<sha256 of your guuey.json bytes>",
"systemPrompt": "<sha256 of your prompt file bytes>",
"snapshot": "<sha256 of the persisted snapshot — what the pod boots from>"
},
"provenanceRecorded": true,
"statusPath": "/v1/apps/7365f90e-…/deployments/12/status",
"plan": {
"snapshot": "changed",
"config": [{ "field": "guestAccess", "current": null, "desired": false }]
}
}

agentDef and systemPrompt hash your bytes verbatim — compare them with sha256sum guuey.json prompts/system.md and fail the job on a mismatch (“green means deployed”). snapshot hashes the exact string persisted on the build row: the platform assembles it from your document (prompt inlined, the app’s generative-UI binding injected) in a canonical form, so identical inputs always produce the identical hash — that is what unchanged compares.

Terminal window
guuey agent status
App: 7365f90e-…
Live: build #12 (live) · xs · deployed 2026-08-16T20:04:11.000Z
managed from acme/agents@da49f6dcaabc (agents/helper)
snapshot sha256 c7d4…
Config:
userAuthMode: "byo"
userAuthConfig: {"issuerUrl":"https://id.example.com","audience":"console"}
allowedDomains: ["https://console.example.com"]
guestAccess: false

Add --check to also compare this checkout byte-exact against the live build (a dry-run reconcile behind the scenes): ✓ Checkout is in sync with build #12 (byte-exact). (exit 0) or ✗ DRIFT — this checkout differs from build #12: followed by the diff (exit 2). A build that was deployed outside agents-as-code (guuey deploy, Studio) shows no provenance.

guuey deployments lists every build (--json includes each build’s provenance and hash); GET /v1/apps/:id/deployments/:n serves provenance and hash per build.

Re-serve a previous build’s exact bytes as a new build — no checkout, no rebuild, from anywhere with --app-id (why this can be byte-exact — immutable snapshots, row-driven serving — is explained in Deployments & rollback):

Terminal window
guuey agent rollback --to 7 --wait
✓ Rolled back — build #9 queued, re-serving build #7 byte-exact — bytes from acme/agents@1f0e2d3c4b5a (agents/helper).
sha256 snapshot: c7d4…

The source is build #7’s persisted snapshot — the string its pod booted from — so the new build serves what served then, byte for byte, and its row keeps #7’s provenance (guuey agent status then reads build #9 (live) (rolled back from #7), still managed from acme/agents@…). Every gate a fresh deploy passes runs again (schema, lints, tier and size ceilings, build caps). If the pinned bytes are already what is live, nothing is queued. Rollback refuses rather than approximates: a build whose stored snapshot would no longer re-serialize byte-identical (the platform’s canonical form moved since — a schema default, the app’s generative-UI binding), a code build, or a row without a snapshot answers 409 ROLLBACK_NOT_EXACT with the way out (guuey agent apply from that commit). Rollback re-serves build material only — it never touches the app’s access policy; if the manifest at that commit declared a different app.access, apply from that commit instead.

Flags: --wait, --json, --app-id <id>; the wire is POST /v1/apps/:id/reconcile/rollback { "buildNumber": 7 }{ applied, unchanged, rolledBackFrom, buildNumber, deploymentId, status, contentHash, provenance, statusPath }, callable with the same credentials as apply.

Do not put a personal login in CI. Mint an app-scoped service token once, as the app’s owner:

Terminal window
guuey tokens create --label "github-actions" --app-id <appId>

The secret (guuey_svc_…) is printed once. It is bound to that one app, may only call the reconcile routes (agent apply / agent status / agent rollback) and read that app’s deployment history — no tier, no billing, no delete, no other apps, and guuey deploy refuses it. It never expires; guuey tokens revoke <tokenId> is the kill switch. Store it as a CI secret and pass it through GUUEY_API_KEY.

A personal key via GUUEY_API_KEY also works (full user authority, real expiry) — fine for a laptop, wrong for a pipeline.

Workspace-owned apps: the CI credential is a workspace API key

Section titled “Workspace-owned apps: the CI credential is a workspace API key”

An app owned by a workspace (a team) cannot mint a service token — a token bound to whichever member minted it would outlive that member’s seat. Its CI credential is a workspace API key (guuey_wkz_…), which authenticates as the workspace itself: a workspace admin creates it in the console at Settings → Workspace → your workspace → API keys → Create key (/settings/workspaces/<workspaceId>/api-keys); the secret is shown once. Store it as a CI secret and pass it the same way, through GUUEY_API_KEYguuey agent apply | status | rollback and the raw reconcile routes accept it for every app that workspace owns. Its scope is fixed by the platform, not per key: the deploy plane (reconcile, deploy, deployment reads and status, env, logs, config, generative-UI asset push) — never app creation or deletion, listing, domains, slugs, branding, service-token or workspace-key management, or anything that needs a named member behind the call. Revoke it from the same console page. (No guuey wkeys CLI verb yet — creation is console-only.)

Two jobs: a plan on every pull request, an apply on merge to main. The same manifest can target one app per environment with --app-id.

name: agent
on:
pull_request:
paths: ["agents/helper/**"]
push:
branches: [main]
paths: ["agents/helper/**"]
env:
GUUEY_API_URL: https://api.us-east-1.guuey.com/v1
GUUEY_API_KEY: ${{ secrets.GUUEY_SERVICE_TOKEN }}
jobs:
plan:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm install -g @guuey/cli
- name: Plan (a diff is expected on a PR — exit 2 is the plan, only a real error fails)
working-directory: agents/helper
run: guuey agent apply --dry-run || test $? -eq 2
apply:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm install -g @guuey/cli
- name: Apply
id: apply
working-directory: agents/helper
run: guuey agent apply --wait --json | tee reconcile.json
- name: Drift gate — the bytes Guuey persisted are the bytes we reviewed
working-directory: agents/helper
run: |
want_def=$(sha256sum guuey.json | cut -d' ' -f1)
want_prompt=$(sha256sum prompts/system.md | cut -d' ' -f1)
got_def=$(jq -r .deployedContentHash.agentDef reconcile.json)
got_prompt=$(jq -r .deployedContentHash.systemPrompt reconcile.json)
test "$want_def" = "$got_def" && test "$want_prompt" = "$got_prompt"

GITHUB_REPOSITORY and GITHUB_SHA are set by Actions, so provenance is recorded automatically; guuey agent status on the app then reads managed from <org>/<repo>@<sha> (agents/helper).

The API, if you’d rather not run the CLI

Section titled “The API, if you’d rather not run the CLI”

Everything above is one route:

POST /v1/apps/:appId/reconcile Authorization: Bearer <service token | workspace API key | personal key>
{
"artifacts": { "guueyJson": "<document string>", "systemPrompt": "<prompt bytes, optional>" },
"config": { … same fields as app.access — optional; when absent, app.access in the document is used },
"provenance": { "repo": "org/repo", "path": "agents/helper", "sha": "<git sha>" },
"deploymentId": "<optional client-supplied id>",
"size": "xs — optional pod size, same values as guuey deploy --size",
"dryRun": false
}
GET /v1/apps/:appId/reconcile → { appId, live: { buildNumber, status, deploymentId, deployedAt, size, provenance, contentHash, rolledBackFrom } | null, config: { … } }
POST /v1/apps/:appId/reconcile/rollback { "buildNumber": 7 }
→ { applied, unchanged, appId, rolledBackFrom, buildNumber, deploymentId, status, contentHash, provenance, statusPath }

Errors are always JSON {"error": {"code", "message"}}, and the call is idempotent, so retries are safe. While an app is on its 7-day trial, GET /v1/apps/:id carries trial: { startedAt, endsAt, status } (null once the app is off the trial); a deploy or reconcile against an expired, unpaid app answers 409 TRIAL_EXPIRED — choosing a plan on the app resumes it, no redeploy needed. A guuey.json document must carry "schema": "1" at the root and only the documented sections — an unrecognized root key is rejected by name.

The root schema is a decimal string ("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 (SUPPORTED_GUUEY_JSON_SCHEMA in @guuey/config), and both refuse rather than guess: guuey agent apply (like guuey deploy) stops before any network call with [SCHEMA_TOO_NEW] and tells you to upgrade @guuey/cli when the document is newer than the CLI; the reconcile and deploy-trigger routes answer 400 SCHEMA_UNSUPPORTED when a document is newer than the platform serves. An older schema is accepted only when a migration to the current one exists — none does yet, so today the rule is simply “equal, or refused”. A document that passes locally and gets SCHEMA_UNSUPPORTED from the API means your CLI is ahead of the platform; wait for the platform to catch up rather than downgrading the file.

Slug, custom domain, store listing, and branding have their own ceremonies (claiming, DNS verification, uploads) and stay on their commands for now. Tier and billing never move to CI. Workspace-owned apps cannot mint service tokens — use a workspace API key (above).