feat(server): answer Forgejo comments via webhook-triggered sessions #8

Merged
lucas merged 7 commits from feat/forgejo-comment-driven into main 2026-10-15 11:58:16 +02:00
Collaborator

Forgejo comments can now start agent sessions. A signed webhook at /api/forgejo/webhook queues issue and PR conversation comments. A dispatcher works through the queue: one trigger at a time per repository, different repositories in parallel. It answers comments that @mention the agent or reply to one of its comments, but only from authors with write access. Each accepted comment starts a new unattended session in the workspace whose git origin is that repository. These sessions get a fixed tool set: git read tools, Forgejo read tools, and comments on the triggering issue or PR only. They never prompt for approval and can't enter plan or idea mode, and that policy is stored so it still applies after a restart. Settings → Forgejo gains a Webhook section (payload URL, generated secret, setup steps) and a list of recent deliveries. When something goes wrong, the agent posts a Forgejo comment; if that fails, it creates an inbox ticket or a notification instead.

Decisions

  • Rows received in the same millisecond keep their insertion order (SQLite rowid breaks ties). The generated ids only sort by time to the millisecond, and a test showed the order flipping.
  • When waiting for a turn to end, the dispatcher ignores an agent_end the SDK marks as non-terminal (isTerminal: false, meaning queued work follows). It also re-checks one tick later whether the session is still streaming.
  • ForgejoDispatcher.whenIdle() was added so the tests can wait on real signals instead of polling with timers.
  • If reading the thread fails, the "not connected" or "ambiguous" notice is posted anyway; a duplicate comment is better than none.
  • If even the workspace lookup fails while reporting a failure, the fallback is a notification.
  • Deliveries list in the settings panel: it shows "Loading…" on first load, and the session link reads "Open session".
  • The webhook intro text in the panel describes the full feature, since the receiver and the dispatcher ship together.
  • The session toolNames list isn't filtered against the tools that are actually registered. The SDK already drops names it doesn't know (sdk.ts ~4009).

Verification

  • bun run typecheck: protocol, server and web pass.
  • cd apps/server && bun test: 689 pass, 0 fail. New tests cover:
    • the webhook route: signature checks, 415, 503, 400 and duplicate handling, self-authored and unsupported events, and that onAccepted fires;
    • the webhook secret in the config;
    • the unattended policy in the permission filter (allow and deny order, the ask-to-deny conversion);
    • the stored session triggers, including that an unreadable trigger throws;
    • the orientation text;
    • mention and reply detection, and the fence around the comment in the first message;
    • the dispatcher with a real temporary database and fake bridge/API: queue order per repository, parallel repositories, the write-access check, the "not connected" notice not repeating, the ambiguity comment plus a single ticket, failures on agent error, the 30-minute timeout (fake timers), and failing rows interrupted by a restart.
  • The dispatcher tests passed 8 runs in a row after the ordering fix.
  • cd apps/web && bun test: 229 pass.
  • Not run, by request: T-66's check that a real SDK session exposes only the policy tools, and every manual, UI or real-Forgejo check.
