bug: setup_forge's admin_token is a local variable, not exported — setup_ops_repo falls back to dev-bot token and fails with 403 #583

Closed
opened 2026-04-10 10:02:57 +00:00 by dev-bot · 0 comments
Collaborator

Description

lib/forge-setup.sh (setup_forge) creates a fresh admin token for the disinto-admin user around line 237:

local admin_token
admin_token=$(curl -sf -X POST \
  -u "${admin_user}:${admin_pass}" \
  -H "Content-Type: application/json" \
  "${forge_url}/api/v1/users/${admin_user}/tokens" \
  -d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \
  | jq -r '.sha1 // empty') || admin_token=""

This token is used within setup_forge() for admin operations (creating bot .profile repos at lines 320, 346, 472, 479, 484). But admin_token is declared as a local variable. When setup_forge returns, the local goes out of scope.

Later in bin/disinto init, setup_ops_repo is called:

setup_ops_repo "$forge_url" "$ops_slug" "$ops_root" "$branch"

setup_ops_repo (in lib/ops-setup.sh) references admin_token with a fallback:

-H "Authorization: token ${admin_token:-${FORGE_TOKEN}}"

But admin_token is not in scope (it was local to setup_forge), so the fallback kicks in: FORGE_TOKEN is dev-bot's token. dev-bot does not have admin-level permissions on the forgejo instance, so:

  1. The check whether disinto-admin/harb-ops exists uses dev-bot's token (also fails with 403 if dev-bot isn't a collaborator) → init thinks the repo doesn't exist
  2. The create attempt uses POST /api/v1/orgs/disinto-admin/repos with dev-bot's token → 403 (dev-bot isn't in disinto-admin org; also disinto-admin is a user, not an org, see sister issue)
  3. The admin-API fallback POST /api/v1/admin/users/disinto-admin/repos with dev-bot's token → 403 (dev-bot is not a site admin)

Init prints Error: failed to create ops repo 'disinto-admin/harb-ops' (HTTP 403) and bails before setting up collaborators, branch protection, and migration.

Reproduction

  1. Fresh disinto deployment (or any deployment where the ops repo doesn't yet exist)
  2. Run bin/disinto init <repo-url> --yes
  3. Observe init get through setup_forge successfully, then fail at ── Ops repo setup ── with HTTP 403

Workaround

Export admin_token (or HUMAN_TOKEN if already in .env) before calling bin/disinto init:

source .env
export admin_token="${HUMAN_TOKEN}"
./bin/disinto init <repo-url> --yes

I used this workaround on harb-dev-box and init completed the ops repo setup successfully.

Fix

Option A — Export admin_token from setup_forge:

export admin_token
admin_token=$(curl ... | jq -r '.sha1 // empty') || admin_token=""

Option B — Pass admin_token as explicit arg to setup_ops_repo:

# in bin/disinto init
setup_ops_repo "$forge_url" "$ops_slug" "$ops_root" "$branch" "$admin_token"
# in lib/ops-setup.sh
setup_ops_repo() {
  local forge_url="$1" ops_slug="$2" ops_root="$3" primary_branch="${4:-main}" admin_token="${5:-${FORGE_TOKEN}}"
  ...
}

Option B is cleaner — explicit dependency, no reliance on env-var leakage.

Also consider: the HUMAN_TOKEN already stored in .env (line 279 in setup_forge) is a suitable admin token for setup_ops_repo to use. Scripts that run after init (supervisor, agents) would benefit from a documented "which token for which operation" mapping.

Context

Discovered while running bin/disinto init as an idempotency experiment on harb-dev-box. Part of a cluster of ~9 init bugs found in that session.

## Description `lib/forge-setup.sh` (setup_forge) creates a fresh admin token for the `disinto-admin` user around line 237: ```bash local admin_token admin_token=$(curl -sf -X POST \ -u "${admin_user}:${admin_pass}" \ -H "Content-Type: application/json" \ "${forge_url}/api/v1/users/${admin_user}/tokens" \ -d '{"name":"disinto-admin-token","scopes":["all"]}' 2>/dev/null \ | jq -r '.sha1 // empty') || admin_token="" ``` This token is used within `setup_forge()` for admin operations (creating bot .profile repos at lines 320, 346, 472, 479, 484). But `admin_token` is declared as a **local** variable. When `setup_forge` returns, the local goes out of scope. Later in `bin/disinto init`, `setup_ops_repo` is called: ```bash setup_ops_repo "$forge_url" "$ops_slug" "$ops_root" "$branch" ``` `setup_ops_repo` (in `lib/ops-setup.sh`) references `admin_token` with a fallback: ```bash -H "Authorization: token ${admin_token:-${FORGE_TOKEN}}" ``` But `admin_token` is not in scope (it was local to `setup_forge`), so the fallback kicks in: `FORGE_TOKEN` is dev-bot's token. dev-bot does not have admin-level permissions on the forgejo instance, so: 1. The check whether `disinto-admin/harb-ops` exists uses dev-bot's token (also fails with 403 if dev-bot isn't a collaborator) → init thinks the repo doesn't exist 2. The create attempt uses `POST /api/v1/orgs/disinto-admin/repos` with dev-bot's token → 403 (dev-bot isn't in disinto-admin org; also `disinto-admin` is a user, not an org, see sister issue) 3. The admin-API fallback `POST /api/v1/admin/users/disinto-admin/repos` with dev-bot's token → 403 (dev-bot is not a site admin) Init prints `Error: failed to create ops repo 'disinto-admin/harb-ops' (HTTP 403)` and bails before setting up collaborators, branch protection, and migration. ## Reproduction 1. Fresh disinto deployment (or any deployment where the ops repo doesn't yet exist) 2. Run `bin/disinto init <repo-url> --yes` 3. Observe init get through `setup_forge` successfully, then fail at `── Ops repo setup ──` with HTTP 403 ## Workaround Export `admin_token` (or `HUMAN_TOKEN` if already in `.env`) before calling `bin/disinto init`: ```bash source .env export admin_token="${HUMAN_TOKEN}" ./bin/disinto init <repo-url> --yes ``` I used this workaround on harb-dev-box and init completed the ops repo setup successfully. ## Fix Option A — **Export admin_token** from `setup_forge`: ```bash export admin_token admin_token=$(curl ... | jq -r '.sha1 // empty') || admin_token="" ``` Option B — **Pass admin_token as explicit arg** to `setup_ops_repo`: ```bash # in bin/disinto init setup_ops_repo "$forge_url" "$ops_slug" "$ops_root" "$branch" "$admin_token" # in lib/ops-setup.sh setup_ops_repo() { local forge_url="$1" ops_slug="$2" ops_root="$3" primary_branch="${4:-main}" admin_token="${5:-${FORGE_TOKEN}}" ... } ``` Option B is cleaner — explicit dependency, no reliance on env-var leakage. Also consider: the `HUMAN_TOKEN` already stored in `.env` (line 279 in setup_forge) is a suitable admin token for setup_ops_repo to use. Scripts that run after init (supervisor, agents) would benefit from a documented "which token for which operation" mapping. ## Context Discovered while running `bin/disinto init` as an idempotency experiment on harb-dev-box. Part of a cluster of ~9 init bugs found in that session.
dev-bot added the
backlog
label 2026-04-10 10:02:57 +00:00
dev-qwen self-assigned this 2026-04-10 14:05:28 +00:00
dev-qwen added
in-progress
and removed
backlog
labels 2026-04-10 14:05:28 +00:00
dev-qwen removed their assignment 2026-04-10 14:14:36 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: disinto-admin/disinto#583
No description provided.