The operator ruled the implementation repo's main (50c8719) the latest and accepted state. Sync the spec to it. Accepted changes are reflected: one bare-metal environment, TrueNAS bulk storage, R2 off-cluster backup, CloudNativePG, single-node VPN gateway with Cilium fail-closed, inbound WireGuard, per-app auth sidecars, Renovate, the CI gate as approver, and the pm-agent. New ADR-0017..0025; ADR-0016 corrected and accepted; R-* IDs unchanged, eight amended. Where the implementation has simply not met a requirement, the requirement stands and the shortfall is recorded in docs/16-known-gaps.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
53 lines
3.6 KiB
Markdown
53 lines
3.6 KiB
Markdown
# ADR-0025 — In-cluster AI Program Manager (pm-agent)
|
|
- Status: Accepted
|
|
- Date: 2026-11-08
|
|
- Serves: R-GITOPS-01, R-TEST-03, R-TEST-06, R-SEC-02
|
|
- Supersedes: —
|
|
- Accepted by: operator ruling of 2026-11-08 that the implementation is the accepted state; effective on merge.
|
|
|
|
## Context
|
|
The operator wants a Gitea issue to be able to become a pull request without a workstation session. That
|
|
puts an AI agent inside the cluster, holding a repository write token and acting on text written by
|
|
whoever can reach the repository. It is a new trust boundary: input is untrusted, the actor can author
|
|
code, and it needs egress to an external model API. "May create Jobs" is also dangerous in its own
|
|
right, since Kubernetes places no check on a Job's `serviceAccountName`.
|
|
|
|
## Decision
|
|
Run **pm-agent**: a dispatcher in its own namespace that polls labelled issues on the implementation
|
|
repo and acts as the Program Manager on each thread; replying to an explicit mention on a PR is an
|
|
optional capability, off by default. It starts at most a small, capped number of worker Jobs, each
|
|
of which authors a change, runs the merge gate and opens a PR. The issue thread and the PR hold all of its state and correspondence.
|
|
|
|
- **Contributor, never approver.** It cannot push to `main` and never merges. Its bot account is a
|
|
non-administrator, so it cannot waive review ([ADR-0024](adr-0024-delivery-authority-ci-gate.md)).
|
|
- **Three independent layers between a hostile comment and the cluster.** The deciding model holds no
|
|
tools; the dispatcher accepts only a validated vocabulary and refuses rather than repairs; and a
|
|
fail-closed admission policy admits only the committed Job shape.
|
|
- **Workers hold no cluster credential**; egress is limited to Gitea and the model API over TLS.
|
|
- **Delivery tooling, not a catalog service.** It declares objects a ServiceDefinition cannot express
|
|
and is hand-authored under `platform/` with its own non-gating Flux tier, so it can never stall the
|
|
app tier. It adds no `R-*` requirement.
|
|
- **Bound by the governance every agent is bound by.** It is an implementation agent under
|
|
[AGENTS.md](../AGENTS.md) and R-TEST-06. It does not hold the System Architect role, and it declines
|
|
and escalates work that needs a spec change.
|
|
|
|
## Alternatives considered
|
|
- **Receive Gitea webhooks** — needs a security setting on the git host widened so it can call into
|
|
the cluster, and a missed delivery is lost silently. Rejected; polling reads current state.
|
|
- **One agent that both triages and implements** — puts tools in reach of raw issue text. Rejected.
|
|
- **A database for state** — a second source of truth beside the thread. Rejected.
|
|
- **Federated Keycloak machine identity** — Gitea's API cannot validate a third-party token. Rejected;
|
|
a local account with a scoped token is what Gitea can verify.
|
|
- **RBAC alone as the boundary** — cannot constrain what a created Job runs as. Rejected; admission is
|
|
the boundary and RBAC its necessary half.
|
|
|
|
## Consequences
|
|
- Issues become an intake for implementation work, and every result lands as an ordinary gated PR.
|
|
- Accepted residual risks, documented in the implementation repo's threat model: a worker can read its
|
|
own credentials (bounded by one-repository, non-admin scope and by egress); TLS egress is wider than
|
|
one API host; and no Gitea token scope separates pushing from merging, so "never merges" rests on the
|
|
code making no merge call.
|
|
- An external model provider sees repository and issue content.
|
|
- Its capability can be stopped by configuration without removing the service.
|
|
- The worker cannot measure the live platform; anything needing a cluster stays unverified in its report.
|