Gitea PR Plugin
Gives the agent per-user Gitea pull-request tools — read PR details, diffs, and comments in chat, and (behind approval) post review comments and submit reviews on Gitea. Like Redmine, it authenticates as the calling user: each tool reads the user's personal Gitea access token from User Secrets at call time. There is no shared instance token.
Load user_secrets alongside this plugin.
Tools
| Tool | Approval | Purpose |
|---|---|---|
gitea_list_prs | none (read) | List open/recent PRs for a repo. |
gitea_get_pr | none (read) | Fetch one PR's details (title, body, state, author). |
gitea_get_pr_diff | none (read) | Get the unified diff (truncated past max_pr_diff_bytes). |
gitea_get_pr_files | none (read) | List changed files with patch hunks — the preferred entry point for large PRs. |
gitea_get_pr_commits | none (read) | List commits in the PR. |
gitea_get_pr_comments | none (read) | Read existing discussion comments. |
gitea_post_pr_comment | gitea.post_pr_comment | Post a general (thread-level) PR comment. |
gitea_post_line_comment | gitea.post_line_comment | Post a line-level review comment on a specific file/line. |
gitea_submit_review | gitea.submit_review | Submit an overall review verdict (comment / approve / request changes). |
Write tools ask for approval before a comment or verdict lands on Gitea.
Who gets which half
Two gates, not one. target_agents (default all) decides whether the family is offered at all; write_target_agents (default main) then decides who gets the three write tools.
By default a subagent can fetch a PR and its diff but cannot comment or submit a verdict. Set write_target_agents: all if subagents should also write.
Ask the assistant to ground claims in actual diff lines (gitea_get_pr_diff / gitea_get_pr_files) and to cross-reference the ingested codebase.
Two review triggers
- Chat-initiated (always on). Type "review PR #42" in a normal turn; write tools hit the interactive approval dialog. Nothing extra to configure.
- Headless (event-triggered) — two declared tasks, shipped disabled. This plugin used to own
POST /ops/webhook/gitea-prand decide in Python which deliveries started a review. That endpoint is deleted: every inbound webhook now arrives onPOST /ops/webhook/{source}(see Inbound Webhooks), where thegiteasource emitsscm.pull_requestfor every PR delivery andscm.review_requestedfor a/reviewcomment or a review-request action. Which of those starts a review is now two entries intasks.declared:— shipped indefault_enterprise.yamland readable, retunable and scopeable by an operator:
| Task | Fires on | when: |
|---|---|---|
| gitea-review-on-pr | scm.pull_request | action is opened, synchronize or reopened |
| gitea-review-on-request | scm.review_requested | (none — both a /review comment and a review-request action count) |
Both use mode: per_event with on_overlap: queue, so a push landing mid-review queues behind it instead of being dropped, and both are shipped enabled: false — the same default auto_post_on_review_comment: false has always had. Turning the headless path on means: load this plugin, define the pr-reviewer access profile the entries name (a webhook-sourced task without one is a config error, not a default), create the gitea-review-bot account the entries' run_as: names and give it a gitea_api_token secret, set enabled: true on the entries, and set auto_post_on_review_comment: true — that last one is what lets the unattended run hold a write tool at all, so leaving it off is a working kill-switch even with the entries enabled. The prerequisites are spelled out below. Chat-initiated review is unaffected by all of it.
**Four things an operator must set up, and what bounds the run once they
have.** The
identity gap is closed: atasks.declared:entry may namerun_as:, and both
shippedgitea-review-*entries carryrun_as: gitea-review-bot. That id
becomes the run'sjob_args["user_id"]— the identity per-user secrets are
keyed on — so a fired review can hold a Gitea token. Before turning the
entries on:
- Add the
gitea_prplugin toplugins:. It is not loaded by default; without it thegitea_*tools do not exist and the run has nothing to review with.- Define
pr-reviewerunderaccess.profiles:. It is not a built-in, and a name this config does not define makes the run refuse at fire time — it does not fall back to something wider. Neither built-in fits:unrestrictedwould hand whoever can open a pull request a full-capability unattended turn, andrestricted_fallbackgrants no repositories, so the run could not read the diff.access.enabledis not required — it governs per-user tiers, while a task's declared profile narrows its unattended runs either way — and the profile applies as an intersection with whatrun_asholds, so it can only take away.- Create the
gitea-review-botaccount, sign in as it once, and give it the token.run_asnames a real account on this server; add a user secret (Settings → User Secrets) whose name matches this plugin'sapi_token_secret(defaultgitea_api_token), holding a Gitea personal access token with repository read + issue/PR write scope. Without it everygitea_*call returnsgitea_api_token_unsetand the run records a success that posted nothing.- Set
auto_post_on_review_comment: true. Until you do, an unattended run holds nogitea_*write tool at all — it reads the pull request and says nothing. It is deliberately a second switch from the entries'enabled:, and it is the one that survives somebody enabling the entries without reading this far.
run_asis operator-written config and nothing else: it is never taken from
the event, so whoever opens the pull request still cannot choose the identity
the turn runs as.What bounds the run, and why it takes two mechanisms. A declared review
is unattended, so its tool prompts auto-approve. Two separate bounds apply to
it and neither replaces the other:
access_profile(required on a webhook-sourced task) bounds which repositories and which tool families the turn may touch, as an intersection with whatrun_asholds.auto_post_on_review_comment+auto_runnable_write_toolsbound which write verbs may happen with nobody at the approval dialog. Off — the default — an auto-approving turn holds nogitea_*write tool and posts nothing. On, it holds the comment tools and nevergitea_submit_review: an unattended reviewer comments, but does not approve or request changes on the pull request whose own title and description shaped its prompt.The profile cannot say the second thing. Its
capabilities:names one of the
three policies inaccess/capabilities.py:fullallows everygitea_*tool
including the verdict, whilechat_onlyandqa_readonlydeny the whole
gitea_prefix, so the review could post nothing at all. There is no profile
that means "comment but do not approve".
_on_prompt_assemble_reviewenforces the second bound run-last, over the
advertised catalog and the tool registry — an un-advertised tool must not
be callable either. It keys onauto_run, the conversation flag the
permission provider itself reads to auto-allow a prompt, so there is one
spelling of "this turn writes unattended". It used to key on a private
gitea_auto_runmarker written only by the deleted/ops/webhook/gitea-pr
endpoint, which meant a declared review turn carried the whole write toolset
auto-approved; that is fixed.Two things a declared review does not get, both because it carries no
gitea_repo/gitea_prconversation metadata — the retired handler stamped
those, and a declared run cannot:bind:substitutes into the prompt only
(triggers.md§12.3), never into metadata, andconversation_metadatais a
reserved key no task entry may write.
- The already-posted skip-note. It is rendered from that binding, so a declared review turn is not told which findings are already on the PR. Both shipped entries therefore say it in the prompt instead: read the existing comments first and repeat none of them. The posting tools still consult the ledger, but that dedupes on an exact file + line + body fingerprint, so a reworded repeat of the same finding on a
synchronizere-fire can still be posted.- One review conversation per pull request.
pr_conversations— the PR↔conversation binding, and its "a review is already running for this PR" check — is written only by the retired handler. Each fire gets a fresh conversation, so a re-review does not see the previous one's transcript. The declared tasks'on_overlap: queueis what keeps two reviews of the same PR from running at once; it queues per task, which is coarser than per PR.
A posted-comment ledger tracks findings already posted on a PR so a re-review (for example after a later push) does not repeat comments.
Configuration
plugins:
- module: codumentor.plugins.gitea_pr
class: GiteaPRPlugin
args:
enabled: true
base_url: "https://gitea.example.com" # optional; falls back to config.auth.gitea.base_url
api_token_secret: gitea_api_token # per-user secret name (default)
default_repo: "owner/repo" # optional; lets "PR #42" resolve
target_agents: all # main | subagent | all — the whole family
write_target_agents: main # same values, write tools only
# Headless (event-triggered) review — off by default
auto_post_on_review_comment: false
# review_owner_user_id: dead — the declared tasks use `run_as:` instead
# auto_runnable_write_tools defaults to the two comment tools
base_url is required only when enabled: true and neither args.base_url nor config.auth.gitea.base_url is set.
Parameters
| Option | Type | Default | Description |
|---|---|---|---|
base_url | string | config.auth.gitea.base_url | Base URL of the Gitea instance. |
api_token_secret | string | gitea_api_token | Per-user secret name holding the personal access token. |
default_repo | string | (none) | owner/repo used when a call omits the repository. |
max_pr_diff_bytes | int | 400000 | Truncate diffs larger than this. |
request_timeout_s | float | 30.0 | Per-request timeout in seconds. |
target_agents | string | all | main \ |
write_target_agents | string | main | Same values, applied to the write tools only. Subagents keep the read half. |
auto_post_on_review_comment | bool | false | Master switch over what an unattended turn (one carrying auto_run) may write. Off, it holds no gitea_* write tool and posts nothing; on, it holds auto_runnable_write_tools. Interactive review is unaffected either way. |
auto_runnable_write_tools | list[string] | [gitea_post_line_comment, gitea_post_pr_comment] | The only write tools an unattended run may auto-run — comment tools, never a verdict. Stripped from the prompt catalog and the tool registry. |
review_owner_user_id | string | (none) | Dead setting, kept only so an existing config still loads. Read by nothing but GiteaWebhookHandler, whose endpoint is deleted and which nothing constructs. The declared tasks name their run identity with run_as: instead — see the note above. |
The headless path also requires the shared webhooks: block (the one HMAC secret store every inbound webhook uses):
webhooks:
enabled: true
secrets:
- "${CODUMENTOR_WEBHOOK_SECRET}"
Point Gitea at POST {api.external_url}/ops/webhook/gitea with that secret (the plugin's own /ops/webhook/gitea-pr endpoint is gone). Gitea deliveries are authenticated and normalized there whatever this plugin is configured to do, so the wiring is worth doing now: scm.pull_request, scm.review_requested and scm.comment are on the event ledger and available to your own tasks.declared: entries from the moment the hook exists.
Per-user setup
Each user adds their personal Gitea access token as a secret:
- In Gitea: Settings → Applications → Generate New Token (read repository scope for PR reads; add write issue/PR scope to post comments).
- In Codumentor: Settings → Extensions → User Secrets → Add, name it
gitea_api_token(or whateverapi_token_secretis set to), paste the token.
The User Secrets pane shows "Gitea API token — needed by Gitea PR" with set/unset state. When a tool runs without the secret set, it returns a clear, non-retryable message telling the user to add it — no network call is made.
Notes
- All tools act as the calling user via their personal token — there is no shared instance-level Gitea credential.
gitea_get_pr_filesis the preferred entry point for large PRs;gitea_get_pr_difftruncates atmax_pr_diff_bytes.- A headless review runs as the account the declared task's
run_as:names (gitea-review-botas shipped) and posts with that account'sgitea_api_tokenuser secret.review_owner_user_idis dead — see the note under Two review triggers. - A headless review can post comments and nothing else.
gitea_submit_review— the approve / request-changes verdict — is stripped from an auto-approving turn, so an unattended reviewer never signs off on a pull request. Off the master switch it holds no write tool at all.