feat(forgejo): make webhook triggers traceable and read write access from the payload #13

Merged
lucas merged 3 commits from feat/forgejo-comment-driven into main 2026-10-15 17:10:31 +02:00
Collaborator

Three changes to the Forgejo comment pipeline.

Delivery log (0a4caa6). Every request to POST /api/forgejo/webhook now becomes one row in a new forgejo_deliveries log (migration 012). The log is capped at the newest 200 rows, because unauthenticated callers can add rows. Each row records the outcome, the HTTP status returned, the reason or error, the delivery and event headers, the body size, and, for verified payloads only, the repo, number, action and sender. Accepted and duplicate rows link to their forgejo_triggers row, and duplicates link to the original trigger. GET /api/forgejo/triggers is replaced by GET /api/forgejo/deliveries. Settings → Forgejo → Recent deliveries now lists the 20 newest entries, each with an outcome badge, HTTP status, detail, delivery id, and the linked trigger's status and session link. Responses to Forgejo are unchanged.

Write access from the signed payload (2eace98). The dispatcher used to ask Forgejo's collaborator-permission endpoint, which Forgejo answers only for site or repo admins, so every trigger failed with a 403 when the agent account was a plain write collaborator. Write access now comes from the payload's repository.permissions, which Forgejo computes for the sender and the receiver has already authenticated by HMAC. The agent account no longer needs admin rights on the repository.

