Files
home-network-k3s-impl/platform/pm-agent/configmap.yaml
T
jakman85andClaude Opus 5 c5b5b8672a
gate / gate (pull_request) Failing after 1m51s
gate / conformance (pull_request) Has been skipped
feat(pm-agent): an in-cluster AI Program Manager that turns Gitea issues into pull requests
A service that reads this repository's Gitea issues, decides what to do about each
thread as the Program Manager persona, and runs one worker agent per assignment that
authors a change, runs the merge gate and opens a pull request. It reacts to issue
creation and to comments; the assignment and all correspondence travel through the
issue thread and the PR. It authenticates with an Anthropic subscription token. It
never pushes to main and never merges.

Three choices carry the design, with alternatives recorded in docs/plans/:
 - it POLLS rather than receiving webhooks (Gitea's webhook SSRF guard, lost
   deliveries, and no inbound reach needed at all);
 - it keeps NO state: the trigger is "the newest thing in this thread was not
   written by me", so the issue thread is the state — no PVC, no backup obligation;
 - the model holds NO tools: its reply is one JSON object this code validates, so
   nothing in the path executes text.

The boundary is platform/kyverno/admission/pm-agent-job-shape.yaml (Enforce,
failurePolicy: Fail), not the RBAC: Kubernetes places no `use` check on
serviceAccountName, so "may create Jobs" is "may run arbitrary code as the
namespace's most privileged identity". Six rules admit only the committed shape, and
the worker identity is bound to nothing with no token mounted, so there is nothing to
escalate to. 28/28 offline Kyverno assertions.

Nothing has run: the image has never been built or pushed, and both the image digest
and the two credentials are deliberate placeholders. See the handoff entry's
"What I did NOT verify".

Refs CLAUDE.md §1/§3/§6, R-SEC-02, R-GITOPS-02.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-05 22:28:10 -05:00

118 lines
7.2 KiB
YAML

