The kubelet's pull credential for agent-runner, plus what is left in git to take the PM agent live. Rebased 2026-10-30 onto main after #81 (CI image pipeline), #82 and #83.
Commit 1 (original #61, rebased): the pull credential and admission rule 7
pm-agent-registry: a kubernetes.io/dockerconfigjson Secret (SOPS, placeholder), referenced by imagePullSecrets on the dispatcher Deployment and the committed worker Job template. It holds the same Gitea token as pm-agent-credentials, in a second format, because the kubelet reads it before any container starts.
Admission (pm-agent-job-shape) rule 1 now requires that entry, and the new rule 7 forbids any other entry or a second one.
PM_SELFTEST asserts the template names exactly pm-agent-registry. It runs in agent-runner's Containerfile verify stage, so CI checks it on every image build.
Conflicts with #81 were resolved keeping both sides: "pull credential wired" and "CI publishes, Renovate bumps". No workstation build or push text came back.
Commit 2: going live
First published digest pinned.deployment.yaml and PM_WORKER_IMAGE now name agent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42. That is images run 340 (sha-523cd9118f95, also main), read from the registry manifest API. The # renovate-digest: markers are unchanged, so Renovate still owns every later bump. The value matches admission's agent-runner@sha256:* pattern.
The first run is dry and needs a label.PM_DRY_RUN=true, PM_REQUIRE_LABEL=agent, config-generation: "2". Dry run is required, not just cautious. The run-340 image's baked worker template predates imagePullSecrets, so admission would deny its worker Jobs. This merge touches infra/agent/**, so images.yamlre-publishes agent-runner. Workers become admissible once Renovate's bump to that new digest merges. Only then flip PM_DRY_RUN.
Token scopes fixed. Gitea 1.24.3 scopes the whole /api/v1/user group, and the dispatcher's first call is GET /user. That call needs read:user, which the old docs did not list. The documented set is now read:user, write:repository, write:issue, read:package.
Org-package permission is documented.determineAccessMode (v1.24.3) gives package read through an org team's Packages unit, or through org visibility. The org is public today. The .example now has a pm-agent-pull team with no repos and Packages = Read, so access does not depend on visibility.
sops is toolbox-only; both .examples say so. They give the in-place ./infra/devcontainer/dev.sh run sops <file> edit, which writes no plaintext file.
The runbook's Secret-key check emitted values before the pipe trimmed them. It is now a go-template that prints key names only.
Handoff: decisions/2026-10-30-pm-agent-live.md. The 2026-10-06 entry has a dated addendum that corrects its stale and wrong claims.
Verified
kyverno test platform/kyverno/tests/pm-agent in the pinned toolbox: 33 passed, 0 failed.
PM_SELFTEST=1 pm-dispatch (worktree template and persona) in the toolbox: selftest OK, rc 0.
Local ./ci/gate.sh in the pinned toolbox, from a §0 worktree at 7f0cbc7: rc 3 (UNDETERMINED, not green). Only gitea-settings did not run (no SETTINGS_API_TOKEN locally). spec-immutability passed. The Kyverno L0 suite: 87 passed, 0 failed. decisions-log, lint, l0, render-golden and l1 were all green. Locally, approval-policy reports N/A because the run is not a PR event.
All three prod nodes, checked read-only: /etc/hosts pins gitea.home.keithjackson.dev to 192.168.2.20. curl /v2/ from each node returns 401 with ssl_verify_result=0.
The agent uses http://gitea.gitea.svc.cluster.local:3000 for the API and git. It never goes through the Gateway, so #83's issue does not apply. Live gitea-allow-pm-agent-ingress is VALID True.
Approval
This touches sensitive paths (platform/kyverno/**, **/*.sops.yaml), so approval-policy will be red until the operator posts /approve-override <reason> as a PR review (type Comment). The agent has not posted one and must not.
The kubelet's pull credential for `agent-runner`, plus what is left in git to take the PM agent live. Rebased 2026-10-30 onto `main` after #81 (CI image pipeline), #82 and #83.
## Commit 1 (original #61, rebased): the pull credential and admission rule 7
- `pm-agent-registry`: a `kubernetes.io/dockerconfigjson` Secret (SOPS, placeholder), referenced by `imagePullSecrets` on the dispatcher Deployment **and** the committed worker Job template. It holds the same Gitea token as `pm-agent-credentials`, in a second format, because the kubelet reads it before any container starts.
- Admission (`pm-agent-job-shape`) rule 1 now **requires** that entry, and the new rule 7 forbids any other entry or a second one.
- `PM_SELFTEST` asserts the template names exactly `pm-agent-registry`. It runs in agent-runner's Containerfile `verify` stage, so CI checks it on every image build.
- Conflicts with #81 were resolved keeping both sides: "pull credential wired" and "CI publishes, Renovate bumps". No workstation build or push text came back.
## Commit 2: going live
- **First published digest pinned.** `deployment.yaml` and `PM_WORKER_IMAGE` now name `agent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42`. That is images run 340 (`sha-523cd9118f95`, also `main`), read from the registry manifest API. The `# renovate-digest:` markers are unchanged, so Renovate still owns every later bump. The value matches admission's `agent-runner@sha256:*` pattern.
- **The first run is dry and needs a label.** `PM_DRY_RUN=true`, `PM_REQUIRE_LABEL=agent`, `config-generation: "2"`. Dry run is *required*, not just cautious. The run-340 image's baked worker template predates `imagePullSecrets`, so admission would deny its worker Jobs. This merge touches `infra/agent/**`, so `images.yaml` **re-publishes agent-runner**. Workers become admissible once Renovate's bump to that new digest merges. Only then flip `PM_DRY_RUN`.
- **Token scopes fixed.** Gitea 1.24.3 scopes the whole `/api/v1/user` group, and the dispatcher's first call is `GET /user`. That call needs `read:user`, which the old docs did not list. The documented set is now `read:user, write:repository, write:issue, read:package`.
- **Org-package permission is documented.** `determineAccessMode` (v1.24.3) gives package read through an org team's Packages unit, or through org visibility. The org is `public` today. The `.example` now has a `pm-agent-pull` team with no repos and Packages = Read, so access does not depend on visibility.
- `sops` is toolbox-only; both `.example`s say so. They give the in-place `./infra/devcontainer/dev.sh run sops <file>` edit, which writes no plaintext file.
- The runbook's Secret-key check emitted values before the pipe trimmed them. It is now a `go-template` that prints key names only.
- Handoff: `decisions/2026-10-30-pm-agent-live.md`. The 2026-10-06 entry has a dated addendum that corrects its stale and wrong claims.
## Verified
- `kyverno test platform/kyverno/tests/pm-agent` in the pinned toolbox: **33 passed, 0 failed**.
- `PM_SELFTEST=1 pm-dispatch` (worktree template and persona) in the toolbox: **selftest OK, rc 0**.
- Local `./ci/gate.sh` in the pinned toolbox, from a §0 worktree at `7f0cbc7`: **rc 3 (UNDETERMINED, not green)**. Only `gitea-settings` did not run (no `SETTINGS_API_TOKEN` locally). `spec-immutability` passed. The Kyverno L0 suite: 87 passed, 0 failed. decisions-log, lint, l0, render-golden and l1 were all green. Locally, approval-policy reports N/A because the run is not a PR event.
- All three prod nodes, checked read-only: `/etc/hosts` pins `gitea.home.keithjackson.dev` to `192.168.2.20`. `curl /v2/` from each node returns `401` with `ssl_verify_result=0`.
- The agent uses `http://gitea.gitea.svc.cluster.local:3000` for the API and git. It never goes through the Gateway, so #83's issue does not apply. Live `gitea-allow-pm-agent-ingress` is `VALID True`.
## Approval
This touches sensitive paths (`platform/kyverno/**`, `**/*.sops.yaml`), so `approval-policy` will be **red until the operator posts `/approve-override <reason>` as a PR review (type Comment)**. The agent has not posted one and must not.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The registry had two blockers. #59 fixed the first (nodes resolved the hostname to the
non-hairpinned WAN and failed TLS against the router's CN=%D cert). This is the second:
the pull is unauthenticated, and this registry refuses unauthenticated clients outright —
measured 2026-10-05, its anonymous token carries access: null and every path 401s.
pm-agent-registry is a dockerconfigjson Secret (SOPS, placeholders) referenced by
imagePullSecrets on the dispatcher Deployment and on the committed worker Job template. It
holds the SAME Gitea token as pm-agent-credentials, because the two consumers need two
formats: the application reads a raw token from env, the kubelet reads a dockerconfigjson
before any container starts. Same one-value-in-two-files pattern as the Keycloak client
secret, documented in both .example files. The operator's part is the value plus one new
scope: read:package.
ADMISSION GAINED A SEVENTH RULE. imagePullSecrets is a pod-spec field no container
references, which is exactly why it is easy to leave unread — the same shape of omission
as initContainers, ephemeralContainers, env and envFrom before it. The kubelet reads the
named Secret, in this namespace, before anything runs. Rule 1's pattern now REQUIRES the
committed entry; rule 7 forbids any other and any second one. Both halves are needed: a
foreach over an absent list yields no result.
The leverage is small and the policy says so: the image is digest-pinned, so a different
pull credential cannot change WHAT runs. Pinned anyway — 'only the committed shape' is the
premise, and the rule costs four lines.
kyverno test: 33 passed, 0 failed (was 28). Three new fixtures, each a real path: the
committed entry plus a second one, a different Secret, and none at all.
The gate also caught me shipping the Secret in plaintext — I never ran sops -e -i on the
placeholder. Recorded in the handoff entry, because the file looked finished.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
gitea-admin
changed title from feat(pm-agent): the kubelet's pull credential, and the seventh field admission had not read to feat(pm-agent): pull credential + admission rule 7, first published digest pinned, dry-run first start, token scopes fixed2026-10-30 06:19:58 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The kubelet's pull credential for
agent-runner, plus what is left in git to take the PM agent live. Rebased 2026-10-30 ontomainafter #81 (CI image pipeline), #82 and #83.Commit 1 (original #61, rebased): the pull credential and admission rule 7
pm-agent-registry: akubernetes.io/dockerconfigjsonSecret (SOPS, placeholder), referenced byimagePullSecretson the dispatcher Deployment and the committed worker Job template. It holds the same Gitea token aspm-agent-credentials, in a second format, because the kubelet reads it before any container starts.pm-agent-job-shape) rule 1 now requires that entry, and the new rule 7 forbids any other entry or a second one.PM_SELFTESTasserts the template names exactlypm-agent-registry. It runs in agent-runner's Containerfileverifystage, so CI checks it on every image build.Commit 2: going live
deployment.yamlandPM_WORKER_IMAGEnow nameagent-runner@sha256:c4eba5b3c508bf456a03391ac76f8275e7ea28a829da1450d99f6f6f25c6eb42. That is images run 340 (sha-523cd9118f95, alsomain), read from the registry manifest API. The# renovate-digest:markers are unchanged, so Renovate still owns every later bump. The value matches admission'sagent-runner@sha256:*pattern.PM_DRY_RUN=true,PM_REQUIRE_LABEL=agent,config-generation: "2". Dry run is required, not just cautious. The run-340 image's baked worker template predatesimagePullSecrets, so admission would deny its worker Jobs. This merge touchesinfra/agent/**, soimages.yamlre-publishes agent-runner. Workers become admissible once Renovate's bump to that new digest merges. Only then flipPM_DRY_RUN./api/v1/usergroup, and the dispatcher's first call isGET /user. That call needsread:user, which the old docs did not list. The documented set is nowread:user, write:repository, write:issue, read:package.determineAccessMode(v1.24.3) gives package read through an org team's Packages unit, or through org visibility. The org ispublictoday. The.examplenow has apm-agent-pullteam with no repos and Packages = Read, so access does not depend on visibility.sopsis toolbox-only; both.examples say so. They give the in-place./infra/devcontainer/dev.sh run sops <file>edit, which writes no plaintext file.go-templatethat prints key names only.decisions/2026-10-30-pm-agent-live.md. The 2026-10-06 entry has a dated addendum that corrects its stale and wrong claims.Verified
kyverno test platform/kyverno/tests/pm-agentin the pinned toolbox: 33 passed, 0 failed.PM_SELFTEST=1 pm-dispatch(worktree template and persona) in the toolbox: selftest OK, rc 0../ci/gate.shin the pinned toolbox, from a §0 worktree at7f0cbc7: rc 3 (UNDETERMINED, not green). Onlygitea-settingsdid not run (noSETTINGS_API_TOKENlocally).spec-immutabilitypassed. The Kyverno L0 suite: 87 passed, 0 failed. decisions-log, lint, l0, render-golden and l1 were all green. Locally, approval-policy reports N/A because the run is not a PR event./etc/hostspinsgitea.home.keithjackson.devto192.168.2.20.curl /v2/from each node returns401withssl_verify_result=0.http://gitea.gitea.svc.cluster.local:3000for the API and git. It never goes through the Gateway, so #83's issue does not apply. Livegitea-allow-pm-agent-ingressisVALID True.Approval
This touches sensitive paths (
platform/kyverno/**,**/*.sops.yaml), soapproval-policywill be red until the operator posts/approve-override <reason>as a PR review (type Comment). The agent has not posted one and must not.🤖 Generated with Claude Code
7fa5e84d25to7f0cbc7ba2feat(pm-agent): the kubelet's pull credential, and the seventh field admission had not readto feat(pm-agent): pull credential + admission rule 7, first published digest pinned, dry-run first start, token scopes fixed