Files
home-network-k3s-impl/platform/pm-agent/deployment.yaml
T
renovate-bot 5f03997a32
gate / gate (pull_request) Failing after 13m29s
gate / conformance (pull_request) Has been cancelled
chore(deps): Update gitea.home.keithjackson.dev/home-network/agent-runner:main Docker digest to c4eba5b
2026-10-30 08:20:42 +00:00

136 lines
6.6 KiB
YAML

---
# =============================================================================
# The Program Manager dispatcher. One long-running process that polls Gitea issues, asks the
# model what to do about each thread under the PM persona, posts the answer on the issue, and
# creates one worker Job per assignment. Program: infra/agent/pm-dispatch.py.
#
# ONE REPLICA, Recreate STRATEGY — AND THAT IS A CORRECTNESS CONSTRAINT, NOT A RESOURCE ONE.
# Two dispatchers polling the same repository would both see the same untouched thread in the
# same window and both act on it: two decisions, two comments, two worker Jobs on the same
# issue, two branches. The service holds no lock and needs none at one replica — the issue
# thread itself is the state (see pm-dispatch.py's header) and a single reader of it is
# consistent by construction. `Recreate` is what extends that to a rollout: the default
# RollingUpdate would briefly run the new pod alongside the old one, which is the two-reader
# case again, for a minute, every time the image changes.
#
# NO PVC, NO VOLUME CLAIM, NOTHING TO BACK UP. The dispatcher keeps no durable state: its
# trigger is "the newest thing in this thread was not written by me", answered from Gitea on
# every cycle. So a restart loses nothing, there is no recovery path to get wrong, and this
# workload creates no R-STATE-03 obligation.
# =============================================================================
apiVersion: apps/v1
kind: Deployment
metadata:
name: pm-agent
namespace: pm-agent
labels:
app.kubernetes.io/name: pm-agent
app.kubernetes.io/part-of: pm-agent
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: pm-agent
template:
metadata:
labels:
app.kubernetes.io/name: pm-agent
app.kubernetes.io/part-of: pm-agent
annotations:
# Roll the pod when its configuration changes. Without this, a ConfigMap edit that
# Flux applies leaves the running process on the OLD values indefinitely — the
# process reads its config once at startup — so the change would appear to have
# landed while changing nothing. (Same class of defect as the inert merged
# cilium-config: decisions/2026-10-05-cilium-roll-out-on-config-change.md.)
#
# It is a hand-maintained marker rather than a computed hash because Flux's
# Kustomization here does not run a configmap generator; bump it in the same commit
# as a change to configmap.yaml.
pm-agent.homelab.keithjackson.dev/config-generation: "1"
spec:
serviceAccountName: pm-agent
# The dispatcher DOES need its token: `kubectl create job` is its one privileged act,
# and platform/pm-agent/rbac.yaml is what bounds it to that. Contrast the worker, whose
# Job template sets this false because it needs no API access at all.
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: dispatch
# THE SAME IMAGE THE WORKERS RUN, at the same digest. They share the committed Job
# template and the env-name contract baked into it, so a mismatched pair would
# disagree about the shape the admission policy enforces. Replaced together, in ONE
# Renovate pull request: both lines carry the same marker, so Renovate treats them as
# one dependency and bumps them in the same branch. The digest comes from CI's publish
# of agent-runner's `main` tag (.gitea/workflows/images.yaml) — the all-zero
# placeholder below is impossible on purpose (see configmap.yaml PM_WORKER_IMAGE).
# renovate-digest: image=gitea.home.keithjackson.dev/home-network/agent-runner tag=main
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42
imagePullPolicy: IfNotPresent
command:
- /usr/local/bin/pm-dispatch
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
capabilities:
drop:
- ALL
envFrom:
# Every non-secret knob, so adding one is a ConfigMap edit plus a bump of the
# config-generation annotation above — not an edit in two places.
- configMapRef:
name: pm-agent-config
env:
# The two credentials, by NAME only (R-IDP-04). Never a literal, never `envFrom`
# over the whole Secret: the dispatcher needs exactly these two keys, and naming
# them is what makes the Secret's other contents (if it ever grows any) not
# reach this process.
- name: GITEA_TOKEN
valueFrom:
secretKeyRef:
name: pm-agent-credentials
key: gitea-token
- name: CLAUDE_CODE_OAUTH_TOKEN
valueFrom:
secretKeyRef:
name: pm-agent-credentials
key: claude-code-oauth-token
resources:
# Modest: the dispatcher is idle between polls, and its one expensive act (a
# single-turn model call with no tools) is bounded by PM_ISSUE_COOLDOWN_SECONDS.
# The real resource consumers are the worker Jobs, capped separately.
requests:
cpu: 50m
memory: 192Mi
limits:
cpu: 500m
memory: 512Mi
# No probes, deliberately. There is no port to probe and no health endpoint, and an
# exec probe would be a second execution path into a pod that runs model-authored
# decisions. The process is a loop that logs every cycle: liveness is answered by
# `kubectl logs`, and a crash is answered by the Deployment's own restart. A probe
# that cannot distinguish "polling normally" from "wedged" would only add a way to
# restart a healthy pod.
volumeMounts:
# The root filesystem is read-only, so HOME, TMPDIR, the Claude Code config
# directory and every cache live here (see the image's ENV block).
- name: workspace
mountPath: /workspace
- name: tmp
mountPath: /tmp
volumes:
- name: workspace
emptyDir:
sizeLimit: 512Mi
- name: tmp
emptyDir:
sizeLimit: 128Mi