From 79ea9f68c9e94ead0e811f702650f20001834dd6 Mon Sep 17 00:00:00 2001 From: Felix Faerber Date: Sun, 5 Jul 2026 15:52:07 +0300 Subject: [PATCH] feat(ops): @ops uses its dedicated TOKEN_OPS, not AGENT_TOKEN The gitea-admin skill now authenticates with TOKEN_OPS (the ops user's admin token), and TOKEN_OPS is injected into the agent process ONLY when name==ops. No other agent holds an admin credential in its env. This unhooks @ops from AGENT_TOKEN, so AGENT_TOKEN can be narrowed to a non-admin token. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/agent.yml | 10 ++++-- .gitea/workflows/scripts/skill-gitea-admin.sh | 34 +++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/.gitea/workflows/agent.yml b/.gitea/workflows/agent.yml index 9029880..00b53c3 100644 --- a/.gitea/workflows/agent.yml +++ b/.gitea/workflows/agent.yml @@ -135,12 +135,13 @@ jobs: - name: Set up `gitea-admin` skill (@ops only — administer the Gitea instance) # Instance administration (orgs/users/repos/labels/secrets/scoped tokens). The SKILL.md is # written ONLY for @ops (skill-gitea-admin.sh gates on NAME), so the admin how-to never - # reaches other agents; permission.skill also denies it to everyone but @ops. Uses - # AGENT_TOKEN (an admin PAT during bootstrap) — see the script header for the token plan. + # reaches other agents; permission.skill also denies it to everyone but @ops. Uses the + # dedicated admin token TOKEN_OPS (the `ops` user), NOT AGENT_TOKEN — this step only writes + # the doc; the token itself is injected into the Run-agent step only when NAME==ops. env: SCRIPTS: ${{ runner.temp }}/agents-scripts NAME: ${{ steps.prep.outputs.name }} - AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} + TOKEN_OPS: ${{ secrets.TOKEN_OPS }} run: bash "$SCRIPTS/skill-gitea-admin.sh" - name: Inspect / fetch image attachments (download only for vision agents) @@ -168,6 +169,9 @@ jobs: # It is already a required secret for the delegation step below; exposing it here too # lets the agent process itself call the Gitea API on demand. AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} + # The dedicated admin token for the `gitea-admin` skill — injected into the agent's process + # ONLY when it is @ops, so no other agent ever holds an admin credential in its env. + TOKEN_OPS: ${{ steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || '' }} NAME: ${{ steps.prep.outputs.name }} MODEL: ${{ steps.prep.outputs.model }} VISION: ${{ steps.prep.outputs.vision }} diff --git a/.gitea/workflows/scripts/skill-gitea-admin.sh b/.gitea/workflows/scripts/skill-gitea-admin.sh index 77d22b5..b9eaccb 100644 --- a/.gitea/workflows/scripts/skill-gitea-admin.sh +++ b/.gitea/workflows/scripts/skill-gitea-admin.sh @@ -3,18 +3,18 @@ # 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 +# The credential is TOKEN_OPS (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 +# TOKEN_OPS 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, TOKEN_OPS should be # narrowed and a dedicated admin token injected only for @ops. # -# Required env (provided by the workflow step): NAME AGENT_TOKEN +# Required env (provided by the workflow step): NAME TOKEN_OPS 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" +if [ -z "${TOKEN_OPS:-}" ]; then + echo "TOKEN_OPS not set — skipping gitea-admin skill" exit 0 fi mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin @@ -29,7 +29,7 @@ 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 +`Authorization: token ${TOKEN_OPS}` (a site-admin token during bootstrap). Both env vars are already set. Work from the issue instructions; report what you did. ## Golden rules @@ -42,7 +42,7 @@ already set. Work from the issue instructions; report what you did. ## Create an organisation ``` -curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \ +curl -sS -X POST -H "Authorization: token $TOKEN_OPS" -H "Content-Type: application/json" \ "$API/orgs" -d '{"username":"acme","visibility":"private"}' ``` @@ -53,14 +53,14 @@ password you just set) to mint a scoped token, and store the token straight into 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" \ +curl -sS -X POST -H "Authorization: token $TOKEN_OPS" -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" \ +curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" -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`, @@ -84,23 +84,23 @@ 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}')" +curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" "$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 +curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":""}' +curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":""}' +curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" "$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" \ +curl -sS -X POST -H "Authorization: token $TOKEN_OPS" "$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 '{…}' +curl -sS -X POST -H "Authorization: token $TOKEN_OPS" "$API/orgs/{org}/labels" -d '{…}' ``` ## Bootstrap a new repo (create + wire it up for the agents) @@ -109,7 +109,7 @@ curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" "$API/orgs/{org}/labels" 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, +5. Ensure the repo can run agents — the org must hold the runtime secrets (ANTHROPIC_API_KEY, TOKEN_OPS, TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above. ## Admin user management -- 2.54.0