From 74d3e1d229b75c057e16f7670e79877fa42d655d Mon Sep 17 00:00:00 2001 From: Felix Faerber Date: Sun, 5 Jul 2026 14:03:57 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat(ops):=20add=20@ops=20agent=20=E2=80=94?= =?UTF-8?q?=20Gitea=20instance=20administrator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A comment-mode operator agent (opus) for administering Gitea itself from issues in gitea/ops: create orgs/users/repos, manage labels & Actions secrets, and mint least-privilege per-user tokens. - agents.json: new @ops role (comment-mode, skill gitea-admin, confirms before destructive ops). - skill-gitea-admin.sh: SKILL.md documenting org/user/repo/label/secret ops + the create-user → mint-scoped-token → store-as-secret flow (never printing tokens). Gated on NAME=ops so the admin how-to is written ONLY for @ops; permission.skill also denies it to other agents. - agent.yml: wire the skill step (uses AGENT_TOKEN — an admin PAT during bootstrap). Bootstrap note: AGENT_TOKEN is admin for now, so every agent's process technically holds an admin credential (skill-scoping hides the doc, not the env var). Once @ops is minting scoped per-user tokens, narrow AGENT_TOKEN and inject a dedicated admin token only for @ops. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/agent.yml | 11 ++ .gitea/workflows/scripts/agents.json | 3 +- .gitea/workflows/scripts/skill-gitea-admin.sh | 108 ++++++++++++++++++ README.md | 1 + 4 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 .gitea/workflows/scripts/skill-gitea-admin.sh diff --git a/.gitea/workflows/agent.yml b/.gitea/workflows/agent.yml index a233df2..e7845af 100644 --- a/.gitea/workflows/agent.yml +++ b/.gitea/workflows/agent.yml @@ -131,6 +131,17 @@ jobs: AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} run: bash "$SCRIPTS/skill-gitea-api.sh" + - 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. + env: + SCRIPTS: ${{ runner.temp }}/agents-scripts + NAME: ${{ steps.prep.outputs.name }} + AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }} + run: bash "$SCRIPTS/skill-gitea-admin.sh" + - name: Inspect / fetch image attachments (download only for vision agents) id: imgs env: diff --git a/.gitea/workflows/scripts/agents.json b/.gitea/workflows/scripts/agents.json index 549c15a..c309b2f 100644 --- a/.gitea/workflows/scripts/agents.json +++ b/.gitea/workflows/scripts/agents.json @@ -3,5 +3,6 @@ "junior": {"model":"ollama-cloud/kimi-k2.7-code:cloud","vision":false,"mode":"pr", "skills":[],"desc":"Junior dev — small, low-risk changes (mostly YAML/compose/config). Text-only, cannot read images. Defers complex or image tasks to @senior or @lead."}, "senior": {"model":"ollama-cloud/glm-5.2:cloud","vision":false,"mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only)."}, "lead": {"model":"anthropic/claude-opus-4-8","vision":true, "mode":"pr", "skills":["gitea-api","node1-ssh"],"desc":"Tech lead — the hardest problems, architecture, and final calls."}, - "qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","skills":["gitea-api"],"desc":"QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs."} + "qa": {"model":"ollama-cloud/minimax-m3:cloud","vision":true, "mode":"comment","skills":["gitea-api"],"desc":"QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs."}, + "ops": {"model":"anthropic/claude-opus-4-8","vision":false,"mode":"comment","skills":["gitea-admin"],"desc":"Gitea operator — administers the Gitea instance itself: create orgs/users/repos, manage labels and secrets, mint scoped per-user tokens, bootstrap new repos with the agent caller. Comments only; never edits code. ALWAYS confirms before any destructive action (delete user/repo/org)."} } \ No newline at end of file diff --git a/.gitea/workflows/scripts/skill-gitea-admin.sh b/.gitea/workflows/scripts/skill-gitea-admin.sh new file mode 100644 index 0000000..5f28ac3 --- /dev/null +++ b/.gitea/workflows/scripts/skill-gitea-admin.sh @@ -0,0 +1,108 @@ +#!/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 it as a secret (org / repo / user level) — 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`. + +## 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)" diff --git a/README.md b/README.md index ff1ed4e..3c5312c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Shared **AI dev-team** workflow for Gitea Actions, reusable across repos. It giv | `@senior` | `ollama-cloud/glm-5.2:cloud` | no | pr | `gitea-api`, `node1-ssh` | Senior dev — complex, multi-file implementation (GLM-5.2 via Ollama Cloud, text-only). | | `@lead` | `anthropic/claude-opus-4-8` | yes | pr | `gitea-api`, `node1-ssh` | Tech lead — the hardest problems, architecture, and final calls. | | `@qa` | `ollama-cloud/minimax-m3:cloud` | yes | comment | `gitea-api` | QA — verifies things work. Drives a headless browser (Playwright) to open a URL/web app, click through it, screenshot, and report bugs or confirm behavior. Comments findings; opens no PRs. | +| `@ops` | `anthropic/claude-opus-4-8` | no | comment | `gitea-admin` | Gitea operator — administers the instance itself (create orgs/users/repos, labels, secrets, scoped per-user tokens, bootstrap repos). Comments only; never edits code. Confirms before destructive actions. | `agent.yml`'s agent registry is the source of truth for this mapping — if you change a model or an agent's skills there, update this table too. -- 2.54.0 From 6832d7ad6cc78d5d58f8fde81764d463cdc7446e Mon Sep 17 00:00:00 2001 From: Felix Faerber Date: Sun, 5 Jul 2026 14:09:13 +0300 Subject: [PATCH 2/2] ops: record minted tokens in the private gitea/secrets inventory @ops now treats gitea/secrets/tokens.md as the source-of-truth inventory (readable by @ffaerber and @ops only) and records every token it mints/rotates there, alongside storing the live value in the matching Actions secret. --- .gitea/workflows/scripts/skill-gitea-admin.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.gitea/workflows/scripts/skill-gitea-admin.sh b/.gitea/workflows/scripts/skill-gitea-admin.sh index 5f28ac3..ab40e24 100644 --- a/.gitea/workflows/scripts/skill-gitea-admin.sh +++ b/.gitea/workflows/scripts/skill-gitea-admin.sh @@ -66,6 +66,14 @@ curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: applica Token **scopes** are groups of `read:`/`write:` on: `repository`, `issue`, `organization`, `user`, `package`, `notification`, `misc`, and (only for a privileged token) `admin`. +## Token inventory — record everything in `gitea/secrets` +The private repo **`gitea/secrets`** (readable only by @ffaerber and @ops) is the source of truth for +tokens. Whenever you mint, rotate, or re-scope a token, append/update a row in its `tokens.md` via the +contents API (`GET` the file for its `sha`, then `PUT` the updated base64 content with that `sha`): +`| | | | | |`. +Storing the live value in the matching Actions secret is what workflows use; the `gitea/secrets` row +is the human-readable inventory. Never paste a token value into any issue/PR/comment/log. + ## 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. -- 2.54.0