Skip to content

Agent contract

The machine contract for agents and automation driving APCO Cloud — via the apco CLI, the MCP server, or raw HTTP. This page is the normative summary; the exhaustive command/payload listing lives in the repository's agents.md and in the reference section.

Constraints

  • Use --json for every command whose output you parse. Stdout is exactly one JSON envelope; human diagnostics and progress go to stderr.
  • Never print, log, or commit environment variable values, API keys, .env* files, or ~/.config/apco/config.json. env pull writes values to a file — do not cat that file into logs.
  • Pass --yes only for intentionally destructive operations (projects delete, db disable) or for deploy automation that must not prompt. Without --yes, destructive commands in --json/non-TTY mode fail with DESTRUCTIVE_CONFIRMATION_REQUIRED (exit code 6) instead of prompting.
  • Do not guess project ids. Read them from apco projects list --json or apco status --json; the CLI itself resolves the current directory's project from .apco/state.json.
  • Do not guess node ids or infer them from project placement output. Admins discover nodes with apco admin nodes list --json, then use the exact id for lifecycle operations. Project and CI responses intentionally omit node topology.
  • On failure, read error.code and error.hint before retrying. Auth errors (CLOUD_UNAUTHORIZED, CLOUD_NOT_LOGGED_IN) will not fix themselves by retrying.
  • Do not blindly retry a node action, placement failure, stale report, fence barrier, or indeterminate claimed job. Re-read node/status detail and act on the returned machine code; a retry must preserve the same authoritative ids and generation or restart discovery.
  • --follow streams and is incompatible with --json (rejected with CLI_INVALID_USAGE); poll apco logs --json / apco status --json instead.

Authentication

Three ways, in order of preference:

  1. Interactive: apco login runs a device flow (browser approval), mints a full-access API key, and stores it in ~/.config/apco/config.json as a token ref (literal: by default; env:NAME, file:PATH, and dotenv:PATH:VAR refs are also supported in that file).
  2. CI / headless: set APCO_TOKEN=<api key>. It takes precedence over the stored login. Create scoped keys in the dashboard (Tokens page).
  3. Raw HTTP: send the key as an x-api-key header to https://apco.space/api/v1/....

apco whoami --json verifies credentials (source is "env" when APCO_TOKEN is in effect). apco logout revokes the CLI key.

Platform URL authority

Project and deployment url fields returned by the control plane are authoritative. Do not derive app hosts from the API hostname: an installation may keep dashboard/API/auth on one control-plane domain while Production, Dev, and Preview use another content domain. Project payloads may include legacyUrl while an older content wildcard remains a serving alias. The examples in this documentation use APCO's current hosted defaults.

The JSON envelope