Trigger lookup tools (ec9fa93). Failure comments name a trigger id that nothing in the deck could resolve. Interactive deck sessions now get deck_forgejo_trigger_get (the trigger row, the triggering comment's URL and body from the stored payload, and its linked delivery-log rows) and deck_forgejo_trigger_list (recent triggers for a repo, optionally one issue/PR), so a failure can be investigated by asking the agent.

Decisions

  • Moving the X-Forgejo-Delivery check after JSON.parse changes one edge case. A signed request with both a missing delivery id and an unparseable body now gets 400 invalid json body instead of 400 missing X-Forgejo-Delivery. Every single-fault response is unchanged.
  • The tests for the two signature failure messages are the two existing 401 tests (empty signature, and a signature over a different body), now asserting the message, verified: false and repo: null. No duplicate tests were added.
  • If insertForgejoTrigger hits a dedupe conflict but cannot find the conflicting row, it throws instead of returning an empty existingId. The receiver records that as store_failed with a 500.
  • too_large, unsupported_content_type and not_configured rows take bodyBytes from the Content-Length header (null if it's missing), since the body is never read for those.
  • repository.permissions describes the webhook's sender, who is the editor on an edit. A comment edited by someone other than its author is therefore dropped (edited by someone other than its author), and a payload without permissions fails closed (author lacks write access (unknown)).
  • Sessions started by a Forgejo comment don't get the trigger lookup tools: their tool surface is the fixed allowlist in session-policy.ts, and a trigger's reason can contain local paths or provider internals that must stay off Forgejo.
  • The trigger lookup accepts ids in any case (ids are uppercase Crockford) and matches repos case-insensitively. Linked deliveries can be empty for older triggers once the 200-row retention has pruned them.

Verification

  • bun run typecheck (protocol, server, web): passes.
  • bun test src/routes-forgejo-webhook.test.ts src/db/forgejo-deliveries.test.ts src/forgejo/dispatcher.test.ts: 29 pass, 0 fail (run on 0a4caa6).
  • bun test src/bridge/forgejo-trigger-tools.test.ts src/db/forgejo-deliveries.test.ts: 6 pass, 0 fail.
  • Full server bun test at ec9fa93 (all three commits): 709 pass, 0 fail across 47 files.
  • bun run --filter '@omp-deck/web' build: succeeds.
  • The end-to-end smoke test (isolated server, curl requests, screenshot of the panel, a live trigger) was not run.
Three changes to the Forgejo comment pipeline. **Delivery log (`0a4caa6`).** Every request to `POST /api/forgejo/webhook` now becomes one row in a new `forgejo_deliveries` log (migration 012). The log is capped at the newest 200 rows, because unauthenticated callers can add rows. Each row records the outcome, the HTTP status returned, the reason or error, the delivery and event headers, the body size, and, for verified payloads only, the repo, number, action and sender. Accepted and duplicate rows link to their `forgejo_triggers` row, and duplicates link to the original trigger. `GET /api/forgejo/triggers` is replaced by `GET /api/forgejo/deliveries`. Settings → Forgejo → Recent deliveries now lists the 20 newest entries, each with an outcome badge, HTTP status, detail, delivery id, and the linked trigger's status and session link. Responses to Forgejo are unchanged. **Write access from the signed payload (`2eace98`).** The dispatcher used to ask Forgejo's collaborator-permission endpoint, which Forgejo answers only for site or repo admins, so every trigger failed with a 403 when the agent account was a plain write collaborator. Write access now comes from the payload's `repository.permissions`, which Forgejo computes for the sender and the receiver has already authenticated by HMAC. The agent account no longer needs admin rights on the repository. **Trigger lookup tools (`ec9fa93`).** Failure comments name a trigger id that nothing in the deck could resolve. Interactive deck sessions now get `deck_forgejo_trigger_get` (the trigger row, the triggering comment's URL and body from the stored payload, and its linked delivery-log rows) and `deck_forgejo_trigger_list` (recent triggers for a repo, optionally one issue/PR), so a failure can be investigated by asking the agent. ## Decisions - Moving the `X-Forgejo-Delivery` check after `JSON.parse` changes one edge case. A signed request with both a missing delivery id and an unparseable body now gets `400 invalid json body` instead of `400 missing X-Forgejo-Delivery`. Every single-fault response is unchanged. - The tests for the two signature failure messages are the two existing 401 tests (empty signature, and a signature over a different body), now asserting the message, `verified: false` and `repo: null`. No duplicate tests were added. - If `insertForgejoTrigger` hits a dedupe conflict but cannot find the conflicting row, it throws instead of returning an empty `existingId`. The receiver records that as `store_failed` with a 500. - `too_large`, `unsupported_content_type` and `not_configured` rows take `bodyBytes` from the `Content-Length` header (null if it's missing), since the body is never read for those. - `repository.permissions` describes the webhook's sender, who is the editor on an edit. A comment edited by someone other than its author is therefore dropped (`edited by someone other than its author`), and a payload without permissions fails closed (`author lacks write access (unknown)`). - Sessions started by a Forgejo comment don't get the trigger lookup tools: their tool surface is the fixed allowlist in `session-policy.ts`, and a trigger's reason can contain local paths or provider internals that must stay off Forgejo. - The trigger lookup accepts ids in any case (ids are uppercase Crockford) and matches repos case-insensitively. Linked deliveries can be empty for older triggers once the 200-row retention has pruned them. ## Verification - `bun run typecheck` (protocol, server, web): passes. - `bun test src/routes-forgejo-webhook.test.ts src/db/forgejo-deliveries.test.ts src/forgejo/dispatcher.test.ts`: 29 pass, 0 fail (run on `0a4caa6`). - `bun test src/bridge/forgejo-trigger-tools.test.ts src/db/forgejo-deliveries.test.ts`: 6 pass, 0 fail. - Full server `bun test` at `ec9fa93` (all three commits): 709 pass, 0 fail across 47 files. - `bun run --filter '@omp-deck/web' build`: succeeds. - The end-to-end smoke test (isolated server, curl requests, screenshot of the panel, a live trigger) was not run.
Every request to POST /api/forgejo/webhook now becomes one row in a new
forgejo_deliveries table (migration 012), capped at the newest 200 rows,
with outcome, HTTP status, detail and best-effort context from the
verified payload. Unverified requests record metadata only. Accepted and
duplicate deliveries link to their forgejo_triggers row.

GET /api/forgejo/triggers is replaced by GET /api/forgejo/deliveries, and
the settings panel lists those entries. Responses to Forgejo are
unchanged.

Co-Authored-By: Claude Opus 5.5 via omp-deck
The dispatcher asked Forgejo for the commenter's repo permission, an
endpoint Forgejo answers only for site or repo admins, so every trigger
from anyone but the agent failed with a 403 when the agent account was
a plain write collaborator.

Write access now comes from the webhook payload's
repository.permissions, which Forgejo computes for the sender and which
the receiver has already authenticated by HMAC. Because that is the
editor's permission on an edit, a comment edited by someone other than
its author is dropped, as is a payload without permissions. The agent
account no longer needs admin rights on the repository.

Co-Authored-By: Claude Opus 5.5 via omp-deck
Failure comments on Forgejo name a trigger id that nothing in the deck
could resolve. Interactive deck sessions now get deck_forgejo_trigger_get
(row, triggering comment, linked deliveries) and deck_forgejo_trigger_list
(recent triggers for a repo, optionally one issue/PR). Trigger-started
sessions don't get them: a trigger's reason may hold local paths or
provider internals that must stay off Forgejo.

Co-Authored-By: Claude Opus 5.5 via omp-deck
omp-deck changed title from feat(forgejo): log every webhook delivery in Recent deliveries to feat(forgejo): make webhook triggers traceable and read write access from the payload 2026-10-15 17:08:59 +02:00
lucas merged commit 606bceaba6 into main 2026-10-15 17:10:31 +02:00
lucas deleted branch feat/forgejo-comment-driven 2026-10-15 17:10:31 +02:00
Sign in to join this conversation.
No reviewers
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
lucas/omp-deck!13
No description provided.