feat(secrets): persist operator admin PAT, hard-fail rotate-secret without it #933

Merged
charles merged 1 commit from fix/onboarding-persist-admin-token into main 2026-05-07 22:55:42 +00:00
Collaborator

Summary

  • Onboarding apply now persists body.admin_token under a per-forge secret name (FORGEJO_ADMIN_TOKEN / GITLAB_ADMIN_TOKEN) after a successful mint, so later repo-admin operations reuse it without re-prompting the operator.
  • POST /watched-repos/:owner/:name/rotate-secret reads that secret instead of grabbing any agent token (which lacks admin scope on operator-owned repos and silently 502'd on editWebhook). Returns 412 with a clear message when the secret is absent or the forge is unsupported.
  • Existing /secrets HTTP surface manages the value going forward (POST / PUT / DELETE / rotate). No schema changes — secret table + name conventions cover it.

Why

Discovered while debugging silent webhook signature failures on charles/claude-hooks. The rotate-secret route picked the first agent token via listResolvedAgents(); agent tokens do not carry admin perms on operator-owned repos, so editWebhook returned 502 with no actionable error. The admin PAT entered during onboarding was used once for mintAgentTokensViaAdmin and then dropped on the floor.

Files

  • apps/server/src/setup/admin-bootstrap.ts — new adminTokenSecretName(forge) helper.
  • apps/server/src/http/handlers/onboarding-apply.ts — encrypt + upsert admin token after successful mint.
  • apps/server/src/main.ts — rotate-secret reads admin PAT from secret store, hard-fails when missing.

Test plan

  • just qa — 3262 pass / 0 fail.
  • Onboarding wizard with admin-token mode → verify FORGEJO_ADMIN_TOKEN row appears in /secrets.
  • Re-run onboarding → row updates (idempotent, rotated_at bumps).
  • POST /watched-repos/.../rotate-secret succeeds with admin token present.
  • Same call returns 412 when secret deleted via DELETE /secrets/FORGEJO_ADMIN_TOKEN.
  • Operator can edit value via PUT /secrets/FORGEJO_ADMIN_TOKEN from settings UI.

Out of scope

  • Forgejo editWebhook PATCH silently ignores secret field updates — rotate route should use DELETE+CREATE instead. Tracked separately.
  • Webhook URL pluralization bug (/webhooks/ vs /webhook/) producing dead hooks at install time. Tracked separately.
## Summary - Onboarding `apply` now persists `body.admin_token` under a per-forge secret name (`FORGEJO_ADMIN_TOKEN` / `GITLAB_ADMIN_TOKEN`) after a successful mint, so later repo-admin operations reuse it without re-prompting the operator. - `POST /watched-repos/:owner/:name/rotate-secret` reads that secret instead of grabbing any agent token (which lacks admin scope on operator-owned repos and silently 502'd on `editWebhook`). Returns **412** with a clear message when the secret is absent or the forge is unsupported. - Existing `/secrets` HTTP surface manages the value going forward (POST / PUT / DELETE / rotate). No schema changes — `secret` table + name conventions cover it. ## Why Discovered while debugging silent webhook signature failures on `charles/claude-hooks`. The rotate-secret route picked the first agent token via `listResolvedAgents()`; agent tokens do not carry admin perms on operator-owned repos, so `editWebhook` returned 502 with no actionable error. The admin PAT entered during onboarding was used once for `mintAgentTokensViaAdmin` and then dropped on the floor. ## Files - `apps/server/src/setup/admin-bootstrap.ts` — new `adminTokenSecretName(forge)` helper. - `apps/server/src/http/handlers/onboarding-apply.ts` — encrypt + upsert admin token after successful mint. - `apps/server/src/main.ts` — rotate-secret reads admin PAT from secret store, hard-fails when missing. ## Test plan - [x] `just qa` — 3262 pass / 0 fail. - [ ] Onboarding wizard with admin-token mode → verify `FORGEJO_ADMIN_TOKEN` row appears in `/secrets`. - [ ] Re-run onboarding → row updates (idempotent, `rotated_at` bumps). - [ ] `POST /watched-repos/.../rotate-secret` succeeds with admin token present. - [ ] Same call returns 412 when secret deleted via `DELETE /secrets/FORGEJO_ADMIN_TOKEN`. - [ ] Operator can edit value via `PUT /secrets/FORGEJO_ADMIN_TOKEN` from settings UI. ## Out of scope - Forgejo `editWebhook` PATCH silently ignores `secret` field updates — rotate route should use DELETE+CREATE instead. Tracked separately. - Webhook URL pluralization bug (`/webhooks/` vs `/webhook/`) producing dead hooks at install time. Tracked separately.
fix(agents): persist agent type provider chain and failover
Some checks failed
qa / dockerfile (pull_request) Successful in 11s
qa / sql-layer-check (pull_request) Successful in 13s
qa / i18n-string-check (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 26s
qa / qa-1 (pull_request) Failing after 10m6s
qa / qa (pull_request) Failing after 0s
ddf660c766
- Add PUT /api/agent-types/:name/dispatch and wire putAgentConfig to save
  provider_chain, failover, and usage cap to agent_type_config.
- Extend agent_type_config schema, GET derived merge, and config revision
  restore for agent_type_config.
- Migration 0009: add dispatch columns via TS helper so legacy stub DBs
  without agent_type_config do not fail migration.
- Invalidate instance skill queries on type-level skill updates (prefix).

Co-authored-by: Cursor <cursoragent@cursor.com>
feat(secrets): persist operator admin PAT, hard-fail rotate-secret without it
All checks were successful
qa / sql-layer-check (pull_request) Successful in 14s
qa / i18n-string-check (pull_request) Successful in 14s
qa / dockerfile (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 40s
qa / qa-1 (pull_request) Successful in 2m8s
qa / qa (pull_request) Successful in 0s
a00628a73c
Onboarding apply now stores the admin token under a per-forge secret name
(FORGEJO_ADMIN_TOKEN / GITLAB_ADMIN_TOKEN) after a successful mint, so
later repo-admin operations can reuse it without re-prompting. The
rotate-secret route reads that secret instead of grabbing any agent
token (which lacks admin scope on operator-owned repos and silently
502'd) and returns 412 with a clear message when the secret is absent.
The existing /secrets HTTP surface manages the value going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer left a comment
  • scope-creep: The admin PAT persistence + rotate-secret fix (3 files, as listed in the PR body) is correct and ready. But this diff also ships a completely separate provider-dispatch feature — 0009_agent_type_config_provider_dispatch.sql, schema columns provider_chain_json / failover_json / usage_threshold_tokens, addProviderDispatchColumnsIfPresent, handleUpsertDispatch, restoreDispatchConfigRevision, PUT /api/agent-types/:name/dispatch, mergeAgent overrides, agent-db-skills-tab.tsx query-key fixes, and api.ts dispatch diff-and-PUT — none of which are mentioned in the title, the Files list, or (presumably) the linked issue. Split it: land the PAT/rotate fix as-is, open a second PR for the dispatch feature.
- **scope-creep**: The admin PAT persistence + rotate-secret fix (3 files, as listed in the PR body) is correct and ready. But this diff also ships a completely separate provider-dispatch feature — `0009_agent_type_config_provider_dispatch.sql`, schema columns `provider_chain_json` / `failover_json` / `usage_threshold_tokens`, `addProviderDispatchColumnsIfPresent`, `handleUpsertDispatch`, `restoreDispatchConfigRevision`, `PUT /api/agent-types/:name/dispatch`, `mergeAgent` overrides, `agent-db-skills-tab.tsx` query-key fixes, and `api.ts` dispatch diff-and-PUT — none of which are mentioned in the title, the Files list, or (presumably) the linked issue. Split it: land the PAT/rotate fix as-is, open a second PR for the dispatch feature.
charles force-pushed fix/onboarding-persist-admin-token from a00628a73c
All checks were successful
qa / sql-layer-check (pull_request) Successful in 14s
qa / i18n-string-check (pull_request) Successful in 14s
qa / dockerfile (pull_request) Successful in 15s
qa / db-schema (pull_request) Successful in 40s
qa / qa-1 (pull_request) Successful in 2m8s
qa / qa (pull_request) Successful in 0s
to 8e9894444b
All checks were successful
qa / sql-layer-check (pull_request) Successful in 18s
qa / dockerfile (pull_request) Successful in 19s
qa / i18n-string-check (pull_request) Successful in 26s
qa / db-schema (pull_request) Successful in 37s
qa / qa-1 (pull_request) Successful in 2m1s
qa / qa (pull_request) Successful in 0s
2026-05-07 22:52:24 +00:00
Compare
Author
Collaborator

Good catch. Branch was cut from fix/agent-type-provider-dispatch so ddf660c7 rode along.

Rebased fix/onboarding-persist-admin-token onto origin/main (dropping the provider-dispatch commit) and force-pushed. Diff is now the 3 files in the PR body only:

 apps/server/src/http/handlers/onboarding-apply.ts | 32 ++++++++++++++++++++++-
 apps/server/src/main.ts                           | 29 ++++++++++++++++----
 apps/server/src/setup/admin-bootstrap.ts          | 12 +++++++++
 3 files changed, 67 insertions(+), 6 deletions(-)

The provider-dispatch work stays on PR #931 as originally intended.

Good catch. Branch was cut from `fix/agent-type-provider-dispatch` so `ddf660c7` rode along. Rebased `fix/onboarding-persist-admin-token` onto `origin/main` (dropping the provider-dispatch commit) and force-pushed. Diff is now the 3 files in the PR body only: ``` apps/server/src/http/handlers/onboarding-apply.ts | 32 ++++++++++++++++++++++- apps/server/src/main.ts | 29 ++++++++++++++++---- apps/server/src/setup/admin-bootstrap.ts | 12 +++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) ``` The provider-dispatch work stays on PR #931 as originally intended.
charles deleted branch fix/onboarding-persist-admin-token 2026-05-07 22:55:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
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
charles/claude-hooks!933
No description provided.