Codumentor logo Codumentor

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

ToolApprovalPurpose
gitea_list_prsnone (read)List open/recent PRs for a repo.
gitea_get_prnone (read)Fetch one PR's details (title, body, state, author).
gitea_get_pr_diffnone (read)Get the unified diff (truncated past max_pr_diff_bytes).
gitea_get_pr_filesnone (read)List changed files with patch hunks — the preferred entry point for large PRs.
gitea_get_pr_commitsnone (read)List commits in the PR.
gitea_get_pr_commentsnone (read)Read existing discussion comments.
gitea_post_pr_commentgitea.post_pr_commentPost a general (thread-level) PR comment.
gitea_post_line_commentgitea.post_line_commentPost a line-level review comment on a specific file/line.
gitea_submit_reviewgitea.submit_reviewSubmit 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

| 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: a tasks.declared: entry may name run_as:, and both
shipped gitea-review-* entries carry run_as: gitea-review-bot. That id
becomes the run's job_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:

  1. Add the gitea_pr plugin to plugins:. It is not loaded by default; without it the gitea_* tools do not exist and the run has nothing to review with.
  2. Define pr-reviewer under access.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: unrestricted would hand whoever can open a pull request a full-capability unattended turn, and restricted_fallback grants no repositories, so the run could not read the diff. access.enabled is 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 what run_as holds, so it can only take away.
  3. Create the gitea-review-bot account, sign in as it once, and give it the token. run_as names a real account on this server; add a user secret (Settings → User Secrets) whose name matches this plugin's api_token_secret (default gitea_api_token), holding a Gitea personal access token with repository read + issue/PR write scope. Without it every gitea_* call returns gitea_api_token_unset and the run records a success that posted nothing.
  4. Set auto_post_on_review_comment: true. Until you do, an unattended run holds no gitea_* 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_as is 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 what run_as holds.
  • auto_post_on_review_comment + auto_runnable_write_tools bound which write verbs may happen with nobody at the approval dialog. Off — the default — an auto-approving turn holds no gitea_* write tool and posts nothing. On, it holds the comment tools and never gitea_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 in access/capabilities.py: full allows every gitea_* tool
including the verdict, while chat_only and qa_readonly deny 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_review enforces the second bound run-last, over the
advertised catalog and the tool registry — an un-advertised tool must not
be callable either. It keys on auto_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_run marker 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_pr conversation metadata — the retired handler stamped
those, and a declared run cannot: bind: substitutes into the prompt only
(triggers.md §12.3), never into metadata, and conversation_metadata is a
reserved key no task entry may write.

  1. 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 synchronize re-fire can still be posted.
  2. 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: queue is 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

OptionTypeDefaultDescription
base_urlstringconfig.auth.gitea.base_urlBase URL of the Gitea instance.
api_token_secretstringgitea_api_tokenPer-user secret name holding the personal access token.
default_repostring(none)owner/repo used when a call omits the repository.
max_pr_diff_bytesint400000Truncate diffs larger than this.
request_timeout_sfloat30.0Per-request timeout in seconds.
target_agentsstringallmain \
write_target_agentsstringmainSame values, applied to the write tools only. Subagents keep the read half.
auto_post_on_review_commentboolfalseMaster 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_toolslist[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_idstring(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:

  1. In Gitea: Settings → Applications → Generate New Token (read repository scope for PR reads; add write issue/PR scope to post comments).
  2. In Codumentor: Settings → Extensions → User Secrets → Add, name it gitea_api_token (or whatever api_token_secret is 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