chore: scrub private infrastructure for a public repo, rewrite README
Audited the working tree and all 26 commits of history for credentials: none
found. No API keys, no private keys, no tokens — the live bot token, webhook
secret and admin token appear nowhere in the repo or its history.
What was there was infrastructure disclosure, which is recon material rather
than a leak, but has no business in a public repo:
- Tailnet addresses and cluster-internal hostnames in code, docs and the CI
template. The model endpoint is now supplied at runtime via
PRAGENT_MODEL_BASE_URL and patched into opencode.json by install_config();
the committed config carries a placeholder, guarded by a test.
- A host path (/home/marcos) as the default rtk directory — now unset.
- Real usernames in the onboarding docs — now alice/acme.
- A standing list of one-time setup tokens that were never revoked, named
individually. Removed. Note that removing the list does not revoke the
tokens: they should still be revoked in the Gitea admin UI.
The substitution happens in Python rather than via opencode's {env:VAR} config
templating, because the reviewer subprocess runs with an allow-listed
environment — resolving it before the process starts keeps that allow-list from
having to grow.
README rewritten for a reader who has never seen the project: what it does and
what that output looks like, honest status (pilot works, framework designed but
unbuilt), the security model up front given what this thing is, and the measured
cost numbers including the two effects that make naive estimates wrong.
History still contains the old addresses. They are tailnet-only and not
credentials, so no rewrite.
Tests: 131 -> 137.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B11e8TZZxJyzHW7jj7KWUN
This commit is contained in:
+13
-28
@@ -39,7 +39,7 @@ ai_review.review_pr() (same core the CI-step uses)
|
||||
skills, delegates to security/tests/perf subagents only on big/risky
|
||||
diffs, and emits: {"summary":..., "findings":[{severity,path,line,
|
||||
problem,fix,suggestion,reference}]}
|
||||
(=ollama: legacy single POST to http://100.74.17.70:8789/v1/messages)
|
||||
(=ollama: legacy single POST to http://<model-proxy-host>:8789/v1/messages)
|
||||
6. parse diff hunks → valid (path, new_line) anchors (RIGHT side)
|
||||
7. post review → POST .../pulls/{i}/reviews (event: COMMENT) as pragent-bot
|
||||
- prose summary → review body intro
|
||||
@@ -207,7 +207,7 @@ persists (`GET /admin/hooks` lists 0, no delivery). So we use **user-level
|
||||
webhooks** instead — one webhook per repo-owner, which fires for every repo that
|
||||
user owns. For a small instance with few owners this is nearly as good.
|
||||
|
||||
To onboard a new owner (e.g. `masi`):
|
||||
To onboard a new owner (e.g. `alice`):
|
||||
|
||||
```bash
|
||||
# 1. generate a one-time token for that user (admin CLI, inside the gitea pod)
|
||||
@@ -215,15 +215,15 @@ K="microk8s kubectl"
|
||||
GPOD=$($K -n gitea get pod -l app=gitea --field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[?(@.status.containerStatuses[0].ready==true)].metadata.name}')
|
||||
$K -n gitea exec "$GPOD" -c gitea -- \
|
||||
gitea admin user generate-access-token --username masi \
|
||||
--scopes write:user,read:user --token-name pragent-userhook-masi
|
||||
gitea admin user generate-access-token --username alice \
|
||||
--scopes write:user,read:user --token-name pragent-userhook-alice
|
||||
|
||||
# 2. register the user-level webhook (events: pull_request)
|
||||
# WEBHOOK_SECRET = the shared HMAC secret in the pragent-webhook K8s Secret
|
||||
python3 - "$MASI_TOKEN" <<'PY'
|
||||
python3 - "$OWNER_TOKEN" <<'PY'
|
||||
import sys, json, urllib.request
|
||||
tok = sys.argv[1]
|
||||
GAPI = "http://100.74.17.70:30000/api/v1"
|
||||
GAPI = "http://<gitea-host>:3000/api/v1"
|
||||
WS = open("/dev/stdin") and __import__("os").environ["WEBHOOK_SECRET"] # or paste
|
||||
req = urllib.request.Request(
|
||||
f"{GAPI}/user/hooks",
|
||||
@@ -234,13 +234,12 @@ req = urllib.request.Request(
|
||||
method="POST", headers={"Authorization":f"token {tok}","Content-Type":"application/json"})
|
||||
print(urllib.request.urlopen(req).status, urllib.request.urlopen(req).read()[:80])
|
||||
PY
|
||||
# 3. revoke the one-time token (Gitea admin UI → Users → masi → Access Tokens).
|
||||
# 3. revoke the one-time token (Gitea admin UI → Users → <owner> → Access Tokens).
|
||||
```
|
||||
|
||||
Owners already covered: `gitea_admin` (webhook id=4), `masi` (webhook id=5),
|
||||
`techspark` (webhook id=6 — a user account, not an org; covers `techspark/suaspark-site`,
|
||||
`techspark/spark-ui`, etc.). True **orgs** need org-level webhooks
|
||||
(`POST /orgs/{org}/hooks`, requires a token with `write:organization`).
|
||||
Owners are onboarded one at a time with the recipe above; true **orgs**
|
||||
need org-level webhooks (`POST /orgs/{org}/hooks`, requires a token with
|
||||
`write:organization`).
|
||||
|
||||
## Gitea SSRF allow-list (required, one-time)
|
||||
|
||||
@@ -248,7 +247,7 @@ Gitea refuses to POST webhooks to in-cluster addresses by default:
|
||||
|
||||
```
|
||||
webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting),
|
||||
deny 'pragent-webhook.pragent.svc.cluster.local(10.152.183.170:80)'
|
||||
deny 'pragent-webhook.pragent.svc.cluster.local(<cluster-ip>:80)'
|
||||
```
|
||||
|
||||
Fix: add a scoped `[webhook]` section to Gitea's `app.ini` via the helm chart's
|
||||
@@ -305,7 +304,7 @@ direct `POST .../v1/messages` path as a fallback. opencode wants a
|
||||
**provider-prefixed** model ref, so `review_pr` maps the bare `OLLAMA_MODEL`
|
||||
(`glm-5.2:cloud`) to `headroom/glm-5.2:cloud` (override with `OPENCODE_MODEL`).
|
||||
The `headroom` provider is defined in `opencode.json` with
|
||||
`options.baseURL=http://100.74.17.70:8789/v1` (the headroom Anthropic proxy).
|
||||
`options.baseURL=http://<model-proxy-host>:8789/v1` (the headroom Anthropic proxy).
|
||||
|
||||
### Local one-shot (no posting)
|
||||
|
||||
@@ -341,7 +340,7 @@ microk8s containerd — it is **not** pulled from a registry (`imagePullPolicy:
|
||||
Never`). The webhook secret + bot token are a Secret (`pragent-webhook`). An
|
||||
emptyDir at `/tmp/pragent-work` holds the per-review checkout + the warmed
|
||||
opencode runtime. Verified: a regular pod on kubernets reaches both
|
||||
`100.74.17.70:8789` (headroom/glm) and `gitea-http.gitea.svc.cluster.local:3000`.
|
||||
`<model-proxy-host>:8789` (headroom/glm) and `gitea-http.gitea.svc.cluster.local:3000`.
|
||||
|
||||
Build + deploy after editing the pilot scripts or the factory:
|
||||
|
||||
@@ -395,17 +394,3 @@ webhook service went live.
|
||||
hook delivery-history API (`.../hooks/{id}/tasks`) returns 404, so delivery is
|
||||
observed via the pragent-webhook pod logs (`kubectl -n pragent logs -f deploy/pragent-webhook`).
|
||||
|
||||
## Cleanup of one-time setup tokens (manual)
|
||||
|
||||
Token revocation via API/CLI is broken in Gitea 1.26.1 (`GET /users/{u}/tokens`
|
||||
401, no CLI `delete-access-token`). Revoke these one-time setup tokens in the
|
||||
Gitea admin UI (Site Administration → Users → *user* → Manage access tokens),
|
||||
name prefix `pragent-`:
|
||||
- `gitea_admin`: `pragent-syswh-reg-2026`, `pragent-syswh-list-2026`,
|
||||
`pragent-syswh-test-2026`, `pragent-syswh-retry-2026`,
|
||||
`pragent-userhook-test-2026`, `pragent-payload-look-2026`,
|
||||
`pragent-cleanup-2026`, `pragent-cleanup2-2026`, `pragent-cleanup3-2026`.
|
||||
- `masi`: `pragent-userhook-masi-2026`.
|
||||
- `techspark`: `pragent-userhook-techspark-2026`.
|
||||
|
||||
(Keep `pragent-bot`'s `pragent-ci` token — that's the live reviewer credential.)
|
||||
+2
-2
@@ -31,7 +31,7 @@ Or via API (with an admin/owner token):
|
||||
curl -X PUT -H "Authorization: token $OWNER_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"permission":"write"}' \
|
||||
"http://100.74.17.70:30000/api/v1/repos/OWNER/REPO/collaborators/pragent-bot"
|
||||
"http://<gitea-host>:3000/api/v1/repos/OWNER/REPO/collaborators/pragent-bot"
|
||||
```
|
||||
|
||||
### 2. Add the `PRAGENT_BOT_TOKEN` secret
|
||||
@@ -97,4 +97,4 @@ PY
|
||||
| `OLLAMA_MODEL` | `glm-5.2:cloud` | Model id passed to the headroom proxy. |
|
||||
| `OLLAMA_MAX_TOKENS` | `6000` | Output token cap. |
|
||||
| `DIFF_MAX_CHARS` | `150000` | Diff truncation cap (with a noted truncation marker). |
|
||||
| `OLLAMA_URL` | `http://100.74.17.70:8789` | headroom proxy (tailnet). If the act-runner can't reach the tailnet IP, expose 8789 as an in-cluster Service+Endpoints and set this to the cluster DNS name. |
|
||||
| `OLLAMA_URL` | `http://<model-proxy-host>:8789` | headroom proxy (tailnet). If the act-runner can't reach the tailnet IP, expose 8789 as an in-cluster Service+Endpoints and set this to the cluster DNS name. |
|
||||
+1
-1
@@ -43,7 +43,7 @@ Env (CI run() path):
|
||||
PR head); optional, defaults to the repo default branch
|
||||
PRAGENT_BOT_TOKEN bot access token (repo secret)
|
||||
PRAGENT_SHA head SHA to tag the review
|
||||
OLLAMA_URL headroom proxy URL, e.g. http://100.74.17.70:8789
|
||||
OLLAMA_URL headroom proxy URL, e.g. http://model-proxy.internal:8789
|
||||
OLLAMA_MODEL model id, e.g. glm-5.2:cloud
|
||||
OLLAMA_MAX_TOKENS (optional) output cap, default 8000
|
||||
DIFF_MAX_CHARS (optional) diff truncation cap, default 150000
|
||||
|
||||
@@ -37,7 +37,7 @@ Env:
|
||||
PRAGENT_OPENCODE_BIN path to the opencode CLI (default: shutil.which / the
|
||||
known linuxbrew path).
|
||||
PRAGENT_RTK_DIR dir holding the `rtk` binary, prepended to PATH for the
|
||||
agent's bash tool (default: /home/marcos/.headroom/bin).
|
||||
agent's bash tool (default: unset).
|
||||
PRAGENT_WORK_ROOT parent for temp workdirs (default: /tmp/pragent-work).
|
||||
PRAGENT_KEEP_WORK if set, leave the workdir on disk for debugging.
|
||||
PRAGENT_REVIEW_TIMEOUT seconds to allow opencode to run (default: 480).
|
||||
@@ -59,7 +59,7 @@ import urllib.request
|
||||
# repo root (this file is at <root>/pilot/opencode_review.py).
|
||||
_DEFAULT_FACTORY = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
RTK_DIR = os.environ.get("PRAGENT_RTK_DIR", "/home/marcos/.headroom/bin")
|
||||
RTK_DIR = os.environ.get("PRAGENT_RTK_DIR", "")
|
||||
WORK_ROOT = os.environ.get("PRAGENT_WORK_ROOT", "/tmp/pragent-work")
|
||||
TIMEOUT = int(os.environ.get("PRAGENT_REVIEW_TIMEOUT", "480"))
|
||||
|
||||
@@ -371,14 +371,47 @@ def sanitize_workdir(workdir: str) -> list[str]:
|
||||
return removed
|
||||
|
||||
|
||||
def install_config(src: str, dst: str) -> bool:
|
||||
"""Copy `opencode.json` from src to dst, substituting the model endpoint.
|
||||
|
||||
The committed `opencode.json` carries a neutral placeholder for the model
|
||||
provider's `baseURL`, so the repo can be public without publishing the
|
||||
address of a private network. The real endpoint is supplied at runtime by
|
||||
`PRAGENT_MODEL_BASE_URL` and patched in here.
|
||||
|
||||
This is done in Python rather than with opencode's own `{env:VAR}` config
|
||||
templating because the reviewer subprocess runs with an allow-listed
|
||||
environment (see `_build_env`) — substituting before the process starts
|
||||
keeps that allow-list free of anything opencode needs to resolve config.
|
||||
|
||||
Returns True if a config was installed.
|
||||
"""
|
||||
if not os.path.isfile(src):
|
||||
return False
|
||||
base_url = os.environ.get("PRAGENT_MODEL_BASE_URL", "").strip()
|
||||
if not base_url:
|
||||
shutil.copy2(src, dst)
|
||||
return True
|
||||
try:
|
||||
with open(src, encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
for prov in (cfg.get("provider") or {}).values():
|
||||
if isinstance(prov, dict) and isinstance(prov.get("options"), dict):
|
||||
prov["options"]["baseURL"] = base_url
|
||||
with open(dst, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
except (OSError, ValueError, AttributeError):
|
||||
# A malformed config is opencode's problem to report, not ours to hide.
|
||||
shutil.copy2(src, dst)
|
||||
return True
|
||||
|
||||
|
||||
def drop_factory(workdir: str) -> None:
|
||||
"""Copy the pragent `opencode.json` + `.opencode/` into the workdir so
|
||||
`opencode run --dir <workdir>` discovers them as project config. Overwrites
|
||||
any existing ones (the workdir is a throwaway archive checkout)."""
|
||||
src = _factory_dir()
|
||||
oc_json = os.path.join(src, "opencode.json")
|
||||
if os.path.isfile(oc_json):
|
||||
shutil.copy2(oc_json, os.path.join(workdir, "opencode.json"))
|
||||
install_config(os.path.join(src, "opencode.json"), os.path.join(workdir, "opencode.json"))
|
||||
src_oc = os.path.join(src, ".opencode")
|
||||
dst_oc = os.path.join(workdir, ".opencode")
|
||||
if os.path.isdir(dst_oc):
|
||||
@@ -491,9 +524,9 @@ def _ensure_global_config(home: str) -> None:
|
||||
src = os.path.join(_factory_dir(), "opencode.json")
|
||||
if not os.path.isfile(src):
|
||||
return
|
||||
# Copy if missing or changed (compare mtime/size to avoid pointless writes).
|
||||
# Copy if missing or changed (compare mtime to avoid pointless writes).
|
||||
if not os.path.isfile(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
|
||||
shutil.copy2(src, dst)
|
||||
install_config(src, dst)
|
||||
|
||||
|
||||
# The ONLY host env vars forwarded to opencode. This is an allow-list, not a
|
||||
|
||||
@@ -21,7 +21,7 @@ Env:
|
||||
GITEA_API in-cluster Gitea base URL
|
||||
PRAGENT_BOT_TOKEN pragent-bot access token (non-admin; must be a Write
|
||||
collaborator on each reviewed repo)
|
||||
OLLAMA_URL headroom proxy URL, e.g. http://100.74.17.70:8789
|
||||
OLLAMA_URL headroom proxy URL, e.g. http://model-proxy.internal:8789
|
||||
OLLAMA_MODEL model id, e.g. glm-5.2:cloud
|
||||
OLLAMA_MAX_TOKENS (optional) output cap, default 6000
|
||||
DIFF_MAX_CHARS (optional) diff truncation cap, default 150000
|
||||
@@ -57,7 +57,7 @@ AI_USAGE_LABEL = "AI-USAGE"
|
||||
|
||||
GITEA_API = os.environ.get("GITEA_API", "http://gitea-http.gitea.svc.cluster.local:3000")
|
||||
BOT_TOKEN = os.environ.get("PRAGENT_BOT_TOKEN", "")
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://100.74.17.70:8789")
|
||||
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://model-proxy.internal:8789")
|
||||
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "glm-5.2:cloud")
|
||||
OLLAMA_MAX_TOKENS = int(os.environ.get("OLLAMA_MAX_TOKENS", "8000"))
|
||||
DIFF_MAX_CHARS = int(os.environ.get("DIFF_MAX_CHARS", "150000"))
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
# review_pr defaults to `opencode` for the webhook service.
|
||||
PRAGENT_ENGINE: ollama
|
||||
# On-network model: headroom proxy on kubernets (tailnet IP).
|
||||
OLLAMA_URL: http://100.74.17.70:8789
|
||||
OLLAMA_URL: ${{ vars.PRAGENT_MODEL_URL }}
|
||||
OLLAMA_MODEL: glm-5.2:cloud
|
||||
OLLAMA_MAX_TOKENS: "6000"
|
||||
DIFF_MAX_CHARS: "150000"
|
||||
|
||||
Reference in New Issue
Block a user