Operator rule 2026-10-29: image creation, hosting and lifecycle belong to CI/CD, never to a workstation build/push. Nothing had ever been pushed to the Gitea registry. - .gitea/workflows/images.yaml + scripts/images.sh: PRs build+verify (no credential); main builds, verifies and pushes toolbox, agent-runner (FROM toolbox) and conformance-runner as sha-<commit> + main, reading the digest back from the registry. Only changed images build. - Each image's own build-image.sh does the build and in-image verification, now engine-neutral (DinD: no podman-only calls, probe over stdin not a bind mount). New toolbox build-image.sh. - Digest references carry `# renovate-digest:` markers; renovate.json5 gains a custom.regex manager for them. The worker Job template is deliberately untracked (it would loop). - renovate.yaml pins renovate-version 44.167.0: the action's default (39.264.0) rejects the current config's managerFilePatterns, so every run would have died at CONFIG_VALIDATION. - Push credential: REGISTRY_PUSH_TOKEN secret; the Actions token has no package scope in Gitea 1.24.3 (container.go Authenticate). Retention: infra/gitea/registry-retention.sh. Co-Authored-By: Claude Opus 5.5 <[email protected]>
119 lines
7.3 KiB
YAML
119 lines
7.3 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. CI builds, verifies and — on main — publishes agent-runner
|
|
# (.gitea/workflows/images.yaml), and Renovate replaces this value with the digest the
|
|
# REGISTRY returned for the image's `main` tag, in a pull request that also moves the
|
|
# Deployment's image (same marker, same branch). Nobody pushes this image from a workstation.
|
|
# Until the first publish the dispatcher starts, fails to pull, and says so — which is the
|
|
# visible failure, not a silent one.
|
|
# renovate-digest: image=gitea.home.keithjackson.dev/home-network/agent-runner tag=main
|
|
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"
|