---
# =============================================================================
# The dispatcher's configuration. Everything here is NON-SECRET and reviewable in a diff;
# the two credentials live in pm-agent-credentials.sops.yaml and are referenced by name only
# (R-IDP-04).
#
# WHAT IS *NOT* HERE, on purpose:
# * the PM persona — baked into the image at /etc/pm-agent/pm-persona.md. It is the
# agent's standing instruction set, and a ConfigMap is `kubectl edit`-able by anyone with
# namespace write. Changing how the PM behaves should be a reviewed commit and an image
# rebuild, not a live edit. (PM_PERSONA_FILE below can point elsewhere, which is the
# escape hatch for someone deliberately experimenting.)
# * the worker Job template — likewise baked in, and for a stronger reason: the template IS
# the shape the admission policy and the dispatcher's RBAC jointly bound. See the header
# of infra/agent/worker-job-template.yaml.
#
# NO SHELL-STYLE EXPANSIONS BELOW — AND THAT RULE APPLIES TO THESE COMMENTS TOO.
# This namespace's Flux Kustomization substitutes from cluster-vars, and envsubst runs over
# the WHOLE rendered manifest, comment lines included. So an unescaped dollar-brace token
# anywhere in this file — even in prose explaining the rule — is either replaced with the
# empty string or, if it is not a valid name, fails the ENTIRE Kustomization. This paragraph
# used to contain two such tokens as examples and the gate refused the change for it
# (ci/l0.sh, flux-postbuild-substitution: 1 UNDEFINED + 1 FATAL-PARSE), which is the check
# working exactly as intended. Every value below is a literal, including the Gitea URL — see
# the comment on it. The escaping contract itself: catalog/README.md.
# =============================================================================
apiVersion: v1
kind: ConfigMap
metadata:
name: pm-agent-config
namespace: pm-agent
labels:
app.kubernetes.io/name: pm-agent
app.kubernetes.io/part-of: pm-agent
data:
# --- where the work comes from ---------------------------------------------
# THE IN-CLUSTER SERVICE ADDRESS, NOT THE PUBLIC HOSTNAME, and for the same reason prod's
# own Flux source uses it (clusters/prod/kustomization.yaml): this path depends on neither
# public DNS, nor Cloudflare, nor the shared Gateway, nor the BASE_DOMAIN knob. A change of
# domain, a certificate problem or a Gateway outage cannot sever the agent from the
# repository it works on. It is also why the egress policy can be a pod-identity rule rather than a
# `world` allow, and why pm-worker's baked-in host allowlist can be two entries long.
PM_GITEA_URL: "http://gitea.gitea.svc.cluster.local:3000"
PM_GITEA_OWNER: "Home-Network"
PM_GITEA_REPO: "home-network-k3s-impl"
PM_BASE_BRANCH: "main"
PM_NAMESPACE: "pm-agent"
# --- what runs the work ----------------------------------------------------
# THE WORKER IMAGE, DIGEST-PINNED. This must be the SAME digest the Deployment runs, since
# the dispatcher and its workers share the committed Job template and the env-name
# contract. A tag here is refused at startup (and would be DENIED by admission anyway).
#
# PLACEHOLDER UNTIL THE IMAGE IS PUBLISHED. The all-zero digest below is deliberately
# impossible: `infra/agent/build-image.sh` builds and verifies the image, the OPERATOR
# pushes it, and the authoritative digest from `--digestfile` replaces this value in a
# reviewed pull request (the same two-step the conformance-runner image follows, and for
# the same reason: a registry credential is not an agent's to hold). Until then the
# dispatcher starts, fails to pull, and says so — which is the visible failure, not a
# silent one.
PM_WORKER_IMAGE: "gitea.home.keithjackson.dev/home-network/agent-runner@sha256:0000000000000000000000000000000000000000000000000000000000000000"
# --- the model -------------------------------------------------------------
# The PM only reads a thread and answers with one JSON object, so it holds no tools and
# takes one turn. The worker does real repository work against a gate. Both run on the
# most capable general model on purpose: the failure mode that matters here is a plausible
# change that is subtly wrong, which is exactly what a cheaper tier buys more of.
PM_MODEL: "claude-opus-5"
PM_WORKER_MODEL: "claude-opus-5"
# --- pace and blast radius -------------------------------------------------
# These five numbers are the throttle. They exist because the first minute after the
# credentials are filled in is the dangerous one: every open issue is new to the agent at
# once.
#
# PM_POLL_INTERVAL_SECONDS how long an issue may wait before the PM looks at it.
# PM_MAX_ACTIVE_WORKERS concurrent worker Jobs. RAM is the binding constraint on this
# 3-node cluster and each worker's limit is 2Gi, so 2 is 4Gi
# worst case. It is also a subscription-rate-limit throttle:
# several Claude Code sessions on one subscription seat contend
# for the same account limits.
# PM_MAX_NEW_WORKERS_PER_CYCLE at most one NEW assignment per poll, so a backlog is
# drained one issue at a time rather than in one stampede.
# PM_ISSUE_COOLDOWN_SECONDS how often one issue may cost a decision. The dispatcher stamps
# this BEFORE it asks the model, so a decision that then fails
# cannot be retried on every poll.
# PM_WORKER_MAX_BUDGET_USD per-assignment spend ceiling the CLI enforces itself. It is
# Claude Code's own client-side estimate rather than a bill, so
# treat it as a runaway-loop stop, not as accounting.
PM_POLL_INTERVAL_SECONDS: "60"
PM_MAX_ACTIVE_WORKERS: "2"
PM_MAX_NEW_WORKERS_PER_CYCLE: "1"
PM_ISSUE_COOLDOWN_SECONDS: "900"
PM_WORKER_MAX_TURNS: "80"
PM_WORKER_MAX_BUDGET_USD: "10"
# --- scope -----------------------------------------------------------------
# EMPTY MEANS EVERY OPEN ISSUE, which is the behaviour this service was asked for: it
# reacts to issue creation and to comments, on all of them.
#
# RECOMMENDED FOR THE FIRST WEEKS, and a one-word change: set this to `agent`, and the
# dispatcher considers only issues a human has labelled `agent`. That bounds the agent to
# work somebody deliberately handed it while trust is being established, with no code
# change and no redeploy of anything but this ConfigMap. The operator's call; the default
# here honours the request as made.
PM_REQUIRE_LABEL: ""
# Dry run: the PM still reads threads, still asks the model and still posts its decision as
# an issue comment — so the operator can read exactly what it WOULD have done — but creates
# no worker Job and so changes no code. The honest way to watch this service for a day
# before letting it write anything.
PM_DRY_RUN: "false"
# If the gate refuses a worker's change, the branch is NOT pushed by default and the issue
# gets the gate log instead. Set to "true" to push anyway and open a red pull request —
# useful when a human wants to see the work, noisy as a standing setting.
PM_PUSH_ON_RED_GATE: "false"