#!/usr/bin/env bash # Set up the `gitea-admin` skill — instance administration for the @ops agent ONLY. # Emits an opencode Skill file under ~/.config/opencode/skills/ documenting how to create # orgs/users/repos, manage labels & secrets, and mint scoped per-user tokens via the Gitea API. # # The credential is AGENT_TOKEN (BOOTSTRAP: currently an admin PAT — temporary). This skill doc is # written ONLY for @ops (gated on NAME) so the how-to never reaches other agents. NOTE: while # AGENT_TOKEN is admin, every agent's process technically holds an admin credential in its env — # that is the bootstrap trade-off. Once @ops is minting scoped per-user tokens, AGENT_TOKEN should be # narrowed and a dedicated admin token injected only for @ops. # # Required env (provided by the workflow step): NAME AGENT_TOKEN set -eu [ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; } if [ -z "${AGENT_TOKEN:-}" ]; then echo "AGENT_TOKEN not set — skipping gitea-admin skill" exit 0 fi mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin cat > ~/.config/opencode/skills/gitea-admin/SKILL.md <<'SKILLET' --- name: gitea-admin description: Administer this Gitea instance — create orgs, users, repos; manage labels & Actions secrets; mint scoped per-user access tokens; bootstrap a new repo with the agent caller workflow. Use for "create org X", "create repo Y", "add user Z", "give user W a token scoped to …", "set label set on …". domains: [gitea, admin, orgs, users, repos, secrets, tokens] tags: [gitea, admin, api, curl, bootstrap] --- # `gitea-admin` Skill (operator / @ops only) Administer the Gitea instance via its REST API at `${GITHUB_SERVER_URL}/api/v1`, authenticated with `Authorization: token ${AGENT_TOKEN}` (a site-admin token during bootstrap). Both env vars are already set. Work from the issue instructions; report what you did. ## Golden rules - **NEVER print, echo, or paste a token, password, or secret value** — not in comments, not in logs. Capture into a shell variable and immediately store it as a secret; report only that it was stored. - **ALWAYS confirm before anything destructive** (delete user/repo/org, remove a member). Post a clear "reply `yes` to confirm deleting X" and stop; only act after the maintainer confirms. - Prefer the **least privilege** that satisfies the request when minting tokens. - Be idempotent where you can (check if the org/repo/label already exists before creating). ## Create an organisation ``` curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ "$API/orgs" -d '{"username":"acme","visibility":"private"}' ``` ## Create a user, then mint a TAILORED token for them (least privilege) Admin creates the user with a password you generate; you then basic-auth AS that user (with the password you just set) to mint a scoped token, and store the token straight into a secret. ``` API="${GITHUB_SERVER_URL}/api/v1" PW=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24) # generated, never printed # 1) create the user curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ "$API/admin/users" -d "$(jq -nc --arg u inter --arg e inter@ffaerber.duckdns.org --arg p "$PW" \ '{username:$u,email:$e,password:$p,must_change_password:false,source_id:0,visibility:"private"}')" # 2) mint a scoped token AS that user (pick the narrowest scopes needed) tok=$(curl -sS -u "inter:$PW" -H "Content-Type: application/json" -X POST "$API/users/inter/tokens" \ -d '{"name":"inter","scopes":["read:repository","write:issue"]}' | jq -r '.sha1') # 3) store the value in BOTH places (see "Secret storage" below) — never print $tok curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ "$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')" ``` Token **scopes** are groups of `read:`/`write:` on: `repository`, `issue`, `organization`, `user`, `package`, `notification`, `misc`, and (only for a privileged token) `admin`. ## Secret storage — `gitea/secrets/.env` is the SOURCE OF TRUTH Every token/secret value MUST live in **`gitea/secrets/.env`** (private, readable only by @ffaerber and @ops) as a `KEY=value` line. That file is the master; the workflows only get a secret because `.env` is mirrored into the org Actions secrets. So whenever you mint, rotate, or re-scope a token you MUST do BOTH, in sync: 1. **`.env`**: `GET /repos/gitea/secrets/contents/.env` for its `sha`, add or replace the `KEY=value` line, then `PUT` the updated base64 content with that `sha`. 2. **Actions secret**: `PUT /orgs/gitea/actions/secrets/{KEY}` with the same value (what runs use). When you DELETE a token, remove it from BOTH. Keep `gitea/secrets/README.md` (the table describing what each KEY is) up to date. Do NOT use `tokens.md` — the values live in `.env`. NEVER paste a token value into any issue/PR/comment/log; it only ever goes into `.env` and the Actions secret. ## Change a user's token scope (the "update my token" flow) Tokens are immutable — you can't edit scopes. Re-mint: delete the old token and create a new one, then overwrite the stored secret. ``` curl -sS -u "inter:$PW" -X DELETE "$API/users/inter/tokens/" # needs the password again tok=$(curl -sS -u "inter:$PW" -X POST "$API/users/inter/tokens" -d '{"name":"inter","scopes":[…new…]}' | jq -r '.sha1') curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')" ``` (If you no longer hold the user's password, reset it first via `PATCH /admin/users/{username}` with a new generated password, then re-mint.) ## Actions secrets & variables ``` curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":""}' curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":""}' curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/user/actions/secrets/{NAME}" -d '{"data":""}' # user-level ``` ## Labels (repo or org-wide). Scoped labels (name `scope/value`) are mutually exclusive if `exclusive:true`. ``` curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" "$API/repos/{owner}/{repo}/labels" \ -d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}' curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" "$API/orgs/{org}/labels" -d '{…}' ``` ## Bootstrap a new repo (create + wire it up for the agents) 1. Create: `POST /orgs/{org}/repos` or `POST /admin/users/{user}/repos` (e.g. `{"name":"homepage","auto_init":true,"private":true}`). 2. Add the standard label set (loop the labels above). 3. Commit the standard caller so it gets the agents — `PUT /repos/{owner}/{repo}/contents/.gitea/workflows/ai-agent.yml` with base64 `content`, `message`, `branch:"main"` (copy the exact caller from the `agents` repo README). 4. Add the agent bot users as collaborators: `PUT /repos/{owner}/{repo}/collaborators/{username}` (`{"permission":"write"}`). 5. Ensure the repo can run agents — the org must hold the runtime secrets (ANTHROPIC_API_KEY, AGENT_TOKEN, TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above. ## Admin user management - Create: `POST /admin/users`. Edit: `PATCH /admin/users/{username}`. Delete: `DELETE /admin/users/{username}` (**confirm first**). - List: `GET /admin/users`. SKILLET chmod -R o=rX ~/.config/opencode/skills/gitea-admin echo "gitea-admin skill installed for @ops ($(wc -l < ~/.config/opencode/skills/gitea-admin/SKILL.md) lines)"