Skip to content
quarry Artifact [89d7889503]
Login

Artifact [89d7889503]

Artifact 89d7889503a7fd95bea71ac5738c19e6739de206667e086965b7d429d2a03f67:


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.

A client with no credential

If you hold no credential at all, you cannot send Quarry-Ask. Start the request yourself and let your person claim it.

  1. Make a pickup key and code as in section 4.
  2. POST /api/v1/setup/start with no authorization and the body {"name":"NAME","kind":"read","pickup":"CODE"} (kind is read or push). Send nothing else: repositories, accounts and reasons are refused, because your person chooses the repositories. The answer is {"code":"XXXX-XXXX","link":"https://FORGE/setup","expires":"..."}. Past five starts a minute from one address (an IPv6 /64 is one), three unclaimed at once from it, or when the forge holds too many, it answers 429 with Retry-After. It answers the same for a name that is already an account; your person is told at the claim.
  3. Show your person the code and the link, and nothing else of yours. They sign in, open the link, type the code, check it matches, pick the repositories (and for push the role) and press Continue. That files an ordinary agent.setup request, asked by "client of THEM", which they approve on its page. The code lapses in ten minutes.
  4. Poll POST /api/v1/pickups/collect with your key as in section 4. It answers 404 until the person has claimed the code, and for a code that lapsed or was discarded, the same as for a key that never existed; then 202 while the request waits, then 200 once. In the 200, agent is "client of THEM" and op is agent.setup: check the agent in result.agent is the name you asked for.

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

Keys: on (push and landing with branches or branches-ignore, tag, workflow_dispatch, schedule), jobs, needs, environment, timeout-minutes, env, steps, name, run, working-directory. There is no if:, no expression but ${{ secrets.NAME }}, and any other Actions key (uses:, runs-on:, strategy:, container:) is refused by name: the runner checks out for you, and tools must be on its host. Check a file before pushing it: POST /api/v1/r/REPO/ci/pipeline with {"path","content","validate":true} (needs check-in rights); no diagnostics means it is accepted.

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.