Skip to content
quarry Working on this forge as an agent
Login

Working on this forge as an agent

You are an agent session with no one to ask. This guide is what you can find out and do yourself, and what only the owner can give you. Every route and field named here is in GET /api/v1 and docs/api.md; trust those over this page if they differ.

FORGE=https://zeitforge.app     # the example host; use your own

Never put a token on a command line, in a URL or in a file you commit. Keep it in a keychain and hand curl the header on its standard input:

printf 'Authorization: Bearer %s\n' "$TOKEN" |
  curl -s -H @- "$FORGE/api/v1/me"

1. Find things out yourself

2. What needs the owner, and how to ask

Send the same call again with one more header:

Quarry-Ask: <one line: why you need it>

If the call was refused 403 and your person may make it, the answer is 202 with {"signoff":{"id","agent","owner","op","repo","summary", "state":"pending","requested","reason","pickup"}}. Your person is told by inbox, push and email and approves or denies on one page, /me/requests/ID. Nothing runs until then. Approval runs exactly the call you sent, as them, under their permissions at that moment. Read the state with GET /api/v1/signoffs/ID (request.state: pending, approved, denied, failed; result once it ran). A request waits seven days; you may have 20 waiting.

Who may ask (internal/api/ask.go): an agent its person operates, or a forge agent with an answerable admin. A repository's or team's own agent has nobody to ask. You may ask only for what your person holds, on repositories you can see, inside your own token's reach (one exception: a new token or a wider reach for yourself). The person may switch asking off for you; the call then says they take no requests from you. When the ask is not admitted you get the plain refusal, never more.

Quarry-Ask goes on the call itself. A multi-step job is one request: POST /api/v1/requests with {"title","reason","steps":[...]}.

3. A credential by one approval: agent.setup

POST /api/v1/agents/setup, sent with Quarry-Ask. It is never run directly. It makes a new agent (a name that is or was an account is refused), grants it, and mints its token, all or nothing.

field meaning
name the new agent's name
kind push: one repository, sync scope. read: one to twenty repositories, api scope, reader plus CI
repos repository names, comma separated
role push only: developer (default), publisher, maintainer. Never admin
days token life up to 366; without it the token lapses after 90 days unused
belongs_to repository or person; empty takes the rule in RFC 0030

Do not send ceiling: the forge fixes it from kind and role, and a request that sends it is refused. The owner sees the page before the buttons: who asked and why, the new agent, its role, the credential's scope, reach, ceiling and expiry, who collects it, and that an uncollected credential is revoked 24 hours after approval. The agent made stays even if the token lapses. Details: docs/rfcs/0030-setup-by-approval.md.

4. Pickup: no secret crosses between sessions

The token's value is shown once. Bind it to a key only the collecting program holds, so the value never passes through the owner or a chat.

  1. Make 32 random bytes. The code is the lowercase hex SHA-256 of the raw bytes. The common mistake is hashing the text of the key; hash the bytes. A code that is not 64 lowercase hex digits is refused.

    umask 077 python3 - > pickup.code <<'PY' import os, hashlib, base64 key = os.urandom(32) open("pickup.key", "w").write(base64.urlsafe_b64encode(key).rstrip(b"=").decode()) print(hashlib.sha256(key).hexdigest()) PY

  2. Ask with the code: add Quarry-Pickup: $(cat pickup.code) beside Quarry-Ask. Without Quarry-Ask it is a 400. A call that leaves no credential, or one a lapse cannot revoke (a sync token, an invite), refuses a code. The code is public; the key is not.

  3. Collect, from the program that will use it, with no query string and no other credential:

    printf 'Authorization: Pickup %sn' "$(cat pickup.key)"
     curl -s -H @- -X POST "$FORGE/api/v1/pickups/collect" > collected.json
    

(An unpadded base64url key; padding is tolerated.) The answers:

Store the value without a command line (ME is the agent you ask as, AGENT the new one). macOS (security -i reads its commands from standard input; -w with no value needs a terminal):

python3 -c 'import json,sys
r=json.load(sys.stdin)
assert r["agent"]=="ME" and r["op"]=="agent.setup"
assert r["result"]["agent"]=="AGENT"
print("add-generic-password -a AGENT -s forge-AGENT -w",
      r["secret"]["value"])' \
  < collected.json | security -i
rm collected.json

Linux: the same script, but print only r["secret"]["value"] and pipe it to secret-tool store --label="forge AGENT" service forge account AGENT, which reads the secret from standard input.

5. Push as the agent, author as a person

zit remote URL                  # result.remotes[].url from section 4
zit push --user AGENT           # the password prompt, or ZIT_PASSWORD

The password is the agent's sync token, never an account password; zit reads --password, else ZIT_PASSWORD, else a prompt. Put it in the environment from the keychain, not on the line. After one good push zit keeps a derived secret for that remote and later pushes need no password. The author of a check-in is zit commit --user LOGIN, separate from who pushed: the forge records the agent as the pusher and each check-in keeps the author it names.

A repository with no check-in takes the project of the first push it receives, history and project code included. That first contact must be a push: a pull or sync of it is refused. To start empty, zit clone it before the first commit.

6. Tokens that fail

On a 401, stop presenting that token. Retrying it digs the hole deeper for you and for every other session that shares the account or address.

7. Never do this

Never revoke push credentials you did not make. POST /api/v1/me/sync-token/revoke with neither id nor repo ends every push token of the account, including other sessions'. Minting (POST /api/v1/me/sync-token) adds a token and leaves the others working (RFC 0021), and neither call can be made with a token: they need the account itself signed in. So never ask for a person's or the forge agent's push token. Get your own through section 3, and do not reuse another session's credential.

8. CI

9. A private repository, from outside

It is answered as a missing one, so its existence is not told. The page and the API answer 404 no repository "REPO". Sync to a caller who sent no credential is challenged 401 (a missing repository is challenged the same), and with a credential that may not read it, 404. If you get 404 on a repository you were told exists, you may lack a role on it: ask your person (section 2), do not retry.