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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e53e5caf8c
commit
06f1924441
+26
-21
@@ -12,13 +12,21 @@ jobs:
|
|||||||
# defense against malicious-issue prompt injection — do not loosen it.
|
# defense against malicious-issue prompt injection — do not loosen it.
|
||||||
if: >
|
if: >
|
||||||
(github.event.comment == null && github.event.issue.user.login == 'ffaerber') ||
|
(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, '🤖') &&
|
||||||
(contains(github.event.comment.body, '@pm') ||
|
(contains(github.event.comment.body, '@pm') ||
|
||||||
contains(github.event.comment.body, '@junior') ||
|
contains(github.event.comment.body, '@junior') ||
|
||||||
contains(github.event.comment.body, '@senior') ||
|
contains(github.event.comment.body, '@senior') ||
|
||||||
contains(github.event.comment.body, '@lead') ||
|
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
|
runs-on: ci-runner
|
||||||
steps:
|
steps:
|
||||||
- name: Acknowledge with 👀
|
- name: Acknowledge with 👀
|
||||||
@@ -121,27 +129,21 @@ jobs:
|
|||||||
run: bash "$SCRIPTS/skill-node1-ssh.sh"
|
run: bash "$SCRIPTS/skill-node1-ssh.sh"
|
||||||
|
|
||||||
- name: Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos)
|
- 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
|
# Emits an opencode Skill file. The skill uses SELF_TOKEN — the running agent's OWN token
|
||||||
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's
|
# (e.g. TOKEN_PM for @pm), injected into the Run-agent step below — so each agent talks to
|
||||||
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the
|
# Gitea as itself. This step only writes the doc; permission.skill scopes who may load it.
|
||||||
# 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.
|
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
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)
|
- 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
|
# 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
|
# written ONLY for @ops (skill-gitea-admin.sh gates on NAME) and permission.skill also denies
|
||||||
# reaches other agents; permission.skill also denies it to everyone but @ops. Uses the
|
# it to every other agent. It uses SELF_TOKEN (which for @ops is TOKEN_OPS), injected into the
|
||||||
# dedicated admin token TOKEN_OPS (the `ops` user), NOT AGENT_TOKEN — this step only writes
|
# Run-agent step. This step only writes the doc.
|
||||||
# the doc; the token itself is injected into the Run-agent step only when NAME==ops.
|
|
||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
NAME: ${{ steps.prep.outputs.name }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
TOKEN_OPS: ${{ secrets.TOKEN_OPS }}
|
|
||||||
run: bash "$SCRIPTS/skill-gitea-admin.sh"
|
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)
|
||||||
@@ -165,13 +167,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
# AGENT_TOKEN powers the `gitea-api` skill (cross-repo issue/PR/Actions read+write).
|
# SELF_TOKEN = the RUNNING agent's OWN token (TOKEN_PM for @pm, TOKEN_OPS for @ops, …).
|
||||||
# It is already a required secret for the delegation step below; exposing it here too
|
# Only this agent's token is placed in its process env, so no agent can act as another.
|
||||||
# lets the agent process itself call the Gitea API on demand.
|
# Powers the gitea-api / gitea-admin skills — each agent calls Gitea as itself. Falls back
|
||||||
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
# to AGENT_TOKEN in repos that haven't been migrated to per-agent tokens (e.g. homelab).
|
||||||
# The dedicated admin token for the `gitea-admin` skill — injected into the agent's process
|
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 }}
|
||||||
# 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 }}
|
NAME: ${{ steps.prep.outputs.name }}
|
||||||
MODEL: ${{ steps.prep.outputs.model }}
|
MODEL: ${{ steps.prep.outputs.model }}
|
||||||
VISION: ${{ steps.prep.outputs.vision }}
|
VISION: ${{ steps.prep.outputs.vision }}
|
||||||
@@ -197,6 +197,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
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 }}
|
AGENT_TOKEN: ${{ secrets.AGENT_TOKEN }}
|
||||||
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
TOKEN_PM: ${{ secrets.TOKEN_PM }}
|
||||||
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
||||||
@@ -224,6 +226,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
SCRIPTS: ${{ runner.temp }}/agents-scripts
|
||||||
GT: ${{ secrets.GITEA_TOKEN }}
|
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_PM: ${{ secrets.TOKEN_PM }}
|
||||||
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
TOKEN_SENIOR: ${{ secrets.TOKEN_SENIOR }}
|
||||||
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
TOKEN_JUNIOR: ${{ secrets.TOKEN_JUNIOR }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Publish — PR (dev agents) or comment (pm/qa), always reply in the issue.
|
# Publish — PR (dev agents) or comment (pm/qa), always reply in the issue.
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step):
|
# 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
|
# 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)
|
# 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
|
set +e # publish is best-effort: a grep-no-match / curl non-zero must NOT kill the step
|
||||||
@@ -12,6 +12,11 @@ case "$NAME" in
|
|||||||
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
lead) TOK="$TOKEN_LEAD";; qa) TOK="$TOKEN_QA";; ops) TOK="$TOKEN_OPS";; *) TOK="";;
|
||||||
esac
|
esac
|
||||||
[ -z "$TOK" ] && TOK="$GT"
|
[ -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.name "$NAME"
|
||||||
git config user.email "$NAME@ffaerber.duckdns.org"
|
git config user.email "$NAME@ffaerber.duckdns.org"
|
||||||
API="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
|
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"
|
echo "MERGE_PR marker but this run is not on a PR thread — skipping merge"
|
||||||
else
|
else
|
||||||
echo "@qa autopilot: merging PR #$NUM (origin issue #${ISSNUM:-$NUM})"
|
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
|
# 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
|
# 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).
|
# agent's own token only if TOK isn't set (then the deploy would need a manual run).
|
||||||
mtok="${AGENT_TOKEN:-$TOK}"
|
mtok="$TTOK"
|
||||||
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
|
mc=$(curl -sS -o /tmp/merge_resp.txt -w '%{http_code}' -X POST \
|
||||||
-H "Authorization: token $mtok" -H "Content-Type: application/json" \
|
-H "Authorization: token $mtok" -H "Content-Type: application/json" \
|
||||||
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}')
|
"$API/pulls/$NUM/merge" -d '{"Do":"merge"}')
|
||||||
@@ -164,7 +169,7 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
else
|
else
|
||||||
n=$((prior + 1))
|
n=$((prior + 1))
|
||||||
echo "@qa autopilot: bounce $n/3 -> @$target"
|
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" \
|
"$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}')" \
|
-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
|
-w '\nbounce -> HTTP %{http_code}\n' || true
|
||||||
@@ -178,17 +183,17 @@ if [ "$MODE" != "pr" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
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
|
# 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.
|
# 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,
|
# 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.
|
# 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 \
|
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:]')
|
| head -1 | grep -oiE '(junior|senior|lead|qa)' | tr '[:upper:]' '[:lower:]')
|
||||||
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
|
if [ -n "$target" ] && [ "$target" != "$NAME" ]; then
|
||||||
echo "auto-delegating to @$target"
|
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" \
|
"$API/issues/$NUM/comments" \
|
||||||
-d "$(jq -nc --arg b "@$target please proceed with issue #$NUM per the plan above (delegated by $NAME)." '{body:$b}')" \
|
-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
|
-w '\ndelegate -> HTTP %{http_code}\n' || true
|
||||||
@@ -272,13 +277,13 @@ prpost() {
|
|||||||
|
|
||||||
if [ "$NEW" = "true" ]; then
|
if [ "$NEW" = "true" ]; then
|
||||||
prpost "$prnum" "$(printf '🤖 **@%s** — ✅ PR ready for review — @ffaerber please review & merge:\n- %s%s' "$NAME" "$url" "$activity")"
|
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
|
# @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
|
# 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.
|
# 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"
|
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" \
|
"$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}')" \
|
-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
|
-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).
|
# 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")"
|
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.
|
# 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
|
case "$NAME" in
|
||||||
junior|senior|lead)
|
junior|senior|lead)
|
||||||
echo "autopilot: dev pushed a fix — re-triggering @qa to re-verify PR #$prnum"
|
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" \
|
"$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}')" \
|
-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
|
-w '\ntrigger-qa -> HTTP %{http_code}\n' || true
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
# plain-text reply (/tmp/agent_out.md) plus the raw event stream (/tmp/events.jsonl).
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step):
|
# 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
|
# IBODY CMT
|
||||||
# FILES (the opencode -f image flags, from the imgs step output)
|
# 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).
|
# AUTOPILOT is 'true' when the issue carries the `autopilot` label (label-gated autopilot mode).
|
||||||
|
|||||||
@@ -3,20 +3,18 @@
|
|||||||
# Emits an opencode Skill file under ~/.config/opencode/skills/ documenting how to create
|
# 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.
|
# orgs/users/repos, manage labels & secrets, and mint scoped per-user tokens via the Gitea API.
|
||||||
#
|
#
|
||||||
# The credential is TOKEN_OPS (BOOTSTRAP: currently an admin PAT — temporary). This skill doc is
|
# 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
|
# written ONLY for @ops (gated on NAME) so the how-to never reaches other agents. NOTE: while
|
||||||
# TOKEN_OPS is admin, every agent's process technically holds an admin credential in its env —
|
# 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, TOKEN_OPS should be
|
# 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.
|
# narrowed and a dedicated admin token injected only for @ops.
|
||||||
#
|
#
|
||||||
# Required env (provided by the workflow step): NAME TOKEN_OPS
|
# Required env (provided by the workflow step): NAME SELF_TOKEN
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
[ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; }
|
[ "${NAME:-}" = "ops" ] || { echo "not @ops — skipping gitea-admin skill"; exit 0; }
|
||||||
if [ -z "${TOKEN_OPS:-}" ]; then
|
# The doc references $SELF_TOKEN (@ops's own admin token, present in the Run-agent step). This step
|
||||||
echo "TOKEN_OPS not set — skipping gitea-admin skill"
|
# only writes the doc for @ops; permission.skill also denies the skill to every other agent.
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin
|
mkdir -p ~/.config/opencode/skills/gitea-admin && chmod 700 ~/.config/opencode/skills/gitea-admin
|
||||||
cat > ~/.config/opencode/skills/gitea-admin/SKILL.md <<'SKILLET'
|
cat > ~/.config/opencode/skills/gitea-admin/SKILL.md <<'SKILLET'
|
||||||
---
|
---
|
||||||
@@ -29,7 +27,7 @@ tags: [gitea, admin, api, curl, bootstrap]
|
|||||||
# `gitea-admin` Skill (operator / @ops only)
|
# `gitea-admin` Skill (operator / @ops only)
|
||||||
|
|
||||||
Administer the Gitea instance via its REST API at `${GITHUB_SERVER_URL}/api/v1`, authenticated with
|
Administer the Gitea instance via its REST API at `${GITHUB_SERVER_URL}/api/v1`, authenticated with
|
||||||
`Authorization: token ${TOKEN_OPS}` (a site-admin token during bootstrap). Both env vars are
|
`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.
|
already set. Work from the issue instructions; report what you did.
|
||||||
|
|
||||||
## Golden rules
|
## Golden rules
|
||||||
@@ -42,7 +40,7 @@ already set. Work from the issue instructions; report what you did.
|
|||||||
|
|
||||||
## Create an organisation
|
## Create an organisation
|
||||||
```
|
```
|
||||||
curl -sS -X POST -H "Authorization: token $TOKEN_OPS" -H "Content-Type: application/json" \
|
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" -H "Content-Type: application/json" \
|
||||||
"$API/orgs" -d '{"username":"acme","visibility":"private"}'
|
"$API/orgs" -d '{"username":"acme","visibility":"private"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -53,14 +51,14 @@ password you just set) to mint a scoped token, and store the token straight into
|
|||||||
API="${GITHUB_SERVER_URL}/api/v1"
|
API="${GITHUB_SERVER_URL}/api/v1"
|
||||||
PW=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24) # generated, never printed
|
PW=$(head -c 24 /dev/urandom | base64 | tr -d '/+=' | head -c 24) # generated, never printed
|
||||||
# 1) create the user
|
# 1) create the user
|
||||||
curl -sS -X POST -H "Authorization: token $TOKEN_OPS" -H "Content-Type: application/json" \
|
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" \
|
"$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"}')"
|
'{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)
|
# 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" \
|
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')
|
-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
|
# 3) store the value in BOTH places (see "Secret storage" below) — never print $tok
|
||||||
curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" -H "Content-Type: application/json" \
|
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}')"
|
"$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`,
|
Token **scopes** are groups of `read:`/`write:` on: `repository`, `issue`, `organization`, `user`,
|
||||||
@@ -84,23 +82,23 @@ then overwrite the stored secret.
|
|||||||
```
|
```
|
||||||
curl -sS -u "inter:$PW" -X DELETE "$API/users/inter/tokens/<name-or-id>" # needs the password again
|
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')
|
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 $TOKEN_OPS" "$API/orgs/gitea/actions/secrets/TOKEN_INTER" -d "$(jq -nc --arg d "$tok" '{data:$d}')"
|
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
|
(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.)
|
new generated password, then re-mint.)
|
||||||
|
|
||||||
## Actions secrets & variables
|
## Actions secrets & variables
|
||||||
```
|
```
|
||||||
curl -sS -X PUT -H "Authorization: token $TOKEN_OPS" "$API/orgs/{org}/actions/secrets/{NAME}" -d '{"data":"<value>"}'
|
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 $TOKEN_OPS" "$API/repos/{owner}/{repo}/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 $TOKEN_OPS" "$API/user/actions/secrets/{NAME}" -d '{"data":"<value>"}' # user-level
|
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`.
|
## Labels (repo or org-wide). Scoped labels (name `scope/value`) are mutually exclusive if `exclusive:true`.
|
||||||
```
|
```
|
||||||
curl -sS -X POST -H "Authorization: token $TOKEN_OPS" "$API/repos/{owner}/{repo}/labels" \
|
curl -sS -X POST -H "Authorization: token $SELF_TOKEN" "$API/repos/{owner}/{repo}/labels" \
|
||||||
-d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}'
|
-d '{"name":"status/review","color":"1d76db","description":"…","exclusive":true}'
|
||||||
curl -sS -X POST -H "Authorization: token $TOKEN_OPS" "$API/orgs/{org}/labels" -d '{…}'
|
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)
|
## Bootstrap a new repo (create + wire it up for the agents)
|
||||||
@@ -109,7 +107,7 @@ curl -sS -X POST -H "Authorization: token $TOKEN_OPS" "$API/orgs/{org}/labels" -
|
|||||||
3. Commit the standard caller so it gets the agents — `PUT /repos/{owner}/{repo}/contents/.gitea/workflows/ai-agent.yml`
|
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).
|
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"}`).
|
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, TOKEN_OPS,
|
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.
|
TOKEN_* , OLLAMA_URL, OLLAMA_CLOUD_API_KEY); set any missing via the secrets calls above.
|
||||||
|
|
||||||
## Admin user management
|
## Admin user management
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Set up `gitea-api` skill (let agents read/write issues, PRs, Actions across repos).
|
# 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
|
# Emits an opencode Skill file under ~/.config/opencode/skills/. The credential is SELF_TOKEN — the
|
||||||
# ~/.config/opencode/skills/ so any dev agent discovers the capability via OpenCode's
|
# RUNNING agent's OWN token (e.g. TOKEN_PM for @pm), present in the Run-agent step's env. So each
|
||||||
# skill registry. The credential is the shared AGENT_TOKEN (a PAT whose scopes the
|
# agent talks to Gitea as itself, with its own scopes. This step only writes the doc, so it always
|
||||||
# maintainer set at creation time — issue/repository/organization/misc read+write, cross-repo).
|
# emits; permission.skill decides which agents may actually load it.
|
||||||
# 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.
|
|
||||||
#
|
#
|
||||||
# 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
|
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
|
mkdir -p ~/.config/opencode/skills/gitea-api && chmod 700 ~/.config/opencode/skills/gitea-api
|
||||||
cat > ~/.config/opencode/skills/gitea-api/SKILL.md <<'SKILLET'
|
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
|
## 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
|
`${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`.
|
`${GITHUB_SERVER_URL}/api/v1`.
|
||||||
|
|
||||||
## What you're actually allowed to do — the token's scopes are the source of truth
|
## 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
|
`repository`, `organization`, and `misc` scope groups, **cross-repo** (any repo the
|
||||||
token's account can see). That covers:
|
token's account can see). That covers:
|
||||||
- issues, PRs, comments, labels, milestones, reviewers (read + write)
|
- issues, PRs, comments, labels, milestones, reviewers (read + write)
|
||||||
@@ -64,9 +58,9 @@ in `agent.yml` exists to enforce.
|
|||||||
|
|
||||||
## Never echo the token
|
## 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,
|
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
|
## Examples
|
||||||
|
|
||||||
@@ -77,9 +71,9 @@ All examples assume `API="${GITHUB_SERVER_URL}/api/v1"`.
|
|||||||
```bash
|
```bash
|
||||||
API="${GITHUB_SERVER_URL}/api/v1"
|
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)
|
# 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
|
# 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"'
|
| 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
|
```bash
|
||||||
API="${GITHUB_SERVER_URL}/api/v1"
|
API="${GITHUB_SERVER_URL}/api/v1"
|
||||||
# Recent runs on a repo
|
# 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
|
# 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)
|
# 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
|
### List repos across an org
|
||||||
|
|
||||||
```bash
|
```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)
|
### Write: comment / label / close on another repo's issue (only when your task requires it)
|
||||||
|
|
||||||
```bash
|
```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"}'
|
"$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"]}'
|
"$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"}'
|
"$API/repos/owner/repo/issues/12" -d '{"state":"closed"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user