# ============================================================================= # TEMPLATE — NOT APPLIED, NOT ENCRYPTED (`.yaml.example` is ignored by kustomize and is # never auto-encrypted by SOPS). Reference scaffold + OPERATOR HAND-OFF. # # WHAT THIS IS: the two credentials the PM agent service holds, and the only two. The # dispatcher and every worker read both by name (R-IDP-04); nothing else in this repository # references them. # # THE COMMITTED `pm-agent-credentials.sops.yaml` HOLDS PLACEHOLDERS. It is encrypted to the # repository's age recipient so the kustomization builds and Flux reconciles, but its values # are the obviously-impossible strings below. Until the operator replaces them the service # is INERT and says so: the dispatcher authenticates to Gitea at startup, fails, and # CrashLoops with `GET /user returned no login — is GITEA_TOKEN valid?` in its log. That is # deliberate — a service that quietly did nothing would be worse than one that visibly # cannot start. # # ============================================================================= # HAND-OFF STEPS (operator) # ============================================================================= # # (1) CREATE A DEDICATED GITEA ACCOUNT FOR THE AGENT. Do NOT reuse `jakman85`, and do NOT # reuse the existing `gitea-admin` agent account. Both hold `owner` permission on this # repository, and `infra/gitea/approval-policy.json` authorises `/approve-override` by # LIVE PERMISSION ALONE — so a token belonging to either would let this service clear # the sensitive-path human review on its own pull requests. CLAUDE.md §1 forbids an agent # doing that in the strongest terms it has, and the cheapest way to make it impossible # rather than forbidden is for the token's account not to have the permission. # # Gitea -> Site Administration -> Identity & Access -> User Accounts -> Create # username: pm-agent (any name; it appears as the author of every comment) # NOT an administrator # Then add it to THIS REPOSITORY as a collaborator with permission `Write`: # Repository -> Settings -> Collaborators -> add `pm-agent`, access `Write` # # Verify, do not assume (CLAUDE.md §2b) — this must print `write`, never `admin` or # `owner`: # # curl -s -H "Authorization: token " \ # https://gitea.home.keithjackson.dev/api/v1/repos/Home-Network/home-network-k3s-impl/collaborators/pm-agent/permission \ # | python3 -c 'import json,sys; print(json.load(sys.stdin).get("permission"))' # # HONEST LIMIT, read it before deciding this is enough: `Write` in Gitea includes the # ability to MERGE a pull request, and no token scope separates "push a branch and open a # pull request" from "merge it". The service never issues a merge call — pm-worker's code # is the whole set of API calls it makes, and there is no merge among them — but that is # a property of the code, not of the credential. What the permission level DOES buy # structurally is the override: a non-admin account cannot clear the sensitive-path # review, so a sensitive-path pull request from this agent correctly sits unmergeable # until a human reviews it. # # (2) MINT THE GITEA TOKEN as that user (log in as `pm-agent`, or use the admin API to # create a token for it): Settings -> Applications -> Generate New Token. # # Scopes: `write:repository` and `write:issue`. Nothing else — in particular NOT # `write:admin`, NOT `write:organization`, NOT `write:user`. # # `write:repository` covers the clone, the branch push and the pull-request create; # `write:issue` covers reading and posting issue comments. (The repository's OTHER CI # token, `SETTINGS_API_TOKEN`, is a separate credential with `read:admin` — do not # conflate them, and do not give this service that one. The 2026-10-02 measurement on # PR #38 recorded in infra/gitea/approval-policy.json is about that token, not this one.) # # (3) MINT THE ANTHROPIC SUBSCRIPTION TOKEN. This service authenticates with a Claude # SUBSCRIPTION (Pro, Max, Team or Enterprise), not with a Console API key. On a machine # where you can complete a browser login: # # claude setup-token # # It opens the normal authorisation flow, prints a ONE-YEAR OAuth token, and saves it # nowhere. Copy it into `claude-code-oauth-token` below. # # THREE THINGS ABOUT THIS TOKEN: # * It EXPIRES after a year, with no in-cluster warning: Claude Code's expiry notice # only appears for an interactive `/login` credential, never for this variable. Put # the date in the handoff entry and in runbooks/pm-agent.md; when it lapses, every # session fails at once and the only symptom is worker Jobs failing to authenticate. # * It must NOT be accompanied by an `ANTHROPIC_API_KEY`. Claude Code's credential # precedence puts an API key ABOVE this variable, so a stray key would silently # take over and bill elsewhere (or fail). pm-worker unsets both API-key variables # before it starts a session, and the image sets neither. # * Usage counts against the subscription seat's own limits, shared with every other # place that seat is used. That is the real reason PM_MAX_ACTIVE_WORKERS defaults to # 2 — several concurrent Claude Code sessions on one seat contend for the same # account limits, and a rate-limited worker fails mid-assignment. # # (4) COPY, FILL, ENCRYPT, COMMIT: # # cp platform/pm-agent/pm-agent-credentials.sops.yaml.example \ # platform/pm-agent/pm-agent-credentials.sops.yaml # $EDITOR platform/pm-agent/pm-agent-credentials.sops.yaml # the two real values # sops -e -i platform/pm-agent/pm-agent-credentials.sops.yaml # # The root `.sops.yaml` creation rule matches this path and encrypts only `data`/ # `stringData`, so the diff stays reviewable. NEVER `sops -d` this file into a terminal, # a log or a transcript (CLAUDE.md §6), and never print the Secret's `.data` when # checking that it exists — list key names only. # # It is already listed under `resources:` in platform/pm-agent/kustomization.yaml, so # replacing the placeholder file is the whole change. # # (5) ROTATION. Both values are independent. To rotate either, repeat its step, re-encrypt, # and merge — Flux updates the Secret and the dispatcher's pod must then be restarted to # re-read it (a Secret change does not roll a Deployment by itself). The worker Jobs pick # the new value up on their next run with no action at all. # ============================================================================= apiVersion: v1 kind: Secret metadata: name: pm-agent-credentials namespace: pm-agent type: Opaque stringData: # Gitea token for the dedicated, NON-ADMIN `pm-agent` account. # Scopes: write:repository, write:issue. gitea-token: REPLACE-ME-gitea-token # Output of `claude setup-token` on a subscription account. One year's validity. claude-code-oauth-token: REPLACE-ME-claude-code-oauth-token