feat(review): ADDITIONAL_CONTEXT_URL — repo-provided static context cached per review

Long agent loops re-send the brief prefix on every step; cheap reusable
knowledge (architecture summary, module map, conventions, glossary) belongs
in a versioned file the maintainers control so the agent doesn't re-derive
it from the source tree on every PR. Two wiring paths, merged (env first):

* env var PRAGENT_ADDITIONAL_CONTEXT_URL — comma-separated, deployment-wide
* .pr-review.json:additional_context_urls — list[str], read from the PR's
  base branch (same trust boundary as the rest of the file)

Implementation:
* _parse_additional_context_env splits/dedupes/trims.
* _resolve_additional_context_urls(config) merges env (first) + config
  (then, skipping env-dupes); caps at 8.
* fetch_additional_context(urls) fetches each URL with urllib (5s timeout,
  http/https only — file://, javascript:, ftp:// rejected defensively),
  caches by URL in a module-level dict for the pod lifetime, truncates
  per-URL to 4k chars + total to 16k chars, best-effort (network errors
  are logged and skipped — never aborts the review).
* Result injected into build_user_prompt under "## Repo-provided context"
  between repo config and prior reviews. In the opencode engine it lands
  in .pragent/brief.md under its own section. The brief explicitly labels
  each block's CONTENT as untrusted (same as PR description) — section
  heading is trustworthy, body isn't.
* parse_repo_config accepts the field, caps at 8 entries, drops
  non-strings and empty strings.

Docs: pilot/README-webhook.md "Repo-provided static context" section —
env var + JSON example + Nexus raw-hosted recipe.

Tests: 14 new (208 total), covering env merging + dedup, scheme rejection,
per-URL cap, total cap, caching by URL, brief injection. All mock urllib
with a context-manager stand-in (no real network).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Marcos
2026-08-20 17:45:51 +00:00
parent 998f793ec2
commit e8ebc54362
4 changed files with 428 additions and 3 deletions
+60 -1
View File
@@ -95,6 +95,63 @@ Without `AI-USAGE` (regression): no usage section, no 🪙 lines — behaviour
identical to before the feature. The usage section is part of the review body,
so it's covered by the existing sha-marker dedupe.
## Repo-provided static context (`ADDITIONAL_CONTEXT_URL`)
Long agent loops resend the brief prefix on every step; the cheap reusable
knowledge — architecture summary, module map, conventions, glossary, past
incident write-ups — lives in a versioned file the maintainers control, so
the agent doesn't have to re-read the source tree to rediscover it on every
PR. Two ways to wire it up:
**Env var** (Deployment-wide, useful for shared house docs):
```bash
PRAGENT_ADDITIONAL_CONTEXT_URL="https://nexus.example/raw/architecture.md,https://nexus.example/raw/glossary.md"
# comma-separated, trimmed, deduped; ≤ 8 URLs total
```
**Per-repo `.pr-review.json`** (read from the PR's base branch — same trust
boundary as the rest of `.pr-review.json`):
```json
{
"additional_context_urls": [
"https://nexus.example/repository/raw-hosted/architecture.md",
"https://nexus.example/repository/raw-hosted/conventions.md"
]
}
```
The two are merged: env first (in declared order), then config entries that
aren't already in env. The first 8 win.
**Behaviour**:
- Fetched **once per review**, cached by URL for the lifetime of the pod.
- **http/https only** — `file://`, `javascript:`, `ftp://`, anything else is
silently dropped.
- 5 s timeout per URL.
- Per-URL truncated to **4 000 chars**, total to **16 000 chars**, then
`…[truncated]` is appended and the next URL is skipped.
- Best-effort: a network error or non-200 is logged to stderr and skipped —
never aborts the review.
- Rendered into the brief under **"Repo-provided context"**, between the
repo config and prior reviews. The brief explicitly labels the *content*
of each block as untrusted author-controlled data (same as the PR
description), so the agent knows to ground findings against it but not
take instructions from it.
**Self-hosted example (Nexus `raw-hosted`)**:
```bash
# Upload a doc to Nexus raw-hosted (anonymous read for in-cluster pods).
curl -u techspark -X PUT \
--data-binary @architecture.md \
https://nexus.example/repository/raw-hosted/architecture.md
# Then reference it from .pr-review.json (above). Cache control is
# browser-style: anonymous read = max-age from response headers.
```
## Webhook fires on any PR update (except `closed`)
The receiver uses a **denylist**, not an allowlist: it reviews on every
@@ -364,7 +421,9 @@ Env on the Deployment: `PRAGENT_ENGINE`, `OPENCODE_MODEL`,
`OPENCODE_EXPERIMENTAL_LSP_TOOL`, `PRAGENT_FACTORY_DIR`, `PRAGENT_OPENCODE_BIN`,
`PRAGENT_WORK_ROOT`, `PRAGENT_REVIEW_TIMEOUT`, `GITEA_API`, `OLLAMA_URL`,
`OLLAMA_MODEL`, `OLLAMA_MAX_TOKENS`, `DIFF_MAX_CHARS`,
`PRAGENT_MAX_CONCURRENT_REVIEWS`, `PRAGENT_MAX_BODY_BYTES` are literals;
`PRAGENT_ADDITIONAL_CONTEXT_URL` (optional, see "Repo-provided static
context" above), `PRAGENT_MAX_CONCURRENT_REVIEWS`, `PRAGENT_MAX_BODY_BYTES`
are literals;
`WEBHOOK_SECRET` + `PRAGENT_BOT_TOKEN` come from the Secret. The image now runs
as uid 10001 — add `securityContext: {runAsNonRoot: true, runAsUser: 10001,
fsGroup: 10001}` to the pod spec so the `/tmp/pragent-work` emptyDir is writable.