--- # ============================================================================= # 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"