Forgejo comments can now start agent sessions. A signed webhook at `/api/forgejo/webhook` queues issue and PR conversation comments. A dispatcher works through the queue: one trigger at a time per repository, different repositories in parallel. It answers comments that @mention the agent or reply to one of its comments, but only from authors with write access. Each accepted comment starts a new unattended session in the workspace whose git `origin` is that repository. These sessions get a fixed tool set: git read tools, Forgejo read tools, and comments on the triggering issue or PR only. They never prompt for approval and can't enter plan or idea mode, and that policy is stored so it still applies after a restart. Settings → Forgejo gains a Webhook section (payload URL, generated secret, setup steps) and a list of recent deliveries. When something goes wrong, the agent posts a Forgejo comment; if that fails, it creates an inbox ticket or a notification instead. ## Decisions - Rows received in the same millisecond keep their insertion order (SQLite `rowid` breaks ties). The generated ids only sort by time to the millisecond, and a test showed the order flipping. - When waiting for a turn to end, the dispatcher ignores an `agent_end` the SDK marks as non-terminal (`isTerminal: false`, meaning queued work follows). It also re-checks one tick later whether the session is still streaming. - `ForgejoDispatcher.whenIdle()` was added so the tests can wait on real signals instead of polling with timers. - If reading the thread fails, the "not connected" or "ambiguous" notice is posted anyway; a duplicate comment is better than none. - If even the workspace lookup fails while reporting a failure, the fallback is a notification. - Deliveries list in the settings panel: it shows "Loading…" on first load, and the session link reads "Open session". - The webhook intro text in the panel describes the full feature, since the receiver and the dispatcher ship together. - The session `toolNames` list isn't filtered against the tools that are actually registered. The SDK already drops names it doesn't know (`sdk.ts` ~4009). ## Verification - `bun run typecheck`: protocol, server and web pass. - `cd apps/server && bun test`: 689 pass, 0 fail. New tests cover: - the webhook route: signature checks, 415, 503, 400 and duplicate handling, self-authored and unsupported events, and that `onAccepted` fires; - the webhook secret in the config; - the unattended policy in the permission filter (allow and deny order, the ask-to-deny conversion); - the stored session triggers, including that an unreadable trigger throws; - the orientation text; - mention and reply detection, and the fence around the comment in the first message; - the dispatcher with a real temporary database and fake bridge/API: queue order per repository, parallel repositories, the write-access check, the "not connected" notice not repeating, the ambiguity comment plus a single ticket, failures on agent error, the 30-minute timeout (fake timers), and failing rows interrupted by a restart. - The dispatcher tests passed 8 runs in a row after the ordering fix. - `cd apps/web && bun test`: 229 pass. - Not run, by request: T-66's check that a real SDK session exposes only the policy tools, and every manual, UI or real-Forgejo check.
Sessions started by a trigger (first kind: forgejo_conversation) have
nobody to answer permission prompts. Their trigger is persisted per
session and fixes the session for its whole lifetime, including resumes:
the policy's tool list is the complete tool surface (built with
restrictToolNames, since the SDK only gates bash/edit/delete/move), and
per-call checks allow comments only on the triggering issue/PR.

In trigger sessions every approval prompt becomes an "unattended" deny,
explicit deny rules still win over the policy, plan and idea mode are
refused, and resume fails closed when the stored trigger is unreadable.
A trigger session's system prompt carries conversation orientation.

Co-Authored-By: Claude Opus 5.5 via omp-deck
Forgejo can now push issue and PR conversation comments to
POST /api/forgejo/webhook. Deliveries are authenticated solely by the
HMAC-SHA256 signature over the raw body, using a webhook secret the
deck generates (readable in the settings, since it must be pasted into
Forgejo; Disconnect deletes it). The agent's own comments, inline
review comments and other events are acknowledged but not stored;
accepted deliveries are queued as pending, deduplicated by delivery id.

Settings -> Forgejo gains a Webhook section (payload URL, secret
generation, setup steps) and a list of the 20 most recent deliveries
with their status, reason and session link.

Co-Authored-By: Claude Opus 5.5 via omp-deck
Queued Forgejo comment triggers are now dispatched: FIFO per repository
with one running trigger per repository, repositories in parallel. A
comment counts when it @mentions the agent (outside code and quotes),
an edit adds such a mention, it quotes an agent comment, or it directly
follows one; its author needs write access. The repository is matched
to a registered workspace by git origin, and each trigger starts a new
forgejo_conversation session seeded with the comment.

Problems surface where people look: a comment when the repository is
not connected or maps to several workspaces (plus an inbox ticket for
the latter), and a failure comment naming the deck session on session
errors, 30-minute timeouts or restarts mid-turn, falling back to an
inbox ticket or notification when Forgejo can't be reached.

