From 89628e50e28a3ab36c019129ae226cbf52ce6651 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 26 Mar 2026 16:16:13 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Per-agent=20Forgejo=20accounts=20?= =?UTF-8?q?=E2=80=94=20identity=20and=20permissions=20via=20authorship=20(?= =?UTF-8?q?#747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each agent now gets its own Forgejo account (dev-bot, review-bot, planner-bot, gardener-bot, vault-bot, supervisor-bot, predictor-bot, action-bot) with a dedicated API token. This enables: - Audit trail: every forge action attributable to a specific agent - Permission boundaries: agents act under their own identity - Vault authorization model: vault-bot comments = proof of approval Changes: - bin/disinto: setup_forge() creates all 8 bot accounts during init, stores per-agent tokens (FORGE_*_TOKEN) in .env, adds all bots as repo collaborators - lib/env.sh: exports per-agent token vars with fallback to FORGE_TOKEN for backwards compat; sets FORGE_BOT_USERNAMES default to all 8 bots - Agent scripts: each agent overrides FORGE_TOKEN with its per-agent token after sourcing env.sh (gardener, planner, supervisor, predictor, vault, action) - .env.example: documents all per-agent token fields Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.example | 13 +++++-- action/action-agent.sh | 2 ++ action/action-poll.sh | 2 ++ bin/disinto | 70 +++++++++++++++++------------------- gardener/gardener-run.sh | 2 ++ lib/env.sh | 11 +++++- planner/planner-run.sh | 2 ++ predictor/predictor-run.sh | 2 ++ supervisor/supervisor-run.sh | 2 ++ vault/vault-agent.sh | 2 ++ vault/vault-fire.sh | 2 ++ vault/vault-poll.sh | 2 ++ vault/vault-reject.sh | 2 ++ 13 files changed, 74 insertions(+), 40 deletions(-) diff --git a/.env.example b/.env.example index e559567..33bc0f6 100644 --- a/.env.example +++ b/.env.example @@ -17,14 +17,23 @@ FORGE_URL=http://localhost:3000 # [CONFIG] local Forgejo instance # ── Auth tokens ─────────────────────────────────────────────────────────── -FORGE_TOKEN= # [SECRET] dev-bot API token +# Each agent has its own Forgejo account and API token (#747). +# Per-agent tokens fall back to FORGE_TOKEN if not set. +FORGE_TOKEN= # [SECRET] dev-bot API token (default for all agents) FORGE_REVIEW_TOKEN= # [SECRET] review-bot API token -FORGE_BOT_USERNAMES= # [CONFIG] comma-separated bot usernames +FORGE_PLANNER_TOKEN= # [SECRET] planner-bot API token +FORGE_GARDENER_TOKEN= # [SECRET] gardener-bot API token +FORGE_VAULT_TOKEN= # [SECRET] vault-bot API token +FORGE_SUPERVISOR_TOKEN= # [SECRET] supervisor-bot API token +FORGE_PREDICTOR_TOKEN= # [SECRET] predictor-bot API token +FORGE_ACTION_TOKEN= # [SECRET] action-bot API token +FORGE_BOT_USERNAMES=dev-bot,review-bot,planner-bot,gardener-bot,vault-bot,supervisor-bot,predictor-bot,action-bot # ── Backwards compatibility ─────────────────────────────────────────────── # If CODEBERG_TOKEN is set but FORGE_TOKEN is not, env.sh falls back to # CODEBERG_TOKEN automatically (same for REVIEW_BOT_TOKEN, CODEBERG_REPO, # CODEBERG_BOT_USERNAMES). No action needed for existing deployments. +# Per-agent tokens default to FORGE_TOKEN when unset (single-token setups). # ── Woodpecker CI ───────────────────────────────────────────────────────── WOODPECKER_TOKEN= # [SECRET] Woodpecker API token diff --git a/action/action-agent.sh b/action/action-agent.sh index 1afbfe1..e6e55ff 100755 --- a/action/action-agent.sh +++ b/action/action-agent.sh @@ -27,6 +27,8 @@ ISSUE="${1:?Usage: action-agent.sh [project.toml]}" export PROJECT_TOML="${2:-${PROJECT_TOML:-}}" source "$(dirname "$0")/../lib/env.sh" +# Use action-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}" source "$(dirname "$0")/../lib/ci-helpers.sh" source "$(dirname "$0")/../lib/agent-session.sh" source "$(dirname "$0")/../lib/formula-session.sh" diff --git a/action/action-poll.sh b/action/action-poll.sh index 100d854..ba0c4ec 100755 --- a/action/action-poll.sh +++ b/action/action-poll.sh @@ -13,6 +13,8 @@ set -euo pipefail export PROJECT_TOML="${1:-}" source "$(dirname "$0")/../lib/env.sh" +# Use action-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}" # shellcheck source=../lib/guard.sh source "$(dirname "$0")/../lib/guard.sh" check_active action diff --git a/bin/disinto b/bin/disinto index 4ec6293..aa4b50c 100755 --- a/bin/disinto +++ b/bin/disinto @@ -439,10 +439,25 @@ setup_forge() { fi # Create bot users and tokens - local dev_token="" review_token="" - for bot_user in dev-bot review-bot; do - local bot_pass + # Each agent gets its own Forgejo account for identity and audit trail (#747). + # Map: bot-username -> env-var-name for the token + local -A bot_token_vars=( + [dev-bot]="FORGE_TOKEN" + [review-bot]="FORGE_REVIEW_TOKEN" + [planner-bot]="FORGE_PLANNER_TOKEN" + [gardener-bot]="FORGE_GARDENER_TOKEN" + [vault-bot]="FORGE_VAULT_TOKEN" + [supervisor-bot]="FORGE_SUPERVISOR_TOKEN" + [predictor-bot]="FORGE_PREDICTOR_TOKEN" + [action-bot]="FORGE_ACTION_TOKEN" + ) + + local env_file="${FACTORY_ROOT}/.env" + local bot_user bot_pass token token_var + + for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot action-bot; do bot_pass="bot-$(head -c 16 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 20)" + token_var="${bot_token_vars[$bot_user]}" if ! curl -sf --max-time 5 \ -H "Authorization: token ${admin_token}" \ @@ -476,7 +491,6 @@ setup_forge() { # Generate token via API (basic auth as the bot user — Forgejo requires # basic auth on POST /users/{username}/tokens, token auth is rejected) - local token token=$(curl -sf -X POST \ -u "${bot_user}:${bot_pass}" \ -H "Content-Type: application/json" \ @@ -499,41 +513,23 @@ setup_forge() { exit 1 fi - if [ "$bot_user" = "dev-bot" ]; then - dev_token="$token" + # Store token in .env under the per-agent variable name + if grep -q "^${token_var}=" "$env_file" 2>/dev/null; then + sed -i "s|^${token_var}=.*|${token_var}=${token}|" "$env_file" else - review_token="$token" + printf '%s=%s\n' "$token_var" "$token" >> "$env_file" + fi + export "${token_var}=${token}" + echo " ${bot_user} token saved (${token_var})" + + # Backwards-compat aliases for dev-bot and review-bot + if [ "$bot_user" = "dev-bot" ]; then + export CODEBERG_TOKEN="$token" + elif [ "$bot_user" = "review-bot" ]; then + export REVIEW_BOT_TOKEN="$token" fi done - # Store tokens in .env - local env_file="${FACTORY_ROOT}/.env" - if [ -n "$dev_token" ]; then - if grep -q '^FORGE_TOKEN=' "$env_file" 2>/dev/null; then - sed -i "s|^FORGE_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file" - elif grep -q '^CODEBERG_TOKEN=' "$env_file" 2>/dev/null; then - sed -i "s|^CODEBERG_TOKEN=.*|FORGE_TOKEN=${dev_token}|" "$env_file" - else - printf '\nFORGE_TOKEN=%s\n' "$dev_token" >> "$env_file" - fi - export FORGE_TOKEN="$dev_token" - export CODEBERG_TOKEN="$dev_token" - echo " dev-bot token saved" - fi - - if [ -n "$review_token" ]; then - if grep -q '^FORGE_REVIEW_TOKEN=' "$env_file" 2>/dev/null; then - sed -i "s|^FORGE_REVIEW_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file" - elif grep -q '^REVIEW_BOT_TOKEN=' "$env_file" 2>/dev/null; then - sed -i "s|^REVIEW_BOT_TOKEN=.*|FORGE_REVIEW_TOKEN=${review_token}|" "$env_file" - else - printf 'FORGE_REVIEW_TOKEN=%s\n' "$review_token" >> "$env_file" - fi - export FORGE_REVIEW_TOKEN="$review_token" - export REVIEW_BOT_TOKEN="$review_token" - echo " review-bot token saved" - fi - # Store FORGE_URL in .env if not already present if ! grep -q '^FORGE_URL=' "$env_file" 2>/dev/null; then printf 'FORGE_URL=%s\n' "$forge_url" >> "$env_file" @@ -569,8 +565,8 @@ setup_forge() { -d "{\"name\":\"${repo_name}\",\"auto_init\":false,\"default_branch\":\"main\"}" >/dev/null 2>&1 || true fi - # Add bot users as collaborators - for bot_user in dev-bot review-bot; do + # Add all bot users as collaborators + for bot_user in dev-bot review-bot planner-bot gardener-bot vault-bot supervisor-bot predictor-bot action-bot; do curl -sf -X PUT \ -H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" \ -H "Content-Type: application/json" \ diff --git a/gardener/gardener-run.sh b/gardener/gardener-run.sh index 58b9b1f..9b730b4 100755 --- a/gardener/gardener-run.sh +++ b/gardener/gardener-run.sh @@ -20,6 +20,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}" # shellcheck source=../lib/env.sh source "$FACTORY_ROOT/lib/env.sh" +# Use gardener-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_GARDENER_TOKEN:-${FORGE_TOKEN}}" # shellcheck source=../lib/agent-session.sh source "$FACTORY_ROOT/lib/agent-session.sh" # shellcheck source=../lib/formula-session.sh diff --git a/lib/env.sh b/lib/env.sh index 0d0aeb6..947c1af 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -53,8 +53,17 @@ export CODEBERG_TOKEN="${FORGE_TOKEN}" # backwards compat export FORGE_REVIEW_TOKEN="${FORGE_REVIEW_TOKEN:-${REVIEW_BOT_TOKEN:-}}" export REVIEW_BOT_TOKEN="${FORGE_REVIEW_TOKEN}" # backwards compat +# Per-agent tokens (#747): each agent gets its own Forgejo identity. +# Falls back to FORGE_TOKEN for backwards compat with single-token setups. +export FORGE_PLANNER_TOKEN="${FORGE_PLANNER_TOKEN:-${FORGE_TOKEN}}" +export FORGE_GARDENER_TOKEN="${FORGE_GARDENER_TOKEN:-${FORGE_TOKEN}}" +export FORGE_VAULT_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" +export FORGE_SUPERVISOR_TOKEN="${FORGE_SUPERVISOR_TOKEN:-${FORGE_TOKEN}}" +export FORGE_PREDICTOR_TOKEN="${FORGE_PREDICTOR_TOKEN:-${FORGE_TOKEN}}" +export FORGE_ACTION_TOKEN="${FORGE_ACTION_TOKEN:-${FORGE_TOKEN}}" + # Bot usernames filter: FORGE_BOT_USERNAMES > legacy CODEBERG_BOT_USERNAMES -export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-}}" +export FORGE_BOT_USERNAMES="${FORGE_BOT_USERNAMES:-${CODEBERG_BOT_USERNAMES:-dev-bot,review-bot,planner-bot,gardener-bot,vault-bot,supervisor-bot,predictor-bot,action-bot}}" export CODEBERG_BOT_USERNAMES="${FORGE_BOT_USERNAMES}" # backwards compat # Project config (FORGE_* preferred, CODEBERG_* fallback) diff --git a/planner/planner-run.sh b/planner/planner-run.sh index c529aa9..ab7d987 100755 --- a/planner/planner-run.sh +++ b/planner/planner-run.sh @@ -18,6 +18,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}" # shellcheck source=../lib/env.sh source "$FACTORY_ROOT/lib/env.sh" +# Use planner-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_PLANNER_TOKEN:-${FORGE_TOKEN}}" # shellcheck source=../lib/agent-session.sh source "$FACTORY_ROOT/lib/agent-session.sh" # shellcheck source=../lib/formula-session.sh diff --git a/predictor/predictor-run.sh b/predictor/predictor-run.sh index 989576d..504e377 100755 --- a/predictor/predictor-run.sh +++ b/predictor/predictor-run.sh @@ -20,6 +20,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}" # shellcheck source=../lib/env.sh source "$FACTORY_ROOT/lib/env.sh" +# Use predictor-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_PREDICTOR_TOKEN:-${FORGE_TOKEN}}" # shellcheck source=../lib/agent-session.sh source "$FACTORY_ROOT/lib/agent-session.sh" # shellcheck source=../lib/formula-session.sh diff --git a/supervisor/supervisor-run.sh b/supervisor/supervisor-run.sh index b4ac052..0bdcd5d 100755 --- a/supervisor/supervisor-run.sh +++ b/supervisor/supervisor-run.sh @@ -24,6 +24,8 @@ FACTORY_ROOT="$(dirname "$SCRIPT_DIR")" export PROJECT_TOML="${1:-$FACTORY_ROOT/projects/disinto.toml}" # shellcheck source=../lib/env.sh source "$FACTORY_ROOT/lib/env.sh" +# Use supervisor-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_SUPERVISOR_TOKEN:-${FORGE_TOKEN}}" # shellcheck source=../lib/agent-session.sh source "$FACTORY_ROOT/lib/agent-session.sh" # shellcheck source=../lib/formula-session.sh diff --git a/vault/vault-agent.sh b/vault/vault-agent.sh index 202474d..eab384f 100755 --- a/vault/vault-agent.sh +++ b/vault/vault-agent.sh @@ -12,6 +12,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/../lib/env.sh" +# Use vault-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" VAULT_DIR="${FACTORY_ROOT}/vault" PROMPT_FILE="${VAULT_DIR}/PROMPT.md" diff --git a/vault/vault-fire.sh b/vault/vault-fire.sh index 18d3d90..515fba9 100755 --- a/vault/vault-fire.sh +++ b/vault/vault-fire.sh @@ -14,6 +14,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/../lib/env.sh" +# Use vault-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" VAULT_DIR="${FACTORY_ROOT}/vault" LOCKS_DIR="${VAULT_DIR}/.locks" diff --git a/vault/vault-poll.sh b/vault/vault-poll.sh index 5dbf06c..736e897 100755 --- a/vault/vault-poll.sh +++ b/vault/vault-poll.sh @@ -20,6 +20,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/../lib/env.sh" +# Use vault-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" LOGFILE="${FACTORY_ROOT}/vault/vault.log" STATUSFILE="/tmp/vault-status" diff --git a/vault/vault-reject.sh b/vault/vault-reject.sh index 95598bc..821094c 100755 --- a/vault/vault-reject.sh +++ b/vault/vault-reject.sh @@ -7,6 +7,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "${SCRIPT_DIR}/../lib/env.sh" +# Use vault-bot's own Forgejo identity (#747) +FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" VAULT_DIR="${FACTORY_ROOT}/vault" LOGFILE="${VAULT_DIR}/vault.log" From 6dcf35c5f92e893cd07622575ea9719ede1e2f35 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 26 Mar 2026 16:20:40 +0000 Subject: [PATCH 2/2] fix: Extract vault-env.sh to deduplicate vault token override Moves shared env.sh sourcing + vault-bot token override into vault/vault-env.sh so the three vault sub-scripts no longer share a duplicate 5-line block. Co-Authored-By: Claude Opus 4.6 (1M context) --- vault/vault-agent.sh | 4 +--- vault/vault-env.sh | 9 +++++++++ vault/vault-fire.sh | 4 +--- vault/vault-reject.sh | 4 +--- 4 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 vault/vault-env.sh diff --git a/vault/vault-agent.sh b/vault/vault-agent.sh index eab384f..1bda3b9 100755 --- a/vault/vault-agent.sh +++ b/vault/vault-agent.sh @@ -11,9 +11,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "${SCRIPT_DIR}/../lib/env.sh" -# Use vault-bot's own Forgejo identity (#747) -FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" +source "${SCRIPT_DIR}/vault-env.sh" VAULT_DIR="${FACTORY_ROOT}/vault" PROMPT_FILE="${VAULT_DIR}/PROMPT.md" diff --git a/vault/vault-env.sh b/vault/vault-env.sh new file mode 100644 index 0000000..79e4176 --- /dev/null +++ b/vault/vault-env.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# vault-env.sh — Shared vault environment: loads lib/env.sh and activates +# vault-bot's Forgejo identity (#747). +# Source this instead of lib/env.sh in vault scripts. + +# shellcheck source=../lib/env.sh +source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/env.sh" +# Use vault-bot's own Forgejo identity +FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" diff --git a/vault/vault-fire.sh b/vault/vault-fire.sh index 515fba9..e240fb0 100755 --- a/vault/vault-fire.sh +++ b/vault/vault-fire.sh @@ -13,9 +13,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "${SCRIPT_DIR}/../lib/env.sh" -# Use vault-bot's own Forgejo identity (#747) -FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" +source "${SCRIPT_DIR}/vault-env.sh" VAULT_DIR="${FACTORY_ROOT}/vault" LOCKS_DIR="${VAULT_DIR}/.locks" diff --git a/vault/vault-reject.sh b/vault/vault-reject.sh index 821094c..9699753 100755 --- a/vault/vault-reject.sh +++ b/vault/vault-reject.sh @@ -6,9 +6,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -source "${SCRIPT_DIR}/../lib/env.sh" -# Use vault-bot's own Forgejo identity (#747) -FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" +source "${SCRIPT_DIR}/vault-env.sh" VAULT_DIR="${FACTORY_ROOT}/vault" LOGFILE="${VAULT_DIR}/vault.log"