Every CLI command in --json mode emits one of two shapes (produced centrally in the CLI's src/output/envelope.ts — no command hand-rolls its output):

json
{ "ok": true, "operation": "<command>", "warnings": [], "...": "command payload" }
json
{
  "ok": false,
  "operation": "deploy",
  "error": {
    "code": "CLOUD_UNAUTHORIZED",
    "message": "Invalid API key.",
    "hint": "Run `apco login` again or provide a valid APCO_TOKEN."
  }
}

error.hint is optional but usually present, and states the recommended next action. MCP tool errors carry the same code + hint text.

Redaction

Success payloads pass through a redaction filter before printing: any field whose key looks secret — token, tokenRef, password, passwd, secret, apiKey, privateKey, value, and the like — is replaced with "[redacted]", recursively. Env-var reads return names only. Assume anything the CLI prints on stdout is safe to store in a transcript; never assume the same about files it writes (env pull).

Exit codes

CodeMeaning
0Success
1Generic/cloud error
2Usage error (bad flags, missing manifest/config/token, not logged in)
4Deployment failed or timed out
5Verification (health check) failed
6Destructive confirmation required (missing --yes)

Error codes

The most common error.code values and how to react (full table):

CodeAction
CLOUD_NOT_LOGGED_INRun apco login or set APCO_TOKEN.
CLOUD_UNAUTHORIZEDKey invalid/expired or authentication was rejected. Don't retry unchanged.
CLOUD_FORBIDDEN_SCOPEToken lacks the required scope/project grant or has an invalid CI-token shape. Replace the credential.
CLOUD_NOT_FOUNDProject/deployment doesn't exist, or no project resolvable from cwd — pass --project.
CLOUD_UNAVAILABLENetwork failure or 5xx; retry later.
CLOUD_REQUEST_FAILEDBusiness error; the message carries the server detail (quota, slug conflict, suspended, ...).
MANIFEST_NOT_FOUND / MANIFEST_INVALIDRun apco init, or fix apco.yml (the hint lists schema issues).
DATABASE_NOT_READYCheck the named branch with apco db status --channel <channel> --json; create it with apco db branch <channel> if appropriate.
DEPLOY_FAILED / DEPLOY_TIMEOUTInspect apco logs --json.
VERIFY_FAILEDDeployed, but the health path never returned 200.
DESTRUCTIVE_CONFIRMATION_REQUIREDRe-run with --yes if intended.

Server-side machine codes surfaced through CLOUD_REQUEST_FAILED (or directly over HTTP) include QUOTA_EXCEEDED, SLUG_CONFLICT, ROLLBACK_UNAVAILABLE, SUSPENDED, FORBIDDEN, FORBIDDEN_SCOPE, PLAN_FEATURE_UNAVAILABLE, RATE_LIMITED, UPLOAD_TOO_LARGE, OCI_REFERENCE_INVALID, EMAIL_NOT_VERIFIED, DATABASE_PROVISIONING, DATABASE_NOT_READY, DATABASE_ENGINE_CONFLICT, DATABASE_ENGINE_UNAVAILABLE, DRY_RUN_UNAVAILABLE, and QUERY_ERROR. OCI terminal failures additionally carry safe deployment errorCode values such as OCI_AUTH_FAILED, OCI_IMAGE_TOO_LARGE, and OCI_PLATFORM_UNSUPPORTED; inspect status/logs and do not retry policy/auth errors unchanged.

Token scopes

API keys carry a scope list (dashboard → Tokens). A key with no scopes is full-access — the apco login default. Grant agents the minimum they need:

ScopeGrants
projects:readList/read projects, deployments, status
projects:writeCreate/rename/delete projects and mutate database branches
deployCreate deployments, rollback, code-only promotion
env:readRead env vars
env:writeWrite env vars
logs:readBuild + runtime logs
jobs:readSafe scheduled-job definitions, history, and attempt metadata
jobs:writeEnable/disable, manual run, and cancellation controls
metrics:readMetrics, resources, analytics
lifecycleStop/start
database:queryRun SQL against a selected branch (also satisfied by projects:write)
database:credentialsFetch selected-branch credentials (also satisfied by projects:write)
ci:deploySetup-only: deploy one project and poll/read only deployments created by that same key

ci:deploy must be the token's only scope and must carry exactly one project id. Create it from the project's GitHub Actions setup card; it is not offered in the generic combinable-scope picker.

Admin (/admin/*) and account (/account/*) endpoints require a session or a full-access key — scoped keys are always rejected there. See Token scopes.

HTTP API

Base https://apco.space/api/v1, header x-api-key: <key>. Responses are { "ok": true, ... } or { "error": { "code", "message" } }-style JSON. Async actions (deploy, rename, stop/start, rollback, database provision/drop) return 202 with a jobId/deploymentId to poll. The full route table is in the HTTP API reference.

Database-bound commands and routes use production | preview | dev; omission defaults to Production for compatibility. Treat channel and engine in every JSON result as part of the safety contract. The first branch fixes postgres or mariadb for the project. Postgres alone supports rollback dry runs; never retry MariaDB DRY_RUN_UNAVAILABLE by silently running the write normally. apco dev --db always uses Dev, and promotion reuses code only while resolving Production data.

When apco.yml configures release.command, deployment results include a safe release summary (configured, status/timestamps/exit code/error/skipped reason) but never the command or environment. Poll it together with deployment status and inspect redacted [release] lines through apco logs --json. Promotion runs a fresh Production release; rollback never replays the historical command and reports skipped. A release failure leaves the previous route serving.

When apco.yml configures jobs, use apco jobs ... --json or the scheduled-job MCP tools. Definitions and runs expose only safe schedule/provenance/state; commands, pinned image references, environments, and container identities remain internal. A manual run is asynchronous, so poll its stable logical run id. Job logs require both jobs:read and logs:read; apco jobs logs --follow cannot be combined with JSON.

When app.type is dockerimage, apco deploy and MCP parse the manifest before packing and send JSON with no source archive. Treat returned artifact.digest as the immutable identity; promotion, Production rollback, and start do not resolve the requested tag again. apco registry set reads its secret from a hidden prompt/stdin, and list/API/MCP responses return only exact host/timestamp metadata—never username, password/token, or ciphertext. OCI has Production/Preview but no Dev or GitHub App source-deploy path.

When the cluster scheduler is enabled, deployment status may add safe portableArtifact and placement summaries. They contain immutable artifact state and tenant-safe placement/route convergence only—never a node id, address, task payload, lease verifier, repository credential, or sealed secret. Legacy responses and scheduler-disabled installations omit these optional fields.

Platform admins use apco admin nodes list|show ... --json for read-only discovery and cordon, uncordon, drain, or cancel-drain for ordinary lifecycle control. force-fence requires both --reason and --yes; remove requires --yes. Never pass --yes until the exact node id and current state have been read. Node enrollment material is created only with apco admin nodes bootstrap-token create --file <new-path> --json; the one-time value goes directly to a mode-0600 file and never to stdout. MCP exposes only the read-only cluster_nodes and cluster_node diagnostics, not node mutations.

Ingestion

  • /llms.txt at the docs root is a compact machine-readable index of the platform (manifest schema, MCP tool list, key concepts) — load it into your agent's context instead of scraping these pages.
  • The repository ships the same contract as agents.md at its root; apco.yml's schema is on the manifest reference page.

See also

APCO Cloud — ship apps with one command.