gagarinDocumentation

Error contract

An agent reads these before any person does. So every error comes with a stable code to branch on and a hint at what to do next.

One shape, whatever broke — a wrong path, an oversized body, a timeout, or a bug of our own.

the shape
{
  "error": {
    "code": "approval_required",
    "message": "destroying project radar and everything in it needs human approval",
    "fix_hint": "we emailed you@example.com — ask them to open it, sign in, and approve (code F25T-YNG5). Then run the same command again. Expires in 15 minutes."
  }
}

Branch on the code, never on the prose. We reserve the right to reword the prose any time it reads badly; the code stays put, or it was never worth anything.

error.codestableRun the same call againapply_failed · timeout · rate_limitedFix the call, then run it againinvalid_port · no_such_service · image_not_yoursGo and tell your humanapproval_required · owner_only · project_suspendedof those three, only approval_required works once a human acts
fig. 6 · Pick the branch on the code, never on the message text. We reword the text whenever it reads badly; the code stays exactly where it is.

Every code

CodeWhat happenedWhat to do about it
unauthorizedThis machine has no credentials it can usegg login, give your human the link and code it prints, and run gg login again once they have approved. Do not ask your human for a token
authorization_pendingNobody has approved the gg login request yetPass on the link and code again if your human does not have them. Run gg login again once they say they approved — not in a loop
expired_tokenThe gg login request lived fifteen minutes and nobody approved itgg login for a fresh link, and pass it on straight away
access_deniedYour human declined the gg login requestAsk them why before running it again. Do not retry on your own
insufficient_scopeThese credentials do not carry that rightA browser session cannot deploy. Deploy from the CLI, with machine credentials
approval_requiredSomebody has to say yes to this firstA deletion, a released address, or a withdrawn dependency. We email your human a button; they sign in to the console and approve. Give them the code from the hint and wait. Do not poll — and one approval covers the fifteen minutes after it
approve_in_consoleApprovals are made by a person signed in to the consoleAn agent credential cannot approve, not even the account’s own. Your human opens the email and approves there
not_your_approvalThat approval was sent to another accountThe hint names the address. Your human signs out of the console and signs in as that account
approval_not_foundNo approval matches that linkIt expired or was withdrawn. Run the command that asked for it again, for a fresh email
no_consoleThis gagarin has no console to approve inNot your fault. It is an operator setting; tell your human
owner_onlyOnly the project owner can do thatGo and ask the owner. Nobody can grant you this, so retrying is wasted
insufficient_roleYou are a viewer — read onlyAsk the owner or an editor to give you write access
project_not_foundNo such project, or none you can seegg projects lists what you can reach; gg init PROJECT starts a new one
project_existsYou already have a project by that nameNames are unique inside an account. Pick another, or deploy into the one you have
invalid_nameThat name will not do2–30 characters: lowercase letters, digits and hyphens, starting with a letter
invalid_roleThe only roles are editor and viewerowner is not something you hand out — it is whoever pays
owner_not_a_memberThat address already owns the projectNothing to do. Their access is as complete as it gets
member_not_foundThat address has no accessgg members PROJECT shows who does
image_requiredYou did not say which imagegg build then gg push — or gg ship, which does both
image_not_yoursThat image belongs to another projectIt has to be in this project's registry. gg build and gg push put it there
invalid_digestThat does not look like a sha256Pass what docker push handed back, or pass nothing at all
invalid_portPort out of rangeGive the port the container actually listens on
invalid_kindThat is not a kind of thing gagarin runsA service, or a job. kind: job is how gg run says “this one ends”; nothing else exists
job_has_no_portA job listens on nothingDrop the port. If it serves, it is a service, not a job
job_has_no_volumeA job keeps nothing between runsDrop the volume. Durable data belongs in a resource the job reaches with --deps
not_a_jobThat name is a service, and a name is one kind foreverGive the job a name of its own. gg status PROJECT shows which is which
invalid_volumeThat volume path will not doAn absolute path inside the container, e.g. /var/lib/postgresql/data
volume_immutableA volume is set once and never movesLeave it where it is, or destroy and recreate — which throws the data away
invalid_domainThat is not a hostnameGive a name you own, e.g. shop.example.com. No scheme, no port, no path
domain_takenAnother service already holds that namePick another, or release it wherever it is declared. We will not say who has it — that is a fact about somebody else's account
domain_attachedYour own names are still declared on this serviceTake each one off first: gg domain rm PROJECT/SERVICE shop.example.com. Otherwise your DNS keeps pointing here after we stop answering for the name
invalid_needsThat dependency list does not parseName services in this project, e.g. gg deps add PROJECT/api db
invalid_depsThe same, on the --deps of a deploy or a runName things in this project. A job cannot be named as something to reach — it listens on nothing. --deps only ever adds; gg deps rm is how one is withdrawn
invalid_sizeNo such sizes, m or l — or leave it out to keep the one it has
service_in_useSomething still depends on thisCut the edge first: gg deps rm PROJECT/api db, which asks for its own approval — though the same click covers the destroy that follows. The refusal names what is depending on it
no_such_serviceThis project has no service by that nameDeploy it first. A deleted service takes its history with it
no_such_revisionThe service never had that revisiongg history PROJECT/SERVICE lists the ones it did
nothing_to_roll_back_toThere is nowhere to roll back toThe service has only ever been deployed once. This is not a bad call
unknown_resource_typeNo such resource typeThe hint lists the ones that exist: postgres, qdrant, valkey, external
wrong_resource_typeAnother type of resource already has that nameDestroy it and create the one you meant — which throws its data away
invalid_storageStorage size out of range1 to 100 GB, set once, at creation
no_storageThis type has no volume to sizeA valkey keeps everything in memory; an external runs nothing at all
no_sizeAn external has no container to sizeDrop --size. It is a set of values, not something we run
no_envThis type mints its own credentialsDrop the values and let us pick — a password you chose is one the running server has never heard of. gg resource secrets reads what we minted
invalid_envOne of those keys will not doEvery variable is published under the resource's own name, so write API_KEY rather than OPENAI_API_KEY — the doubled name is why this is refused
env_requiredAn external's values are yours to supplygg resource rotate PROJECT/NAME --set KEY=value to change one of them, or --env-file <file> to replace all of them
conflicting_envA rotation either replaces the values or amends them--env and --env-file say the bundle is now precisely this; --set and --unset change part of it and leave the rest. Send one or the other
no_such_keyThat external does not publish the key you asked to removegg resource secrets lists what it does publish. Name the key without the resource prefix — BASE_URL, not OPENAI_BASE_URL
already_existsThat external is already thereChanging what it publishes is its own call: gg resource rotate. Restating it from an old file would roll the key backwards, which is why this is refused rather than allowed
cannot_rotateThere is nothing recorded to replacegg resource secrets shows what it holds. Worth reporting — this should not happen
rotate_failedWe could not tell the server its new passwordNothing changed: the old credential is still in use, and this is safe to run again. gg status will say if the resource is not running
no_such_resourceThis project has no resource by that namegg status PROJECT lists what it has
not_tunnelableAn external has nothing to tunnel toNothing runs on our side of an external — it is a set of values, and gg resource secrets reads them
tunnel_unavailableNo running pod on the far end of the tunnelgg status PROJECT says whether the resource is running and why not, then gg connect again
cannot_delegateA minted credential cannot mint anotherMint it from the machine a human approved. A token that could issue its own replacements would make revoking a leaked one pointless
name_requiredA credential needs a nameName it after where it will live — it is what you read in gg credentials months later
name_too_longThat name is longer than anyone will read120 characters is plenty for something that appears in a list
invalid_expiryThat expiry is out of range1 to 365 days, or leave it out for 90. There is no never
not_a_resourceThat is a service or a job, not a resourceUse the /services/ path — a job is deleted and deployed down it too. gg status PROJECT shows which is which
not_a_serviceThat is a resource or a job, not a serviceYou cannot deploy a service over either, or give either a domain. A resource you reach by name from inside the project; a job you re-run with gg run
no_such_resourceNo resource by that namegg status PROJECT lists everything the project has
backup_unsupportedThis resource type keeps no backupsOnly postgres is backed up. A cache is a cache; if the data matters it belongs in postgres
backup_unconfiguredThis gagarin runs without a backup bucketNot your fault. Tell your human
restore_target_not_emptyThe restore target already holds dataA restore only fills a new, empty resource: gg resource restore PROJECT/new-name --source old-name. Nothing here overwrites a live database
backup_mismatchThat backup key belongs to a different projectgg resource backups PROJECT/NAME lists this one’s
no_backupsNothing stored for that source yetThe nightly pass takes the first one, or take one now: gg resource backup PROJECT/NAME
backup_failedThe dump itself failedThe resource must be running. gg status PROJECT, then retry once
backup_list_failedThe bucket could not be listedTry once more. If it happens again, tell your human
restore_failedThe restore itself failedThe new resource must be running first — restore does not resurrect a destroyed one. gg status PROJECT, then retry
apply_failedState was saved; the cluster would not take itRun exactly the same call again. It is idempotent
cluster_errorWe could not reach the infrastructureNot your fault, and not something you can fix. Tell your human
logs_unavailableThere is no pod yetgg status PROJECT first
no_such_routeWrong path or wrong methodCheck the skill. Do not invent endpoints
body_too_largeThat request body is over the limitBig values belong in secrets, not in a deploy call
timeoutWe gave up waiting on the infrastructureTry once more. If it happens again, tell your human
rate_limitedToo oftenWait a minute. Never retry in a tight loop
internal_errorA bug in gagarinTell us. The details are in the platform's log, not in yours
invalid_emailThe address given to gg share or gg transfer does not parseAsk your human for it again rather than guessing
email_failedThe approval email did not leave gagarinNothing was asked for. Try once more in a minute, then tell your human — nothing is waiting in an inbox to be found
email_cappedThat address has had twenty approval emails in a dayHave your human approve the requests already in their inbox, or wait a few hours
eject_failedThe export would not assembleThat is a bug in gagarin, not in your call. Tell us

Three refusals that get mixed up

approval_required means go and get your human, have them sign in and press a button, then run the same thing again — it will work the second time. owner_only means go and ask whoever pays the bill; there is no approval that opens that door for you. project_suspended means neither of those. Waiting will not help, retrying will not help, and the message says why: a balance that ran out, which the owner can top up, or an abuse finding, which needs a conversation with support. Three codes, because what happens next is three different moves.