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]>
platform/kyverno — L0 policy-as-code gate & admission enforcer (T-0.5)
Kyverno is the platform's L0 static/policy gate (in CI, no cluster) and its
in-cluster admission enforcer (ADR-0011, docs/14, docs/07). The policies here
encode spec invariants as code: they run in CI as a merge gate today (Phase 0, no
cluster) and the same policies are wired to run in-cluster as an admission controller
later (Phase 2, admission/).
Enforcement complements, never replaces, behavior tests. A policy admitting a manifest is not proof the runtime behaves (R-TEST-01). The L2/L3 Chainsaw behavior + fault-injection tests remain mandatory. See
admission/README.md.
Layout
platform/kyverno/
├── README.md # this file: policy→requirement map + CI entrypoint
├── labels.md # label/annotation convention (PROPOSED contract — escalated)
├── policies/ # ClusterPolicies (authored once; CI + admission)
│ ├── require-default-deny-baseline.yaml # R-NET-01 (generate baseline)
│ ├── validate-default-deny-shape.yaml # R-NET-01 (reject malformed baseline)
│ ├── disallow-plaintext-secrets.yaml # R-SEC-01 (CI-only: sops block required)
│ ├── restrict-host-namespaces.yaml # R-SEC-02 (+R-NET-01 bypass guard)
│ ├── require-egress-class-label.yaml # R-NET-05
│ ├── require-auth-mode-label.yaml # R-IDP-02
│ └── label-cdi-worker-pods.yaml # R-NET-05+R-IDP-02 (mutate: CDI's own worker pods)
├── servicedefinition/ # ServiceDefinition shape validation (kyverno-json)
│ ├── validate-servicedefinition.yaml # R-GITOPS-02
│ ├── payloads/ (valid-* / invalid-*) # pass & fail fixtures
│ └── README.md
├── tests/ # `kyverno test` suite — runnable L0 tests NOW, no cluster
│ ├── kyverno-test.yaml # policies × fixtures × expected results
│ ├── resources/ (good-* / bad-* fixtures + generated golden)
│ └── controller-worker-pods/ # the CDI-worker mutation, proven BOTH ways
│ ├── kyverno-test.yaml # 2 Tests: cdi-sa requester vs ordinary requester
│ ├── userinfo-cdi-sa.yaml / userinfo-ordinary.yaml
│ └── resources/ + patched/ # real + FORGED importer pods; expected patch
├── admission/ # in-cluster enforcing bundle (scaffolding, T-2.7)
│ ├── namespace.yaml · helmrelease.yaml · kustomization.yaml
│ ├── secret-provenance.yaml # R-SEC-01 admission variant (provenance)
│ └── README.md
└── scripts/run-l0.sh # the single-command CI entrypoint for the gate
Policy → requirement map
| Policy / artifact | Rule(s) | Requirement | Spec source | Behaves |
|---|---|---|---|---|
require-default-deny-baseline |
generate-default-deny |
R-NET-01 | docs/03 default-deny baseline | Every app namespace auto-gets a deny-all (ingress+egress, DNS-only) NetworkPolicy |
validate-default-deny-shape |
baseline-must-deny-both-directions |
R-NET-01 | docs/03 | A baseline that leaves a direction open is rejected |
disallow-plaintext-secrets |
secrets-must-be-sops-encrypted |
R-SEC-01 | docs/07, docs/05, ADR-0007 | Committed Secrets must be SOPS-encrypted (sops block); plaintext rejected. CI-only |
restrict-host-namespaces |
disallow-host-namespaces, disallow-privileged-containers |
R-SEC-02 (+R-NET-01) | docs/07 least reach | App pods can't use hostNetwork/PID/IPC or privileged (would bypass default-deny) |
require-egress-class-label |
require-egress-class |
R-NET-05 | docs/03, docs/13 | App pods must declare egress-class ∈ {internet,vpn} |
require-auth-mode-label |
require-auth-mode |
R-IDP-02 | docs/13, docs/15 | App pods must declare auth-mode ∈ {oidc,forward-auth,native,none} |
label-cdi-worker-pods (mutate) |
add-worker-pod-labels |
R-NET-05, R-IDP-02 | docs/03, docs/13 | CDI's controller-generated worker pods (importer-*/uploader-*/cloner-*, created by SA cdi/cdi-sa) get egress-class: internet + auth-mode: none added, so they satisfy the two policies above instead of being exempted from them. Matched on the generating ServiceAccount, never a pod-name prefix |
validate-servicedefinition (kyverno-json) |
shape / dependencies / storage | R-GITOPS-02 | docs/13 (+03,15) | ServiceDefinition is schema/invariant valid before render; public⇒not none auth |
admission/secret-provenance |
secrets-must-be-flux-managed |
R-SEC-01 | docs/07 | Admission-only variant: app-ns Secrets must come from Flux/SOPS (see below) |
Coverage of the T-0.5 requirement set: R-TEST-01 (behavior-tagged, reproducible
tests — this whole suite), R-NET-01, R-SEC-01, R-SEC-02, R-IDP-02,
R-NET-05, R-GITOPS-02. Every policy carries its R-* IDs in the
homelab.keithjackson.dev/requirements annotation so the coverage report can pick
them up automatically.
CI entrypoint (the L0 gate)
CI (WS-GITOPS .gitea/workflows, then Gitea Actions) runs the single command:
platform/kyverno/scripts/run-l0.sh
which performs, with pinned tool versions (KYVERNO_VERSION, KYVERNO_JSON_VERSION):
# 1) ClusterPolicy tests — offline, no cluster
kyverno test platform/kyverno/tests
# 2) ServiceDefinition validation — offline, no cluster
kyverno-json scan --policy platform/kyverno/servicedefinition/validate-servicedefinition.yaml \
--payload <each catalog/ ServiceDefinition and the valid-* fixtures> # must pass
# plus each invalid-* fixture must be REJECTED (script asserts non-zero exit)
kyverno test emits pass/fail per policy×rule×resource and supports JUnit output for
the coverage/merge gate:
kyverno test platform/kyverno/tests --output junit > results/kyverno-l0.junit.xml
Exit non-zero fails the merge gate (docs/14 gating rules). Deterministic, hermetic,
self-contained — no cluster, no external state (reproducibility contract, R-TEST-02).
Notes / handoffs
- Label keys are a proposed contract (
labels.md) shared with WS-NET / WS-SEC / WS-GITOPS. Values are spec-fixed; keys need owner sign-off (coordination-protocol §2). - Controller-generated pods are a cold-start hazard. A policy that matches any Pod in an
app namespace also matches pods that no manifest in git authors. Prod hit this on 2026-09-06:
require-auth-mode-label+require-egress-class-labeldenied CDI's ownimporter-prime-*pod, soDataVolume unifi/unifi-rootdiskstuck inImportScheduledand the UniFi VM had no disk. Fix:policies/label-cdi-worker-pods.yamlmutates those pods to carry truthful labels (matched on the generating ServiceAccountcdi/cdi-sa), rather than excluding them.virt-launcher-*pods need nothing — they inherit the VM pod-template labels. KubeVirt hotplug (hp-volume-*) and export (virt-export-*) pods do not inherit them and are unused here; extend the same policy if either is adopted. PolicyExceptionis NOT usable in this install: the Kyverno admission/background/reports controllers all run--enablePolicyException=false(chart default, verified on prod 2026-09-06). The CRD exists (from the pre-applied CRD bundle) so aPolicyExceptionwould APPLY cleanly and be silently ignored — a policy hole that looks like a fix and isn't.- Secrets CI vs admission divergence is deliberate and documented in
admission/README.mdandpolicies/disallow-plaintext-secrets.yaml. - The ServiceDefinition JSON Schema (WS-GITOPS T-0.3) is the complementary structural
contract; this Kyverno policy encodes the invariants from
docs/13. - Tool availability: neither
kyvernonorkyverno-jsonis installed on the WS-TEST workstation, so this suite was authored but not executed here; the CI agent (T-0.2/T-0.4 harness) runsscripts/run-l0.shas the gate.