Co-Authored-By: Claude Opus 5.5 via omp-deck
omp-deck left a comment

The design holds up well overall. Restricting the tool surface when the session is created, rather than in the permission filter, is the right call. Resume failing closed when the trigger row can't be read, and the per-repo queue, are both solid. The inline comments cover one gap in the policy boundary (the repository is never checked, only the number), a dedupe question, a misclassification on older Forgejo versions, and a few operational points. Nits are marked as such.

The design holds up well overall. Restricting the tool surface when the session is created, rather than in the permission filter, is the right call. Resume failing closed when the trigger row can't be read, and the per-repo queue, are both solid. The inline comments cover one gap in the policy boundary (the repository is never checked, only the number), a dedupe question, a misclassification on older Forgejo versions, and a few operational points. Nits are marked as such.
@ -0,0 +76,4 @@
decide(toolName, params, scope) {
if (Object.hasOwn(FORGEJO_COMMENT_TOOLS, toolName)) {
// Both tools are accepted for issues and PRs alike: PR comments use issue routes.
if ((params as { number?: unknown } | null)?.number === scope.number) return "allow";
Author
Collaborator

The policy compares only number and ignores scope.owner/scope.repo. The comment tools post to whichever repository buildForgejoTools(cwd) resolves from the checkout's current origin, both when the session is created and again on every resume. The workspace index, on the other hand, caches the origin and never re-checks it (see workspace-index.ts). If a checkout's origin changes, the dispatcher can match a workspace for a/x while the session's tools point at b/y, and #N then gets a comment in the wrong repository. The same can happen when an old trigger session is resumed after its origin was changed.

Suggestion: in createSession/resumeSession, when a trigger is set, compare the repository buildForgejoTools resolved against trigger.scope and refuse the session (or skip the Forgejo tools) if they differ.

The policy compares only `number` and ignores `scope.owner`/`scope.repo`. The comment tools post to whichever repository `buildForgejoTools(cwd)` resolves from the checkout's **current** `origin`, both when the session is created and again on every resume. The workspace index, on the other hand, caches the origin and never re-checks it (see `workspace-index.ts`). If a checkout's `origin` changes, the dispatcher can match a workspace for `a/x` while the session's tools point at `b/y`, and `#N` then gets a comment in the wrong repository. The same can happen when an old trigger session is resumed after its origin was changed. Suggestion: in `createSession`/`resumeSession`, when a trigger is set, compare the repository `buildForgejoTools` resolved against `trigger.scope` and refuse the session (or skip the Forgejo tools) if they differ.
Author
Collaborator

Fixed in d3cb733. buildForgejoTools now also returns the repository it resolved. When a trigger is set, createSession and resumeSession call assertTriggerRepo and refuse the session if that repository isn't scope.owner/repo (case-insensitive) or the checkout has no Forgejo tools at all. A resumed session whose origin has drifted therefore fails closed as well. The dispatcher records the refusal as a failed trigger.

Fixed in d3cb733. `buildForgejoTools` now also returns the repository it resolved. When a trigger is set, `createSession` and `resumeSession` call `assertTriggerRepo` and refuse the session if that repository isn't `scope.owner/repo` (case-insensitive) or the checkout has no Forgejo tools at all. A resumed session whose origin has drifted therefore fails closed as well. The dispatcher records the refusal as a failed trigger.
lucas marked this conversation as resolved
@ -0,0 +159,4 @@
const longestRun = Math.max(0, ...(body.match(/`+/g) ?? []).map((r) => r.length));
const fence = "`".repeat(Math.max(3, longestRun + 1));
return [
`Forgejo comment on ${t.repo} ${t.isPull ? "PR" : "issue"} #${t.number}: "${issue?.title ?? "?"}"`,
Author
Collaborator

Nit: the comment body is fenced, but the issue title goes into the seed unfenced, and the issue author doesn't need write access. The system context marks comment text as untrusted, but not titles. Consider marking the title as untrusted too.

Nit: the comment body is fenced, but the issue title goes into the seed unfenced, and the issue author doesn't need write access. The system context marks comment text as untrusted, but not titles. Consider marking the title as untrusted too.
Author
Collaborator

Done in d3cb733. The untrusted-input rule in the orientation now covers comment text, issue and PR titles, and anything else written by Forgejo users.

Done in d3cb733. The untrusted-input rule in the orientation now covers comment text, issue and PR titles, and anything else written by Forgejo users.
lucas marked this conversation as resolved
@ -0,0 +215,4 @@
buildConversationSeed({ repo: row.repo, number: row.number, isPull: row.isPull, reason, payload }),
);
if (outcome === "ok") {
finishForgejoTrigger(row.id, "done", reason);
Author
Collaborator

The session stays live after its turn. The bridge never disposes idle sessions, so every accepted comment leaves a full SDK session in memory until the server restarts. Since there's no user on the other end, consider handle.dispose() (non-destructive: the JSONL stays and "Open session" can still resume it) once the turn has settled, on both the success and failure paths.

The session stays live after its turn. The bridge never disposes idle sessions, so every accepted comment leaves a full SDK session in memory until the server restarts. Since there's no user on the other end, consider `handle.dispose()` (non-destructive: the JSONL stays and "Open session" can still resume it) once the turn has settled, on both the success and failure paths.
Author
Collaborator

Done in 69d517e. The dispatcher calls handle.dispose() once the turn has settled, on success, failure and timeout alike. This is non-destructive: the JSONL stays, and opening the session from Recent deliveries resumes it.

Done in 69d517e. The dispatcher calls `handle.dispose()` once the turn has settled, on success, failure and timeout alike. This is non-destructive: the JSONL stays, and opening the session from Recent deliveries resumes it.
lucas marked this conversation as resolved
@ -0,0 +308,4 @@
if (api) {
const [owner = "", repo = ""] = row.repo.split("/");
const body =
`omp-deck failed to handle this comment: ${reason}.` +
Author
Collaborator

reason goes into a public Forgejo comment as is. On the process() catch path, that is err.message from any exception (bridge, SDK, DB), which can include local paths or other internal details. On public repositories this leaks information. Suggestion: post a generic message plus the deck session id or trigger id, and keep the detailed reason in the row and the server log, where the deliveries list already shows it.

`reason` goes into a public Forgejo comment as is. On the `process()` catch path, that is `err.message` from any exception (bridge, SDK, DB), which can include local paths or other internal details. On public repositories this leaks information. Suggestion: post a generic message plus the deck session id or trigger id, and keep the detailed reason in the row and the server log, where the deliveries list already shows it.
Author
Collaborator

Fixed in 69d517e. The public comment now carries only a fixed label per failure kind (restart, timeout, agent turn failed, Forgejo API error, internal error) plus the trigger and session ids. The detailed reason stays in the row (Recent deliveries), the server log and the fallback inbox ticket. The test asserts that the provider error text doesn't appear in the comment.

Fixed in 69d517e. The public comment now carries only a fixed label per failure kind (restart, timeout, agent turn failed, Forgejo API error, internal error) plus the trigger and session ids. The detailed reason stays in the row (Recent deliveries), the server log and the fallback inbox ticket. The test asserts that the provider error text doesn't appear in the comment.
lucas marked this conversation as resolved
@ -0,0 +34,4 @@
* `issue_comment` still classifies as an issue comment.
*/
export function resolveEventType(eventTypeHeader: string | undefined, eventHeader: string | undefined): string {
return eventTypeHeader ?? eventHeader ?? "";
Author
Collaborator

In the fallback for Forgejo versions that send no X-Forgejo-Event-Type, the module doc says inline review comments arrive with X-Forgejo-Event: pull_request_comment, while conversation comments arrive as issue_comment. So in the fallback, pull_request_comment means an inline review comment, but classifyForgejoDelivery accepts it as a conversation comment. The fallback should map pull_request_comment to "ignore" (or only fall back for issue_comment). A test for this would help.

In the fallback for Forgejo versions that send no `X-Forgejo-Event-Type`, the module doc says inline review comments arrive with `X-Forgejo-Event: pull_request_comment`, while conversation comments arrive as `issue_comment`. So in the fallback, `pull_request_comment` means an inline review comment, but `classifyForgejoDelivery` accepts it as a conversation comment. The fallback should map `pull_request_comment` to "ignore" (or only fall back for `issue_comment`). A test for this would help.
Author
Collaborator

Right, fixed in 69d517e. Without X-Forgejo-Event-Type, resolveEventType now maps an Event of pull_request_comment to pull_request_review_comment, so it is ignored. issue_comment still falls back as before. Tests cover both fallback cases.

Right, fixed in 69d517e. Without `X-Forgejo-Event-Type`, `resolveEventType` now maps an Event of `pull_request_comment` to `pull_request_review_comment`, so it is ignored. `issue_comment` still falls back as before. Tests cover both fallback cases.
lucas marked this conversation as resolved
@ -0,0 +55,4 @@
/** Index a newly registered workspace. No-op until the index has been built. */
export function noteWorkspaceAdded(ws: Workspace): void {
if (!index) return;
Author
Collaborator

Two races, either of which leaves a newly registered workspace unmatched until restart (and posts "not connected" in the meantime):

  • If the workspace is registered while the first build is still running, this is a no-op, and the build's listWorkspaces() snapshot may already be out of date.
  • The repoKey lookup isn't awaited, so a trigger that arrives right after registration misses the workspace.

Possible fix: on a miss, check the DB workspaces that aren't in the index before returning nothing.

Two races, either of which leaves a newly registered workspace unmatched until restart (and posts "not connected" in the meantime): - If the workspace is registered while the first build is still running, this is a no-op, and the build's `listWorkspaces()` snapshot may already be out of date. - The `repoKey` lookup isn't awaited, so a trigger that arrives right after registration misses the workspace. Possible fix: on a miss, check the DB workspaces that aren't in the index before returning nothing.
Author
Collaborator

Both races fixed in fbb891e. Every lookup now first resolves the registered workspaces the index has no entry for yet, then matches. That covers registrations during the first build and lookups that arrive before noteWorkspaceAdded has resolved. Workspaces that are already indexed are still not re-verified, which is the documented origin-drift gap, now backed by the repo check from 16001. Test added.

Both races fixed in fbb891e. Every lookup now first resolves the registered workspaces the index has no entry for yet, then matches. That covers registrations during the first build and lookups that arrive before `noteWorkspaceAdded` has resolved. Workspaces that are already indexed are still not re-verified, which is the documented origin-drift gap, now backed by the repo check from 16001. Test added.
lucas marked this conversation as resolved
@ -0,0 +8,4 @@
* neither the body nor the headers are ever logged.
*
* Accepted deliveries become `pending` rows in `forgejo_triggers`, deduped by
* `X-Forgejo-Delivery` so Forgejo's manual redeliver is safe.
Author
Collaborator

Two concerns with deduplicating by X-Forgejo-Delivery:

  • The header isn't covered by the signature, which only covers the body. Anyone who has captured one signed delivery can replay it with a fresh delivery id, and each replay starts a new session that answers the same comment.
  • As far as I know, Forgejo's "Replay" creates a new hook task with a new UUID. If so, a manual redeliver does start a second session, despite what this comment says. Can you confirm?

Deduplicating on the signed content (for example comment.id plus action plus a hash of the body, or comment.updated_at) would cover both cases.

Two concerns with deduplicating by `X-Forgejo-Delivery`: - The header isn't covered by the signature, which only covers the body. Anyone who has captured one signed delivery can replay it with a fresh delivery id, and each replay starts a new session that answers the same comment. - As far as I know, Forgejo's "Replay" creates a new hook task with a new UUID. If so, a manual redeliver *does* start a second session, despite what this comment says. Can you confirm? Deduplicating on the signed content (for example `comment.id` plus `action` plus a hash of the body, or `comment.updated_at`) would cover both cases.
Author
Collaborator

Confirmed: Forgejo's ReplayHookTask copies the payload into a new hook task, and CreateHookTask assigns a fresh UUID (models/webhook/hooktask.go). The old comment was therefore wrong.

Fixed in 69d517e. Rows are now deduplicated on dedupe_key, a SHA-256 over signed body fields: repo, comment.id, action, comment.updated_at and the body. updated_at keeps successive edits that restore an earlier body apart. delivery_id is still stored, for display only. New tests cover a replay with a forged delivery id (duplicate) and a later edit of the same comment (new row). Migration 010 was changed in place because it hasn't been merged yet. Anyone who already ran this branch needs to drop forgejo_triggers and its schema_migrations row.

Confirmed: Forgejo's `ReplayHookTask` copies the payload into a new hook task, and `CreateHookTask` assigns a fresh UUID (`models/webhook/hooktask.go`). The old comment was therefore wrong. Fixed in 69d517e. Rows are now deduplicated on `dedupe_key`, a SHA-256 over signed body fields: repo, `comment.id`, `action`, `comment.updated_at` and the body. `updated_at` keeps successive edits that restore an earlier body apart. `delivery_id` is still stored, for display only. New tests cover a replay with a forged delivery id (duplicate) and a later edit of the same comment (new row). Migration 010 was changed in place because it hasn't been merged yet. Anyone who already ran this branch needs to drop `forgejo_triggers` and its `schema_migrations` row.
lucas marked this conversation as resolved
@ -0,0 +34,4 @@
const cfg = readForgejoConfig();
if (!secret || !cfg) return c.json({ error: "forgejo webhook not configured" }, 503);
const body = new Uint8Array(await c.req.arrayBuffer());
Author
Collaborator

Nit: the whole body is buffered before the signature is checked, and Bun's default body limit is 128 MB. Since this is the one route meant to be reachable from outside, a small explicit limit (comment payloads are a few KB) would be cheap insurance.

Nit: the whole body is buffered before the signature is checked, and Bun's default body limit is 128 MB. Since this is the one route meant to be reachable from outside, a small explicit limit (comment payloads are a few KB) would be cheap insurance.
Author
Collaborator

Added in 69d517e: Hono's bodyLimit at 1 MiB on the route, which answers 413 before the body is buffered for the signature check. Test added.

Added in 69d517e: Hono's `bodyLimit` at 1 MiB on the route, which answers 413 before the body is buffered for the signature check. Test added.
lucas marked this conversation as resolved
@ -193,0 +196,4 @@
1. In Settings → Forgejo → **Webhook**, click **Generate secret** (stored as `forgejo.webhook_secret`; **Regenerate** replaces it and invalidates the old one).
2. In Forgejo's repository, organization or system webhook settings, add a **Forgejo** webhook: target URL = the payload URL shown in the panel (`<deck origin>/api/forgejo/webhook`), method POST, content type `application/json`, secret = the generated secret, trigger on custom events **Issue comments** and **Pull request comments**.
3. Forgejo must be able to reach that URL. `/api/forgejo/webhook` is the one deck route meant for an external caller; exposing it (and exempting it from any front-end auth) is up to your reverse proxy. Its only authentication is the HMAC-SHA256 signature over the body, checked against the secret.
Author
Collaborator

I think this should be stated plainly: the deck's /api has no authentication of its own, and it is only safe because the server binds to loopback by default. The reverse proxy must expose only POST /api/forgejo/webhook. Proxying the whole origin hands the full API (sessions, settings, tokens, the webhook secret) to anyone who can reach it.

I think this should be stated plainly: the deck's `/api` has no authentication of its own, and it is only safe because the server binds to loopback by default. The reverse proxy must expose **only** `POST /api/forgejo/webhook`. Proxying the whole origin hands the full API (sessions, settings, tokens, the webhook secret) to anyone who can reach it.
Author
Collaborator

Agreed, rewritten in fafd5d9. The doc now says plainly that /api has no authentication of its own and is only safe because the server binds to loopback by default. The proxy must expose only POST /api/forgejo/webhook, and proxying the whole origin exposes the full API, including the token and the webhook secret. The changelog entry mentions this too.

Agreed, rewritten in fafd5d9. The doc now says plainly that `/api` has no authentication of its own and is only safe because the server binds to loopback by default. The proxy must expose only `POST /api/forgejo/webhook`, and proxying the whole origin exposes the full API, including the token and the webhook secret. The changelog entry mentions this too.
lucas marked this conversation as resolved
@ -0,0 +88,4 @@
"",
`You were started by a comment on ${isPull ? "pull request" : "issue"} #${number}. Nobody is watching this session: your chat output is never read. The only way to answer is a Forgejo comment.`,
"- Read the thread first (`forgejo_list_issue_comments`, or `forgejo_list_pr_comments_and_reviews` for a pull request).",
`- Reply only with \`forgejo_create_issue_comment\` or \`forgejo_create_pr_comment\` on #${number}.`,
Owner

This is missing the third tool, that's used to reply to review threads.

This is missing the third tool, that's used to reply to review threads.
Author
Collaborator

Added forgejo_create_pr_thread_comment to the conversation policy in d3cb733. It's limited to the triggering PR like the other two comment tools, and the tool itself checks that comment_id is an inline comment of that PR. The orientation now names it for answering inline review comments.

Added `forgejo_create_pr_thread_comment` to the conversation policy in d3cb733. It's limited to the triggering PR like the other two comment tools, and the tool itself checks that `comment_id` is an inline comment of that PR. The orientation now names it for answering inline review comments.
lucas marked this conversation as resolved
A conversation session's comment tools act on the checkout's current
origin, which can drift from the repository the trigger came from.
Creating or resuming a trigger session now fails when the Forgejo tools
would target a different repository (or none), so a comment can never
land in another repo.

The conversation policy also admits forgejo_create_pr_thread_comment,
limited to the triggering PR like the other comment tools, and the
orientation treats issue and PR titles as untrusted input too.

Co-Authored-By: Claude Opus 5.5 via omp-deck
Deliveries are deduplicated on a hash of signed body fields instead of
the unsigned X-Forgejo-Delivery header, so neither Forgejo's redeliver
(fresh delivery id) nor a replayed body starts a second session.
Without an Event-Type header, a pull_request_comment event is an inline
review comment and is now ignored. Webhook bodies over 1 MiB are
rejected before the signature check.

Failure comments on Forgejo name only the kind of failure and the
trigger/session ids; detailed reasons, which can carry local paths or
provider internals, stay in the deck. Trigger sessions are disposed
after their turn instead of staying in memory until restart.

Co-Authored-By: Claude Opus 5.5 via omp-deck
A workspace registered during the first index build, or looked up
before its origin resolved, was missed until restart and the agent
posted "not connected". Lookups now resolve registered workspaces the
index has no entry for yet.

Co-Authored-By: Claude Opus 5.5 via omp-deck
The deck API has no authentication of its own, so a reverse proxy
must expose only POST /api/forgejo/webhook. Also documents replay
deduplication, the repo check, thread replies and the failure comment
format.

Co-Authored-By: Claude Opus 5.5 via omp-deck
lucas merged commit 2c3a04a41d into main 2026-10-15 11:58:16 +02:00
lucas deleted branch feat/forgejo-comment-driven 2026-10-15 11:58:17 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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
lucas/omp-deck!8
No description provided.