Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6618de9c9f | ||
|
|
82c4b07fea | ||
|
|
2f1ae61b06 | ||
|
|
03c2bef880 | ||
|
|
6832d7ad6c | ||
|
|
74d3e1d229 | ||
|
|
4cbbc9b2d7 |
@@ -92,6 +92,7 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
||||||
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
||||||
|
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
|
||||||
run: bash "$SCRIPTS/route.sh"
|
run: bash "$SCRIPTS/route.sh"
|
||||||
|
|
||||||
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
|
||||||
@@ -131,6 +132,17 @@ jobs:
|
|||||||
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
run: bash "$SCRIPTS/skill-gitea-api.sh"
|
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)
|
- name: Inspect / fetch image attachments (download only for vision agents)
|
||||||
id: imgs
|
id: imgs
|
||||||
env:
|
env:
|
||||||
@@ -187,6 +199,7 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
||||||
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
||||||
|
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
@@ -212,6 +225,7 @@ jobs:
|
|||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
|
||||||
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
TOKEN_QA: ${{ secrets.TOKEN_QA }}
|
||||||
|
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
MODE: ${{ steps.prep.outputs.mode }}
|
MODE: ${{ steps.prep.outputs.mode }}
|
||||||
NUM: ${{ github.event.issue.number }}
|
NUM: ${{ github.event.issue.number }}
|
||||||
|
|||||||
@@ -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."},
|
"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)."},
|
"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."},
|
"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)."}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill
|
|||||||
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
|
||||||
case "$NAME" in
|
case "$NAME" in
|
||||||
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
||||||
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
git config user.name "$NAME"
|
git config user.name "$NAME"
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ cp "$AGENTS_JSON" /tmp/agents.json
|
|||||||
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
|
||||||
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
|
||||||
name=""
|
name=""
|
||||||
for a in pm junior senior lead qa; do
|
for a in pm junior senior lead qa ops; do
|
||||||
case "$scan" in *"@$a"*) name=$a; break;; esac
|
case "$scan" in *"@$a"*) name=$a; break;; esac
|
||||||
done
|
done
|
||||||
if [ -z "$name" ]; then
|
if [ -z "$name" ]; then
|
||||||
@@ -44,7 +44,7 @@ echo "Routing to @$name (model=$model vision=$vision mode=$mode skills=$skills)"
|
|||||||
# Act as the agent's own Gitea user when its token is set; else the built-in bot.
|
# Act as the agent's own Gitea user when its token is set; else the built-in bot.
|
||||||
case "$name" in
|
case "$name" in
|
||||||
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
pm) TOK="$TOKEN_PM";; senior) TOK="$TOKEN_SENIOR";; junior) TOK="$TOKEN_JUNIOR";;
|
||||||
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -z "$TOK" ] && TOK="$GT"
|
||||||
git config user.name "$name"
|
git config user.name "$name"
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#!/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/<name-or-id>" # 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":"<value>"}'
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
|
||||||
|
curl -sS -X PUT -H "Authorization: token $AGENT_TOKEN" "$API/user/actions/secrets/{NAME}" -d '{"data":"<value>"}' # 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)"
|
||||||
@@ -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). |
|
| `@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. |
|
| `@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. |
|
| `@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
|
`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.
|
or an agent's skills there, update this table too.
|
||||||
|
|||||||
Reference in New Issue
Block a user