Compare commits

...
Author SHA1 Message Date
ffaerber c792228e2c Merge pull request 'docs: README reflects per-agent SELF_TOKEN model' (#62) from docs/token-model-selftoken into main 2026-07-05 15:40:29 +02:00
Felix FaerberandClaude Opus 4.8 1c4e4ce950 docs: README reflects per-agent SELF_TOKEN model
The secret table still described AGENT_TOKEN as primary and TOKEN_* as
optional "falls back to the bot". The per-agent-token refactor inverted that:
each agent's own TOKEN_* is primary (selected into SELF_TOKEN), AGENT_TOKEN is
now only the fallback for repos without per-agent tokens. Document TOKEN_OPS,
the SELF_TOKEN selection, and that TOKEN_QA needs write:repository to merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:40:01 +03:00
ffaerber 738848304e Merge pull request 'agents: per-agent Gitea identity — each agent uses its own token' (#59) from feat/per-agent-tokens into main 2026-07-05 15:33:54 +02:00
Felix FaerberandClaude Opus 4.8 06f1924441 agents: per-agent Gitea identity — each agent uses its own token
Drop the shared AGENT_TOKEN as the primary credential; every agent now acts
as its own Gitea user (TOKEN_PM for @pm, TOKEN_OPS for @ops, …) for API
calls, delegation/autopilot trigger comments, and PR merges.

- agent.yml: Run-agent step injects SELF_TOKEN — a ternary selecting the
  running agent's own token by name, falling back to AGENT_TOKEN for repos
  not yet migrated to per-agent tokens (e.g. homelab). Only that one token
  enters the agent process, so no agent can act as another. The gitea-api /
  gitea-admin skill-setup steps no longer carry a token (they only write docs).
- Gate: trust the agent roster (pm/junior/senior/lead/qa/ops) as comment
  authors so an agent's own delegation/autopilot trigger comment (posted with
  its PAT, no 🤖 prefix) fires the next run. @ops added to the mention set.
- publish.sh: TOK = agent identity (comments/replies); new TTOK = trigger/merge
  token (agent PAT, else AGENT_TOKEN fallback) for delegation, autopilot @qa
  triggers, and PR merges that must fire downstream workflows.
- skill-gitea-api.sh / skill-gitea-admin.sh / run-agent.sh: AGENT_TOKEN/
  TOKEN_OPS → SELF_TOKEN in the emitted skill docs and env contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 16:20:29 +03:00
ffaerber e53e5caf8c Merge pull request 'feat(ops): @ops uses dedicated TOKEN_OPS not AGENT_TOKEN' (#58) from feat/ops-dedicated-token into main 2026-07-05 14:52:09 +02:00
Felix FaerberandClaude Opus 4.8 79ea9f68c9 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) <noreply@anthropic.com>
2026-07-05 15:52:07 +03:00
Felix FaerberandClaude Opus 4.8 6c753dc0a0 chore: repo moved to gitea/agents — update self-references
Repo transferred ffaerber/agents -> gitea/agents. Point the scripts checkout (agent.yml
repository:), the standard caller (uses:), and the README at gitea/agents. The trigger gate still
keys on the maintainer 'ffaerber' (unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:43:34 +03:00
ffaerber 23c48e66c4 Merge pull request 'ops: .env is the token source of truth (not tokens.md)' (#57) from fix/ops-env-source-of-truth into main 2026-07-05 14:01:13 +02:00
Felix Faerber 6618de9c9f ops: .env is the token source of truth (not tokens.md)
Every token value must be written to gitea/secrets/.env (the master, mirrored to org Actions secrets)
AND the Actions secret, kept in sync, removed from both on delete. Drop the tokens.md instruction —
the @ops test dutifully re-created tokens.md because the skill still referenced it.
2026-07-05 15:01:10 +03:00
ffaerber 82c4b07fea Merge pull request 'feat(ops): make @ops routable + TOKEN_OPS identity' (#56) from feat/ops-routing into main 2026-07-05 13:53:11 +02:00
Felix FaerberandClaude Opus 4.8 2f1ae61b06 feat(ops): make @ops routable + post under its own TOKEN_OPS identity
PR #55 added @ops to the registry but route.sh only looped pm/junior/senior/lead/qa, so @ops could
never be triggered, and there was no token case for it. Add 'ops' to the routing loop and the
TOKEN_OPS case in route.sh + publish.sh, and pass TOKEN_OPS in the workflow env next to TOKEN_QA
(route/publish/rescue steps) so route.sh (set -u) doesn't error on an @ops run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:53:09 +03:00
ffaerber 03c2bef880 Merge pull request 'feat(ops): add @ops Gitea-administrator agent' (#55) from feat/ops-agent into main
Reviewed-on: ffaerber/agents#55
2026-07-05 13:19:29 +02:00
Felix Faerber 6832d7ad6c 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.
2026-07-05 14:09:13 +03:00
Felix FaerberandClaude Opus 4.8 74d3e1d229 feat(ops): add @ops agent — Gitea instance administrator
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) <noreply@anthropic.com>
2026-07-05 14:03:57 +03:00
ffaerber 4cbbc9b2d7 Merge pull request 'fix(autopilot): merge via AGENT_TOKEN so it triggers deploy' (#52) from fix/autopilot-merge-triggers-deploy into main
Reviewed-on: ffaerber/agents#52
2026-07-05 08:05:42 +02:00
9 changed files with 212 additions and 66 deletions
+38 -15
View File
@@ -1,6 +1,6 @@
name: agent
# Reusable AI-agent workflow, shared across repos. A caller repo triggers on issue_comment/issues
# and invokes this via: uses: ffaerber/agents/.gitea/workflows/agent.yml@main (secrets: inherit).
# and invokes this via: uses: gitea/agents/.gitea/workflows/agent.yml@main (secrets: inherit).
# The gate + steps run in the caller's event context (github.event.* / github.repository are the caller's).
on:
workflow_call:
@@ -12,13 +12,21 @@ jobs:
# defense against malicious-issue prompt injection — do not loosen it.
if: >
(github.event.comment == null && github.event.issue.user.login == 'ffaerber') ||
(github.event.comment != null && github.event.comment.user.login == 'ffaerber' &&
(github.event.comment != null &&
(github.event.comment.user.login == 'ffaerber' ||
github.event.comment.user.login == 'pm' ||
github.event.comment.user.login == 'junior' ||
github.event.comment.user.login == 'senior' ||
github.event.comment.user.login == 'lead' ||
github.event.comment.user.login == 'qa' ||
github.event.comment.user.login == 'ops') &&
!contains(github.event.comment.body, '🤖') &&
(contains(github.event.comment.body, '@pm') ||
contains(github.event.comment.body, '@junior') ||
contains(github.event.comment.body, '@senior') ||
contains(github.event.comment.body, '@lead') ||
contains(github.event.comment.body, '@qa')))
contains(github.event.comment.body, '@qa') ||
contains(github.event.comment.body, '@ops')))
runs-on: ci-runner
steps:
- name: Acknowledge with 👀
@@ -54,7 +62,7 @@ jobs:
- name: Fetch shared agent scripts (this repo)
uses: actions/checkout@v4
with:
repository: ffaerber/agents
repository: gitea/agents
ref: main
path: .agents-workflow
token: ${{ secrets.GITEA_TOKEN }}
@@ -92,6 +100,7 @@ jobs:
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
run: bash "$SCRIPTS/route.sh"
- name: Install opencode + provider config (+ Playwright MCP for browser agents)
@@ -120,17 +129,23 @@ jobs:
run: bash "$SCRIPTS/skill-node1-ssh.sh"
- name: Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos)
# Mirrors the node1-ssh pattern: emit an opencode Skill file under
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the
# maintainer set at creation time — issue/repository/organization/misc read+write, cross-repo).
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
# broken skill. The token is passed via env and never inlined into shell.
# Emits an opencode Skill file. The skill uses SELF_TOKEN — the running agent's OWN token
# (e.g. TOKEN_PM for @pm), injected into the Run-agent step below — so each agent talks to
# Gitea as itself. This step only writes the doc; permission.skill scopes who may load it.
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
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) and permission.skill also denies
# it to every other agent. It uses SELF_TOKEN (which for @ops is TOKEN_OPS), injected into the
# Run-agent step. This step only writes the doc.
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
NAME: ${{ steps.prep.outputs.name }}
run: bash "$SCRIPTS/skill-gitea-admin.sh"
- name: Inspect / fetch image attachments (download only for vision agents)
id: imgs
env:
@@ -152,10 +167,11 @@ jobs:
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write).
# 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 }}
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
# Only this agent's token is placed in its process env, so no agent can act as another.
# Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Falls back
# to AGENT_TOKEN in repos that haven't been migrated to per-agent tokens (e.g. homelab).
SELF_TOKEN: ${{ steps.prep.outputs.name == 'pm' && secrets.TOKEN_PM || steps.prep.outputs.name == 'junior' && secrets.TOKEN_JUNIOR || steps.prep.outputs.name == 'senior' && secrets.TOKEN_SENIOR || steps.prep.outputs.name == 'lead' && secrets.TOKEN_LEAD || steps.prep.outputs.name == 'qa' && secrets.TOKEN_QA || steps.prep.outputs.name == 'ops' && secrets.TOKEN_OPS || secrets.AGENT_TOKEN }}
NAME: ${{ steps.prep.outputs.name }}
MODEL: ${{ steps.prep.outputs.model }}
VISION: ${{ steps.prep.outputs.vision }}
@@ -181,12 +197,15 @@ jobs:
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
GT: ${{ secrets.GITEA_TOKEN }}
# Fallback PAT for triggering comments (delegation/autopilot) + merges in repos without
# per-agent tokens. Where per-agent tokens exist, the agent's own token is used instead.
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
TOKEN_PM: ${{ secrets.TOKEN_PM }}
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
NAME: ${{ steps.prep.outputs.name }}
MODE: ${{ steps.prep.outputs.mode }}
NUM: ${{ github.event.issue.number }}
@@ -207,11 +226,15 @@ jobs:
env:
SCRIPTS: ${{ runner.temp }}/agents-scripts
GT: ${{ secrets.GITEA_TOKEN }}
# Fallback PAT for triggering comments (delegation/autopilot) + merges in repos without
# per-agent tokens. Where per-agent tokens exist, the agent's own token is used instead.
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
TOKEN_PM: ${{ secrets.TOKEN_PM }}
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
TOKEN_LEAD: ${{ secrets.TOKEN_LEAD }}
TOKEN_QA: ${{ secrets.TOKEN_QA }}
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
NAME: ${{ steps.prep.outputs.name }}
MODE: ${{ steps.prep.outputs.mode }}
NUM: ${{ github.event.issue.number }}
+2 -2
View File
@@ -1,6 +1,6 @@
name: ai-agent
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into
# Standard caller for the shared AI-agent workflow (gitea/agents). Copy this file VERBATIM into
# any repo that should get the agents — it is identical in every repo. All logic + scripts live in
# agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
# required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
@@ -11,5 +11,5 @@ on:
types: [opened]
jobs:
agent:
uses: ffaerber/agents/.gitea/workflows/agent.yml@main
uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit
+2 -1
View File
@@ -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)."}
}
+19 -14
View File
@@ -2,16 +2,21 @@
# Publish — PR (dev agents) or comment (pm/qa), always reply in the issue.
#
# Required env (provided by the workflow step):
# GT AGENT_TOKEN TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
# GT TOKEN_PM TOKEN_SENIOR TOKEN_JUNIOR TOKEN_LEAD TOKEN_QA
# NAME MODE NUM TITLE BRANCH NEW GITHUB_SERVER_URL GITHUB_REPOSITORY
# IS_PR AUTOPILOT ISSNUM (autopilot: @qa label-gated merge/halt + auto-trigger @qa on a fresh PR)
set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step
# Post/PR as the agent's OWN Gitea user when its token is configured; else the built-in bot.
case "$NAME" in
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
[ -z "$TOK" ] && TOK="$GT"
# Trigger token: comments that must FIRE the next workflow (delegation, autopilot) and PR merges
# cannot use the built-in GITEA_TOKEN (Gitea won't start new runs from it). Use the agent's own PAT
# when it has one; otherwise fall back to AGENT_TOKEN (repos without per-agent tokens, e.g. homelab).
TTOK="$TOK"
{ [ -z "$TTOK" ] || [ "$TTOK" = "$GT" ]; } && TTOK="${AGENT_TOKEN:-}"
git config user.name "$NAME"
git config user.email "$NAME@ffaerber.duckdns.org"
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
@@ -120,11 +125,11 @@ if [ "$MODE" != "pr" ]; then
echo "MERGE_PR marker but this run is not on a PR thread — skipping merge"
else
echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})"
# Merge with AGENT_TOKEN (a PAT) — NOT the built-in Actions token — so the resulting push to
# Merge with TOK (a PAT) — NOT the built-in Actions token — so the resulting push to
# main TRIGGERS downstream workflows (e.g. deploy). A merge made with the built-in GITEA_TOKEN
# does not fire new runs (loop-prevention), which silently skips the deploy. Fall back to the
# agent's own token only if AGENT_TOKEN isn't set (then the deploy would need a manual run).
mtok="${AGENT_TOKEN:-$TOK}"
# agent's own token only if TOK isn't set (then the deploy would need a manual run).
mtok="$TTOK"
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
-H "Authorization: token $mtok" -H "Content-Type: application/json" \
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}')
@@ -164,7 +169,7 @@ if [ "$MODE" != "pr" ]; then
else
n=$((prior + 1))
echo "@qa autopilot: bounce $n/3 -> @$target"
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$NUM/comments" \
-d "$(jq -nc --arg b "@$target please address @qa's feedback above and update this PR (autopilot fix attempt $n/3)." '{body:$b}')" \
-w '\nbounce -> HTTP %{http_code}\n' || true
@@ -178,17 +183,17 @@ if [ "$MODE" != "pr" ]; then
exit 0
fi
# Auto-delegate: if the plan names a teammate, trigger them via AGENT_TOKEN (a PAT, so it
# Auto-delegate: if the plan names a teammate, trigger them via TOK (a PAT, so it
# fires a new workflow run — the built-in token cannot). Never targets @pm or self, so the
# chain always terminates at a dev. The '🤖' guard on the trigger stops status-comment loops.
if [ -n "$AGENT_TOKEN" ]; then
if [ -n "$TTOK" ]; then
# Only delegate on an explicit "DELEGATE: @<agent>" line — never on a prose mention,
# so an agent that is asking the maintainer a question does not hand off prematurely.
target=$(grep -oiE 'DELEGATE:[[:space:]]*@(junior|senior|lead|qa)' /tmp/agent_out.md 2>/dev/null \
| head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]')
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
echo "auto-delegating to @$target"
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$NUM/comments" \
-d "$(jq -nc --arg b "@$target please proceed with issue #$NUM per the plan above (delegated by $NAME)." '{body:$b}')" \
-w '\ndelegate -> HTTP %{http_code}\n' || true
@@ -272,13 +277,13 @@ prpost() {
if [ "$NEW" = "true" ]; then
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")"
# AUTOPILOT: hand the fresh PR to @qa automatically (via AGENT_TOKEN, so it fires a new run).
# AUTOPILOT: hand the fresh PR to @qa automatically (via TOK, so it fires a new run).
# @qa then verifies and — if green — merges + closes via its MERGE_PR marker. The comment lands
# on the PR thread ($prnum) so the next run resolves the origin issue's label from the branch
# name. The '🤖' guard on the trigger gate stops status-comment loops.
if [ "$AUTOPILOT" = "true" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$prnum" ]; then
if [ "$AUTOPILOT" = "true" ] && [ -n "$TTOK" ] && [ -n "$prnum" ]; then
echo "autopilot: auto-triggering @qa to review PR #$prnum"
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$prnum/comments" \
-d "$(jq -nc --arg b "@qa please verify this PR (autopilot: issue #$NUM is labeled autopilot). Merge it if correct, or bounce it back to the dev with exactly what needs fixing." '{body:$b}')" \
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
@@ -288,11 +293,11 @@ else
# repeat the full write-up in the comment (the reasoning trail below shows what this run did).
prpost "$prnum" "$(printf '🤖 **@%s** — pushed an update to the PR:\n- %s%s' "$NAME" "$url" "$activity")"
# AUTOPILOT: after a dev pushes a fix (e.g. following a @qa bounce), hand back to @qa to re-verify.
if [ "$AUTOPILOT" = "true" ] && [ -n "$AGENT_TOKEN" ] && [ -n "$prnum" ]; then
if [ "$AUTOPILOT" = "true" ] && [ -n "$TTOK" ] && [ -n "$prnum" ]; then
case "$NAME" in
junior|senior|lead)
echo "autopilot: dev pushed a fix — re-triggering @qa to re-verify PR #$prnum"
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $TTOK" -H "Content-Type: application/json" \
"$API/issues/$prnum/comments" \
-d "$(jq -nc --arg b "@qa please re-verify this PR (autopilot). Merge it if now correct, or bounce it back with exactly what still needs fixing." '{body:$b}')" \
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
+2 -2
View File
@@ -27,7 +27,7 @@ cp "$AGENTS_JSON" /tmp/agents.json
# here — see agent.yml: this reusable workflow sees it as 'workflow_call'.)
if [ -n "$CID" ]; then scan="$BODY"; else scan="$IBODY"; fi
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
done
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.
case "$name" in
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
[ -z "$TOK" ] && TOK="$GT"
git config user.name "$name"
+1 -1
View File
@@ -3,7 +3,7 @@
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
#
# Required env (provided by the workflow step):
# ANTHROPIC_API_KEY AGENT_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
# ANTHROPIC_API_KEY SELF_TOKEN NAME MODEL VISION MODE HAS_IMAGES BRANCH AUTOPILOT NUM TITLE
# IBODY CMT
# FILES (the opencode -f image flags, from the imgs step output)
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
@@ -0,0 +1,118 @@
#!/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 SELF_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
# SELF_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, SELF_TOKEN should be
# narrowed and a dedicated admin token injected only for @ops.
#
# Required env (provided by the workflow step): NAME SELF_TOKEN
set -eu
[ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; }
# The doc references $SELF_TOKEN (@ops's own admin token, present in the Run-agent step). This step
# only writes the doc for @ops; permission.skill also denies the skill to every other agent.
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 ${SELF_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 $SELF_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 $SELF_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 $SELF_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 $SELF_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 $SELF_TOKEN" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
curl -sS -X PUT -H "Authorization: token $SELF_TOKEN" "$API/repos/{owner}/{repo}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
curl -sS -X PUT -H "Authorization: token $SELF_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 $SELF_TOKEN" "$API/repos/{owner}/{repo}/labels" \
-d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}'
curl -sS -X POST -H "Authorization: token $SELF_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, SELF_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)"
+19 -25
View File
@@ -1,19 +1,13 @@
#!/usr/bin/env bash
# Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos).
# Mirrors the node1-ssh pattern: emit an opencode Skill file under
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the
# maintainer set at creation time — issue/repository/organization/misc read+write, cross-repo).
# Only emitted when AGENT_TOKEN is actually present, so repos without it don't get a
# broken skill. The token is passed via env and never inlined into shell.
# Emits an opencode Skill file under ~/.config/opencode/skills/. The credential is SELF_TOKEN — the
# RUNNING agent's OWN token (e.g. TOKEN_PM for @pm), present in the Run-agent step's env. So each
# agent talks to Gitea as itself, with its own scopes. This step only writes the doc, so it always
# emits; permission.skill decides which agents may actually load it.
#
# Required env (provided by the workflow step): AGENT_TOKEN
# Required env (provided by the workflow step): (none — the token is in the Run-agent step)
set -eu
if [ -z "$AGENT_TOKEN" ]; then
echo "AGENT_TOKEN not set — skipping gitea-api skill"
exit 0
fi
mkdir -p ~/.config/opencode/skills/gitea-api && chmod 700 ~/.config/opencode/skills/gitea-api
cat > ~/.config/opencode/skills/gitea-api/SKILL.md <<'SKILLET'
---
@@ -33,14 +27,14 @@ Use this skill to talk to the **Gitea REST API** (`${GITHUB_SERVER_URL}/api/v1`)
## How it works
Calls go via `curl` with the header `Authorization: token ${AGENT_TOKEN}`. Both
Calls go via `curl` with the header `Authorization: token ${SELF_TOKEN}`. Both
`${GITHUB_SERVER_URL}` (the instance root, e.g. `https://git.example.com`) and
`${AGENT_TOKEN}` are present in your environment. The API root is
`${SELF_TOKEN}` are present in your environment. The API root is
`${GITHUB_SERVER_URL}/api/v1`.
## What you're actually allowed to do — the token's scopes are the source of truth
The shared `AGENT_TOKEN` was granted **read and write** on the `issue`,
The shared `SELF_TOKEN` was granted **read and write** on the `issue`,
`repository`, `organization`, and `misc` scope groups, **cross-repo** (any repo the
token's account can see). That covers:
- issues, PRs, comments, labels, milestones, reviewers (read + write)
@@ -64,9 +58,9 @@ in `agent.yml` exists to enforce.
## Never echo the token
**Never print, log, or exfiltrate `AGENT_TOKEN`.** Do not pass it to `echo`, do not
**Never print, log, or exfiltrate `SELF_TOKEN`.** Do not pass it to `echo`, do not
include it in a comment, do not write it to a file. If you need to show a curl command,
redact the header as `Authorization: token $AGENT_TOKEN`.
redact the header as `Authorization: token $SELF_TOKEN`.
## Examples
@@ -77,9 +71,9 @@ All examples assume `API="${GITHUB_SERVER_URL}/api/v1"`.
```bash
API="${GITHUB_SERVER_URL}/api/v1"
# Get issue/PR #12 on repo owner/repo (a PR if the number is a pull; issues/PRs share one number space)
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/issues/12" | jq '{title,state,body,user:.user.login}'
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/issues/12" | jq '{title,state,body,user:.user.login}'
# Its comment thread
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/issues/12/comments?limit=100" \
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/issues/12/comments?limit=100" \
| jq -r '.[] | "### @\(.user.login):\n\(.body)\n"'
```
@@ -91,27 +85,27 @@ find the owner/repo for a `#N` in *this* repo, just use `${GITHUB_REPOSITORY}`.
```bash
API="${GITHUB_SERVER_URL}/api/v1"
# Recent runs on a repo
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/runs?limit=10" | jq '.[] | {id,status,conclusion,head_branch,event}'
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/runs?limit=10" | jq '.[] | {id,status,conclusion,head_branch,event}'
# Jobs for a run
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/runs/$RUN_ID/jobs" | jq '.[] | {name,status,conclusion}'
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/runs/$RUN_ID/jobs" | jq '.[] | {name,status,conclusion}'
# Logs for a job (returns a text/plain stream)
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/repos/owner/repo/actions/jobs/$JOB_ID/logs"
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/repos/owner/repo/actions/jobs/$JOB_ID/logs"
```
### List repos across an org
```bash
curl -sS -H "Authorization: token $AGENT_TOKEN" "$API/orgs/$ORG/repos?limit=50" | jq '.[] | .full_name'
curl -sS -H "Authorization: token $SELF_TOKEN" "$API/orgs/$ORG/repos?limit=50" | jq '.[] | .full_name'
```
### Write: comment / label / close on another repo's issue (only when your task requires it)
```bash
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12/comments" -d '{"body":"related to #N"}'
curl -sS -X POST -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12/labels" -d '{"labels":["related"]}'
curl -sS -X PATCH -H "Authorization: token $AGENT_TOKEN" -H "Content-Type: application/json" \
curl -sS -X PATCH -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
"$API/repos/owner/repo/issues/12" -d '{"state":"closed"}'
```
+11 -6
View File
@@ -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.
@@ -36,7 +37,7 @@ it is the source of truth, and `agents` itself uses the same file:
```yaml
name: ai-agent
run-name: "ai-agent · #${{ github.event.issue.number }}" # quotes required: bare # starts a YAML comment
# Standard caller for the shared AI-agent workflow (ffaerber/agents). Copy this file VERBATIM into
# Standard caller for the shared AI-agent workflow (gitea/agents). Copy this file VERBATIM into
# any repo that should get the agents — it is identical in every repo. All logic + scripts live in
# agents/.gitea/workflows/; scripts are fetched from @main at run time. The `jobs.agent` wrapper is
# required: a reusable (workflow_call) workflow can only be invoked from a caller job, not top-level.
@@ -48,7 +49,7 @@ on:
types: [opened]
jobs:
agent:
uses: ffaerber/agents/.gitea/workflows/agent.yml@main
uses: gitea/agents/.gitea/workflows/agent.yml@main
secrets: inherit
```
@@ -75,11 +76,15 @@ points `$SCRIPTS` at it. Keep the workflow and its scripts moving together on `m
|--------|-----|
| `ANTHROPIC_API_KEY` | `@lead` (and `@pm`/`@senior`/`@qa` if on Claude) |
| `OLLAMA_URL`, `OLLAMA_CLOUD_API_KEY` | local ornith / Ollama Cloud (gemma4, kimi-k2.7-code, glm-5.2, minimax-m3) |
| `AGENT_TOKEN` | PAT (issue/repository/organization/misc read+write, cross-repo) — posts the delegation comment that fires the next agent **and** powers the `gitea-api` skill (read/write issues, PRs, comments, labels, and Actions runs/logs across any repo). Do not re-narrow its scopes without also removing the `gitea-api` skill. |
| `TOKEN_PM`,`TOKEN_SENIOR`,`TOKEN_JUNIOR`,`TOKEN_LEAD`,`TOKEN_QA` | optional — post/commit as each agent's own Gitea user (falls back to the bot) |
| `TOKEN_PM`,`TOKEN_SENIOR`,`TOKEN_JUNIOR`,`TOKEN_LEAD`,`TOKEN_QA` | **primary** — each agent's own Gitea-user PAT. The running agent gets *only its own* token (as `SELF_TOKEN`) so it posts, commits, comments, and (for `@qa` autopilot) merges as itself, and its `gitea-api` skill acts with its own scopes. `TOKEN_QA` needs `write:repository` to merge. |
| `TOKEN_OPS` | `@ops` only — the admin PAT behind the `gitea-admin` skill (create orgs/users/repos, manage labels & secrets, mint scoped tokens). Injected into the agent process only when the agent is `@ops`. |
| `AGENT_TOKEN` | **fallback** — an admin PAT used only where per-agent `TOKEN_*` aren't configured (e.g. an un-migrated consuming repo). Covers the two things the built-in `GITEA_TOKEN` can't do: post the delegation/autopilot comment that *fires the next run*, and merge a PR so the push *triggers downstream deploys*. Where per-agent tokens exist, each agent uses its own instead. |
`GITEA_TOKEN` is auto-provided. Tip: set these once at the **org** level so every repo inherits
them via `secrets: inherit`.
Each agent authenticates as **itself**: the Run-agent step selects that agent's `TOKEN_*` into
`SELF_TOKEN` (never another agent's), falling back to `AGENT_TOKEN` only when its own token is unset.
`GITEA_TOKEN` is auto-provided (used for reads, and as the reply identity only when an agent has no
`TOKEN_*` of its own). Tip: set these once at the **org** level so every repo inherits them via
`secrets: inherit`.
## Also add to each consuming repo