From 470ece82ed9b2342f9de24c7d6bd87d9b300e404 Mon Sep 17 00:00:00 2001 From: tegwick Date: Thu, 27 Aug 2026 23:12:57 +0200 Subject: [PATCH] feat(forge): resolve an optional forge read credential (STATE-WP-0084-T02/T03) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine repositories are invisible to derivation because central may not read them. This adds the consuming half of the credential lane MASON-WP-0003 built. The cluster has no agent injector and no secrets-store CSI driver, so the pod authenticates to OpenBao with a projected ServiceAccount token (audience `openbao`, not the API server) and reads the KV path itself. `forgeRead.*` carries coordinates only; no credential is a chart value, an image layer, or a Kubernetes Secret. The credential reaches git through GIT_CONFIG_* setting http.extraHeader, not through `-c` and not through userinfo in the clone URL — both of those put the token in the process listing. It is redacted from ForgeDeriveError, which is logged, stored in reset outcomes, and returned over the API. Absent stays a supported state: with no credential, or with OpenBao unreachable, resolution returns None and public derivation runs unchanged. Raising would turn "nine repositories are unreadable" into "the pass failed", which is what T01 exists to prevent. Chart default is disabled. 717 pass. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 2583210@bnt-lap001 Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006 --- api/services/forge_credential.py | 122 +++ api/services/forge_projection.py | 42 +- .../state-hub/templates/deployment.yaml | 39 +- .../state-hub/templates/serviceaccount.yaml | 12 + .../apps/charts/state-hub/values.yaml | 25 + .../legacy-meter-weekly-review-20260827.json | 882 ++++++++++++++++++ tests/test_forge_projection.py | 152 +++ ...084-forge-read-for-private-repositories.md | 67 +- 8 files changed, 1334 insertions(+), 7 deletions(-) create mode 100644 api/services/forge_credential.py create mode 100644 deploy/railiance/apps/charts/state-hub/templates/serviceaccount.yaml create mode 100644 docs/evidence/legacy-meter-weekly-review-20260827.json diff --git a/api/services/forge_credential.py b/api/services/forge_credential.py new file mode 100644 index 0000000..5714b65 --- /dev/null +++ b/api/services/forge_credential.py @@ -0,0 +1,122 @@ +"""Resolve the forge read credential (STATE-WP-0084-T03, MASON-WP-0003-T05). + +Three sources, tried in order: a mounted file, an environment variable, and +OpenBao via Kubernetes auth. Production uses the third — this cluster has no +agent injector and no secrets-store CSI driver, so the pod authenticates with +its projected ServiceAccount token and reads the KV path itself, which is what +`MASON-WP-0003-T02` built the Kubernetes auth role for. The first two exist so +the code is runnable and testable outside the cluster. + +**Absent is a supported state, never an error.** A hub with no credential still +derives every public repository; only private ones become unreadable, and +`STATE-WP-0084-T01` already reports that as its own named condition rather than +as records that stopped deriving. So every failure here — no configuration, no +network, OpenBao down, permission denied — resolves to `None`. Raising would +convert "cannot read nine repositories" into "the whole pass failed". +""" + +from __future__ import annotations + +import logging +import os +import time +from pathlib import Path + +import httpx + +logger = logging.getLogger(__name__) + +TOKEN_ENV = "FORGE_READ_TOKEN" +TOKEN_FILE_ENV = "FORGE_READ_TOKEN_FILE" +OPENBAO_ADDR_ENV = "OPENBAO_ADDR" +OPENBAO_ROLE_ENV = "OPENBAO_K8S_ROLE" +OPENBAO_JWT_PATH_ENV = "OPENBAO_K8S_TOKEN_PATH" +OPENBAO_AUTH_MOUNT_ENV = "OPENBAO_K8S_AUTH_MOUNT" +SECRET_PATH_ENV = "FORGE_READ_SECRET_PATH" +SECRET_KEY_ENV = "FORGE_READ_SECRET_KEY" + +# Long enough that a fleet reset of 121 repositories does not re-authenticate +# 121 times; short enough that a rotated token is picked up without a redeploy, +# which is what MASON-WP-0003-T02 requires of this lane. +CACHE_TTL_SECONDS = 300.0 + +_cache: tuple[float, str | None] | None = None + + +def reset_cache() -> None: + global _cache + _cache = None + + +def _from_file() -> str | None: + path = os.environ.get(TOKEN_FILE_ENV) + if not path: + return None + try: + return Path(path).read_text(encoding="utf-8").strip() or None + except OSError: + # Deliberately not falling through to the environment: a broken mount + # that silently used a stale value would look like success. + logger.warning("forge credential: token file %s is unreadable", path) + return None + + +def _from_env() -> str | None: + return (os.environ.get(TOKEN_ENV) or "").strip() or None + + +def _from_openbao() -> str | None: + addr = (os.environ.get(OPENBAO_ADDR_ENV) or "").strip().rstrip("/") + secret_path = (os.environ.get(SECRET_PATH_ENV) or "").strip().strip("/") + role = (os.environ.get(OPENBAO_ROLE_ENV) or "").strip() + jwt_path = os.environ.get(OPENBAO_JWT_PATH_ENV) or "/var/run/secrets/openbao/token" + mount = (os.environ.get(OPENBAO_AUTH_MOUNT_ENV) or "kubernetes").strip("/") + key = (os.environ.get(SECRET_KEY_ENV) or "token").strip() + if not (addr and secret_path and role): + return None + try: + jwt = Path(jwt_path).read_text(encoding="utf-8").strip() + except OSError: + logger.warning("forge credential: no ServiceAccount token at %s", jwt_path) + return None + try: + with httpx.Client(timeout=10.0) as client: + login = client.post( + f"{addr}/v1/auth/{mount}/login", json={"role": role, "jwt": jwt} + ) + login.raise_for_status() + client_token = login.json()["auth"]["client_token"] + read = client.get( + f"{addr}/v1/{secret_path}", headers={"X-Vault-Token": client_token} + ) + read.raise_for_status() + data = read.json()["data"] + # KV v2 nests the payload under a second "data"; v1 does not. + if isinstance(data.get("data"), dict): + data = data["data"] + except (httpx.HTTPError, KeyError, ValueError) as exc: + # Never include the response body: a failed KV read can echo content. + logger.warning("forge credential: OpenBao lookup failed (%s)", type(exc).__name__) + return None + value = data.get(key) + if not isinstance(value, str) or not value.strip(): + logger.warning("forge credential: key %r absent at the KV path", key) + return None + return value.strip() + + +def forge_read_token(*, use_cache: bool = True) -> str | None: + """The forge read credential, or `None` if this instance has none.""" + global _cache + now = time.monotonic() + if use_cache and _cache is not None and now - _cache[0] < CACHE_TTL_SECONDS: + return _cache[1] + if os.environ.get(TOKEN_FILE_ENV): + # Configured to use a file means *that* file and nothing else. Falling + # back would let a broken mount quietly resolve to a stale environment + # value that nobody knows is in use. + token = _from_file() + else: + token = _from_env() or _from_openbao() + _cache = (now, token) + return token diff --git a/api/services/forge_projection.py b/api/services/forge_projection.py index 0a8da10..e04f6a3 100644 --- a/api/services/forge_projection.py +++ b/api/services/forge_projection.py @@ -13,6 +13,7 @@ what makes the reset in `T03` verifiable: you can always ask what the projection from __future__ import annotations import os +import base64 import re import subprocess import tempfile @@ -163,16 +164,50 @@ class DerivedProjection: } -def _run_git(*args: str, cwd: str | None = None, timeout: float = 120.0) -> str: +from api.services.forge_credential import forge_read_token # noqa: F401 + +# Kept as module attributes so callers and tests that reached for them here +# still resolve after the sources moved to `forge_credential`. +FORGE_TOKEN_ENV = "FORGE_READ_TOKEN" +FORGE_TOKEN_FILE_ENV = "FORGE_READ_TOKEN_FILE" + + +def _credential_env(token: str | None) -> dict[str, str]: + """Git config carrying the credential, passed by environment not argv. + + `-c http.extraHeader=...` would place the token in the process command line, + where it is readable by anything that can run `ps` and lands in any log that + records invocations. GIT_CONFIG_* achieves the same configuration without + that exposure. + """ + if not token: + return {} + header = base64.b64encode(f"x-access-token:{token}".encode()).decode() + return { + "GIT_CONFIG_COUNT": "1", + "GIT_CONFIG_KEY_0": "http.extraHeader", + "GIT_CONFIG_VALUE_0": f"Authorization: Basic {header}", + } + + +def _run_git( + *args: str, cwd: str | None = None, timeout: float = 120.0, token: str | None = None +) -> str: # Without this a clone of a private repository blocks on a username prompt # instead of failing, and an unattended derivation pass hangs rather than # reporting. Failing fast is what makes the unreadable case observable. env = {**os.environ, "GIT_TERMINAL_PROMPT": "0", "GIT_ASKPASS": "", "GCM_INTERACTIVE": "never"} + env.update(_credential_env(token)) proc = subprocess.run( ["git", *args], cwd=cwd, capture_output=True, text=True, timeout=timeout, env=env ) if proc.returncode != 0: - raise ForgeDeriveError((proc.stderr or proc.stdout).strip()[:400]) + detail = (proc.stderr or proc.stdout).strip()[:400] + if token: + # Never let a credential reach an exception that is logged, stored + # in a reset outcome, or returned over the API. + detail = detail.replace(token, "***") + raise ForgeDeriveError(detail) return proc.stdout.strip() @@ -269,12 +304,13 @@ def derive_from_forge( someone's local state instead (ADR-012 context). """ url = f"{forge_base.rstrip('/')}/{repo_slug}.git" + token = forge_read_token() with tempfile.TemporaryDirectory(prefix=f"forge-{repo_slug}-") as tmp: args = ["clone", "--depth", "1", "--quiet"] if ref: args += ["--branch", ref] try: - _run_git(*args, url, tmp) + _run_git(*args, url, tmp, token=token) except subprocess.TimeoutExpired as exc: raise ForgeDeriveError(f"clone timed out for {repo_slug}") from exc except ForgeDeriveError as exc: diff --git a/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml index a72abae..d91989b 100644 --- a/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml +++ b/deploy/railiance/apps/charts/state-hub/templates/deployment.yaml @@ -18,11 +18,14 @@ spec: checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} labels: {{- include "statehub.labels" . | nindent 8 }} spec: + {{- if .Values.serviceAccount.name }} + serviceAccountName: {{ .Values.serviceAccount.name | quote }} + {{- end }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} - {{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }} + {{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled .Values.forgeRead.enabled }} volumes: {{- if .Values.sweep.enabled }} - name: sweep-repos @@ -39,6 +42,19 @@ spec: configMap: name: {{ .Values.classificationAllowed.name | quote }} {{- end }} + {{- if .Values.forgeRead.enabled }} + # A projected token with an explicit audience, not the legacy + # auto-mounted one: the auto-mounted token has the API server as its + # audience, so a copy of it is a credential for the cluster. This one is + # only accepted by OpenBao, and the kubelet rotates it in place. + - name: openbao-token + projected: + sources: + - serviceAccountToken: + path: token + audience: {{ .Values.forgeRead.openbao.audience | quote }} + expirationSeconds: {{ .Values.forgeRead.openbao.expirationSeconds }} + {{- end }} {{- end }} containers: - name: state-hub @@ -58,7 +74,7 @@ spec: - -c - git config --global --add safe.directory '*' {{- end }} - {{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled }} + {{- if or .Values.sweep.enabled .Values.classificationAllowed.enabled .Values.forgeRead.enabled }} volumeMounts: {{- if .Values.sweep.enabled }} - name: sweep-repos @@ -72,6 +88,11 @@ spec: mountPath: {{ .Values.classificationAllowed.mountPath | quote }} readOnly: true {{- end }} + {{- if .Values.forgeRead.enabled }} + - name: openbao-token + mountPath: /var/run/secrets/openbao + readOnly: true + {{- end }} env: {{- if .Values.sweep.enabled }} - name: STATE_HUB_SWEEP_HOSTNAME @@ -83,6 +104,20 @@ spec: - name: REPO_CLASSIFICATION_ALLOWED_PATH value: {{ printf "%s/repo-classification.allowed.yaml" .Values.classificationAllowed.mountPath | quote }} {{- end }} + {{- if .Values.forgeRead.enabled }} + # Coordinates only. The token itself is never a chart value, never + # in the image, and never in a Kubernetes Secret in this release. + - name: OPENBAO_ADDR + value: {{ .Values.forgeRead.openbao.addr | quote }} + - name: OPENBAO_K8S_ROLE + value: {{ .Values.forgeRead.openbao.role | quote }} + - name: OPENBAO_K8S_TOKEN_PATH + value: /var/run/secrets/openbao/token + - name: FORGE_READ_SECRET_PATH + value: {{ .Values.forgeRead.openbao.secretPath | quote }} + - name: FORGE_READ_SECRET_KEY + value: {{ .Values.forgeRead.openbao.secretKey | quote }} + {{- end }} {{- end }} envFrom: {{- if .Values.config.enabled }} diff --git a/deploy/railiance/apps/charts/state-hub/templates/serviceaccount.yaml b/deploy/railiance/apps/charts/state-hub/templates/serviceaccount.yaml new file mode 100644 index 0000000..6efb4c9 --- /dev/null +++ b/deploy/railiance/apps/charts/state-hub/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create }} +# STATE-WP-0084-T02. The OpenBao Kubernetes auth role built by MASON-WP-0003-T02 +# binds to this ServiceAccount by name and deliberately does not bind to +# `default` — so the pod must stop running as `default` before the lane can +# carry anything. Creating it here keeps that binding in the release rather than +# in an operator's memory. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.serviceAccount.name | quote }} + labels: {{- include "statehub.labels" . | nindent 4 }} +{{- end }} diff --git a/deploy/railiance/apps/charts/state-hub/values.yaml b/deploy/railiance/apps/charts/state-hub/values.yaml index a1a6200..0ec45e9 100644 --- a/deploy/railiance/apps/charts/state-hub/values.yaml +++ b/deploy/railiance/apps/charts/state-hub/values.yaml @@ -30,9 +30,34 @@ config: instanceRole: unknown instanceLabel: "" +# STATE-WP-0084-T02. The OpenBao Kubernetes auth role binds to this name and +# not to `default`, so this is load-bearing, not cosmetic. +serviceAccount: + create: true + name: state-hub + secret: name: state-hub-env +# Forge read credential for deriving private repositories (STATE-WP-0084, +# MASON-WP-0003). Disabled by default: a hub without it still derives every +# public repository, so this is added capability, not a prerequisite. +# +# There is no agent injector and no secrets-store CSI driver on this cluster, so +# the pod authenticates to OpenBao itself with its projected ServiceAccount +# token and reads the KV path. Nothing here is the credential; these are only +# coordinates. Rotating the token in OpenBao needs no chart change and no +# redeploy. +forgeRead: + enabled: false + openbao: + addr: "" + role: state-hub-forge-derivation + audience: openbao + expirationSeconds: 3600 + secretPath: "" + secretKey: token + resources: requests: cpu: 250m diff --git a/docs/evidence/legacy-meter-weekly-review-20260827.json b/docs/evidence/legacy-meter-weekly-review-20260827.json new file mode 100644 index 0000000..bc31c52 --- /dev/null +++ b/docs/evidence/legacy-meter-weekly-review-20260827.json @@ -0,0 +1,882 @@ +{ + "captured_at": "2026-08-27T14:00:07.862953+00:00", + "api_base": "http://127.0.0.1:8000", + "workplan": "STATE-WP-0070", + "retired_interfaces": [], + "weekly_review": { + "generated_at": "2026-08-27T14:00:09.212468Z", + "window_start": "2026-08-20T14:00:09.134254Z", + "window_end": "2026-08-27T14:00:09.134254Z", + "cadence": "weekly", + "activity_core_handoff": { + "activity_id": "statehub-legacy-interface-review", + "cadence": "weekly", + "source_endpoint": "/legacy-meter/weekly-review", + "state_owner": "state-hub", + "scheduler_owner": "activity-core" + }, + "interfaces": [ + { + "interface": { + "id": "8051fb40-66c9-4dc4-a413-13e8573e04a3", + "interface_key": "event_subject:org.statehub.workstream.completed", + "interface_kind": "event_subject", + "legacy_since": "2026-06-04T06:09:42.198193Z", + "replacement_ref": "org.statehub.workplan.completed", + "owner_component": "state-hub.events", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T06:09:42.198193Z", + "updated_at": "2026-06-04T06:09:42.198193Z" + }, + "all_time": { + "calls": 247, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 247 + }, + "users": { + "unknown": 247 + }, + "components": { + "state-hub.events": 247 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-13T13:22:00.748140Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 45d (>= 30d required for 247 all-time call(s))" + }, + { + "interface": { + "id": "cd9342fd-27e7-4a46-a1be-d41499e106a3", + "interface_key": "rest_api:DELETE /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-06T17:28:13.052813Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-06T17:28:13.052813Z", + "updated_at": "2026-06-06T17:28:13.052813Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-06-06T17:28:13.047529Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 81d (>= 7d required for 1 all-time call(s))" + }, + { + "interface": { + "id": "d0294dce-7913-4268-94c3-1b2502aefb0f", + "interface_key": "rest_api:GET /sbom/", + "interface_kind": "rest_api", + "legacy_since": "2026-08-22T17:06:38.703608Z", + "replacement_ref": "sbom-nexus:/sbom/", + "owner_component": "state-hub.sbom-compat", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-08-22T17:06:38.703608Z", + "updated_at": "2026-08-22T17:23:43.730661Z" + }, + "all_time": { + "calls": 4, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4 + }, + "users": { + "unknown": 4 + }, + "components": { + "unknown": 4 + } + }, + "window": { + "calls": 4, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4 + }, + "users": { + "unknown": 4 + }, + "components": { + "unknown": 4 + } + }, + "last_seen_at": "2026-08-22T17:23:46.146788Z", + "retirement_candidate": false, + "retirement_reason": "4 call(s) in review window" + }, + { + "interface": { + "id": "2f77ac12-8c97-4974-a2f9-2996927982d1", + "interface_key": "rest_api:GET /sbom/report/licences/", + "interface_kind": "rest_api", + "legacy_since": "2026-08-22T17:06:41.978426Z", + "replacement_ref": "sbom-nexus:/sbom/", + "owner_component": "state-hub.sbom-compat", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-08-22T17:06:41.978426Z", + "updated_at": "2026-08-22T17:23:44.794796Z" + }, + "all_time": { + "calls": 4, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4 + }, + "users": { + "unknown": 4 + }, + "components": { + "unknown": 4 + } + }, + "window": { + "calls": 4, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4 + }, + "users": { + "unknown": 4 + }, + "components": { + "unknown": 4 + } + }, + "last_seen_at": "2026-08-22T17:56:34.405129Z", + "retirement_candidate": false, + "retirement_reason": "4 call(s) in review window" + }, + { + "interface": { + "id": "b627edf5-3302-4366-ba2e-547d32f634b5", + "interface_key": "rest_api:GET /sbom/snapshots/", + "interface_kind": "rest_api", + "legacy_since": "2026-08-22T17:06:37.057774Z", + "replacement_ref": "sbom-nexus:/sbom/", + "owner_component": "state-hub.sbom-compat", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-08-22T17:06:37.057774Z", + "updated_at": "2026-08-22T17:22:15.080213Z" + }, + "all_time": { + "calls": 5, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 5 + }, + "users": { + "unknown": 5 + }, + "components": { + "unknown": 5 + } + }, + "window": { + "calls": 5, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 5 + }, + "users": { + "unknown": 5 + }, + "components": { + "unknown": 5 + } + }, + "last_seen_at": "2026-08-22T17:23:53.844021Z", + "retirement_candidate": false, + "retirement_reason": "5 call(s) in review window" + }, + { + "interface": { + "id": "d03ee274-cd6f-4cd5-8f22-e4d298416299", + "interface_key": "rest_api:GET /sbom/snapshots/{snapshot_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-08-22T17:23:45.969876Z", + "replacement_ref": "sbom-nexus:/sbom/", + "owner_component": "state-hub.sbom-compat", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-08-22T17:23:45.969876Z", + "updated_at": "2026-08-22T17:23:45.969876Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "last_seen_at": "2026-08-22T17:23:45.967492Z", + "retirement_candidate": false, + "retirement_reason": "1 call(s) in review window" + }, + { + "interface": { + "id": "a6cb291e-8312-42f8-8a66-fde369482881", + "interface_key": "rest_api:GET /sbom/{repo_slug}", + "interface_kind": "rest_api", + "legacy_since": "2026-08-22T17:23:45.089542Z", + "replacement_ref": "sbom-nexus:/sbom/", + "owner_component": "state-hub.sbom-compat", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-08-22T17:23:45.089542Z", + "updated_at": "2026-08-22T17:23:45.089542Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "last_seen_at": "2026-08-22T17:23:45.670511Z", + "retirement_candidate": false, + "retirement_reason": "1 call(s) in review window" + }, + { + "interface": { + "id": "3e4a3d0b-08fa-45c3-91e2-4f514a914b97", + "interface_key": "rest_api:GET /workstreams/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:26:14.533764Z", + "replacement_ref": "/workplans/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:26:14.533764Z", + "updated_at": "2026-06-04T00:26:14.533764Z" + }, + "all_time": { + "calls": 830269, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 830269 + }, + "users": { + "unknown": 830269 + }, + "components": { + "unknown": 830269 + } + }, + "window": { + "calls": 24746, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 24746 + }, + "users": { + "unknown": 24746 + }, + "components": { + "unknown": 24746 + } + }, + "last_seen_at": "2026-08-21T13:50:37.994459Z", + "retirement_candidate": false, + "retirement_reason": "24746 call(s) in review window" + }, + { + "interface": { + "id": "e4c140ec-7479-46f9-b185-4b533f19d639", + "interface_key": "rest_api:GET /workstreams/workplan-index", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T05:20:58.321869Z", + "replacement_ref": "/workplans/index", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T05:20:58.321869Z", + "updated_at": "2026-06-04T05:20:58.321869Z" + }, + "all_time": { + "calls": 39, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 39 + }, + "users": { + "unknown": 39 + }, + "components": { + "unknown": 39 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T05:20:33.378203Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 50d (>= 7d required for 39 all-time call(s))" + }, + { + "interface": { + "id": "3a00b19e-e7db-4403-98aa-a77c9ab61ecc", + "interface_key": "rest_api:GET /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:25:59.014966Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:25:59.014966Z", + "updated_at": "2026-06-04T00:25:59.014966Z" + }, + "all_time": { + "calls": 3411470, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 3411470 + }, + "users": { + "unknown": 3411470 + }, + "components": { + "unknown": 3411470 + } + }, + "window": { + "calls": 111410, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 111410 + }, + "users": { + "unknown": 111410 + }, + "components": { + "unknown": 111410 + } + }, + "last_seen_at": "2026-08-21T13:50:32.103349Z", + "retirement_candidate": false, + "retirement_reason": "111410 call(s) in review window" + }, + { + "interface": { + "id": "69e1a255-a8b9-4101-8a68-b1bed23abfda", + "interface_key": "rest_api:GET /workstreams/{workstream_id}/dependencies/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T00:25:59.291135Z", + "replacement_ref": "/workplans/{workplan_id}/dependencies/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T00:25:59.291135Z", + "updated_at": "2026-06-04T00:25:59.291135Z" + }, + "all_time": { + "calls": 1643952, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1643952 + }, + "users": { + "unknown": 1643952 + }, + "components": { + "unknown": 1643952 + } + }, + "window": { + "calls": 52105, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 52105 + }, + "users": { + "unknown": 52105 + }, + "components": { + "unknown": 52105 + } + }, + "last_seen_at": "2026-08-21T13:50:30.082480Z", + "retirement_candidate": false, + "retirement_reason": "52105 call(s) in review window" + }, + { + "interface": { + "id": "b1aae931-b51f-4a85-a723-865dd65f6132", + "interface_key": "rest_api:PATCH /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T06:09:41.901035Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T06:09:41.901035Z", + "updated_at": "2026-06-04T06:09:41.901035Z" + }, + "all_time": { + "calls": 584, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 584 + }, + "users": { + "unknown": 584 + }, + "components": { + "unknown": 584 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-08-16T16:33:34.045078Z", + "retirement_candidate": false, + "retirement_reason": "quiet 10d of 30d required for 584 all-time call(s)" + }, + { + "interface": { + "id": "ed6451c9-d2cc-4486-9afe-d3852782cabc", + "interface_key": "rest_api:POST /workstreams/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T07:40:24.168535Z", + "replacement_ref": "/workplans/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T07:40:24.168535Z", + "updated_at": "2026-06-04T07:40:24.168535Z" + }, + "all_time": { + "calls": 811, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 811 + }, + "users": { + "unknown": 811 + }, + "components": { + "unknown": 811 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-08-16T01:03:41.093965Z", + "retirement_candidate": false, + "retirement_reason": "quiet 11d of 30d required for 811 all-time call(s)" + }, + { + "interface": { + "id": "281fd706-b192-4194-8055-2d8733c115f9", + "interface_key": "rest_api:POST /workstreams/{workstream_id}/dependencies/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T22:57:37.149207Z", + "replacement_ref": "/workplans/{workplan_id}/dependencies/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T22:57:37.149207Z", + "updated_at": "2026-06-04T22:57:37.149207Z" + }, + "all_time": { + "calls": 4975, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4975 + }, + "users": { + "unknown": 4975 + }, + "components": { + "unknown": 4975 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-07T20:31:35.389791Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 50d (>= 30d required for 4975 all-time call(s))" + } + ], + "retirement_candidates": [ + { + "interface": { + "id": "8051fb40-66c9-4dc4-a413-13e8573e04a3", + "interface_key": "event_subject:org.statehub.workstream.completed", + "interface_kind": "event_subject", + "legacy_since": "2026-06-04T06:09:42.198193Z", + "replacement_ref": "org.statehub.workplan.completed", + "owner_component": "state-hub.events", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T06:09:42.198193Z", + "updated_at": "2026-06-04T06:09:42.198193Z" + }, + "all_time": { + "calls": 247, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 247 + }, + "users": { + "unknown": 247 + }, + "components": { + "state-hub.events": 247 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-13T13:22:00.748140Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 45d (>= 30d required for 247 all-time call(s))" + }, + { + "interface": { + "id": "cd9342fd-27e7-4a46-a1be-d41499e106a3", + "interface_key": "rest_api:DELETE /workstreams/{workstream_id}", + "interface_kind": "rest_api", + "legacy_since": "2026-06-06T17:28:13.052813Z", + "replacement_ref": "/workplans/{workplan_id}", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-06T17:28:13.052813Z", + "updated_at": "2026-06-06T17:28:13.052813Z" + }, + "all_time": { + "calls": 1, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 1 + }, + "users": { + "unknown": 1 + }, + "components": { + "unknown": 1 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-06-06T17:28:13.047529Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 81d (>= 7d required for 1 all-time call(s))" + }, + { + "interface": { + "id": "e4c140ec-7479-46f9-b185-4b533f19d639", + "interface_key": "rest_api:GET /workstreams/workplan-index", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T05:20:58.321869Z", + "replacement_ref": "/workplans/index", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T05:20:58.321869Z", + "updated_at": "2026-06-04T05:20:58.321869Z" + }, + "all_time": { + "calls": 39, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 39 + }, + "users": { + "unknown": 39 + }, + "components": { + "unknown": 39 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-08T05:20:33.378203Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 50d (>= 7d required for 39 all-time call(s))" + }, + { + "interface": { + "id": "281fd706-b192-4194-8055-2d8733c115f9", + "interface_key": "rest_api:POST /workstreams/{workstream_id}/dependencies/", + "interface_kind": "rest_api", + "legacy_since": "2026-06-04T22:57:37.149207Z", + "replacement_ref": "/workplans/{workplan_id}/dependencies/", + "owner_component": "state-hub.api", + "status": "legacy", + "replacement_verified": true, + "manual_hold": false, + "hold_reason": null, + "notes": null, + "retired_at": null, + "created_at": "2026-06-04T22:57:37.149207Z", + "updated_at": "2026-06-04T22:57:37.149207Z" + }, + "all_time": { + "calls": 4975, + "tenant_count": 1, + "user_count": 1, + "component_count": 1, + "tenants": { + "unknown": 4975 + }, + "users": { + "unknown": 4975 + }, + "components": { + "unknown": 4975 + } + }, + "window": { + "calls": 0, + "tenant_count": 0, + "user_count": 0, + "component_count": 0, + "tenants": {}, + "users": {}, + "components": {} + }, + "last_seen_at": "2026-07-07T20:31:35.389791Z", + "retirement_candidate": true, + "retirement_reason": "no usage in review window; quiet 50d (>= 30d required for 4975 all-time call(s))" + } + ] + }, + "days": 7 +} diff --git a/tests/test_forge_projection.py b/tests/test_forge_projection.py index ffa2e84..0c2ca38 100644 --- a/tests/test_forge_projection.py +++ b/tests/test_forge_projection.py @@ -12,6 +12,8 @@ from pathlib import Path import pytest +from api.services import forge_credential as fc + from api.services import forge_projection as fp @@ -532,3 +534,153 @@ class TestUnreadableIsNotMissing: d = outcome.to_dict() assert d["unreadable_count"] == 1 and d["errored"] == 1 assert d["repositories"] == 3 + + +class TestForgeCredential: + """Optional forge read credential (STATE-WP-0084-T03). + + Absent is a valid state: a hub with no credential must still derive every + public repository. The credential must never reach argv, a log, or an + exception — the places a secret leaks without anyone deciding to leak it. + """ + + @pytest.fixture(autouse=True) + def _clear_cache(self): + """The resolved credential is cached for 5 minutes in production. + + That cache is deliberate — a fleet reset of 121 repositories must not + authenticate to OpenBao 121 times — so tests clear it rather than + disable it, and exercise the same code path production uses. + """ + fc.reset_cache() + yield + fc.reset_cache() + + def test_absent_credential_is_none_not_empty_string(self, monkeypatch): + monkeypatch.delenv(fc.TOKEN_ENV, raising=False) + monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False) + assert fc.forge_read_token() is None + + def test_a_file_is_preferred_over_the_environment(self, tmp_path, monkeypatch): + """Kubernetes rotates a mounted file without a redeploy.""" + f = tmp_path / "token" + f.write_text("from-file\n", encoding="utf-8") + monkeypatch.setenv(fc.TOKEN_ENV, "from-env") + monkeypatch.setenv(fc.TOKEN_FILE_ENV, str(f)) + assert fc.forge_read_token() == "from-file" + + def test_an_unreadable_token_file_does_not_fall_back_silently(self, monkeypatch): + """Falling back to a stale env value would hide a broken mount.""" + monkeypatch.setenv(fc.TOKEN_FILE_ENV, "/nonexistent/token") + monkeypatch.setenv(fc.TOKEN_ENV, "from-env") + assert fc.forge_read_token() is None + + def test_openbao_is_the_last_resort_not_the_first(self, tmp_path, monkeypatch): + """A file or env value must not trigger a network call.""" + f = tmp_path / "token" + f.write_text("local", encoding="utf-8") + monkeypatch.setenv(fc.TOKEN_FILE_ENV, str(f)) + monkeypatch.setattr( + fc, "_from_openbao", lambda: pytest.fail("OpenBao consulted unnecessarily") + ) + assert fc.forge_read_token() == "local" + + def test_openbao_failure_resolves_to_absent_not_an_exception(self, monkeypatch): + """A hub that cannot reach OpenBao must still derive public repos. + + Raising here would turn "nine repositories are unreadable" into "the + whole derivation pass failed" — the outcome STATE-WP-0084-T01 exists to + prevent. + """ + monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False) + monkeypatch.delenv(fc.TOKEN_ENV, raising=False) + monkeypatch.setenv(fc.OPENBAO_ADDR_ENV, "https://openbao.invalid") + monkeypatch.setenv(fc.SECRET_PATH_ENV, "kv/data/forge") + monkeypatch.setenv(fc.OPENBAO_ROLE_ENV, "state-hub-forge-derivation") + monkeypatch.setenv(fc.OPENBAO_JWT_PATH_ENV, "/nonexistent/sa-token") + assert fc.forge_read_token() is None + + def test_openbao_unwraps_kv_v2(self, tmp_path, monkeypatch): + jwt = tmp_path / "sa" + jwt.write_text("jwt-value", encoding="utf-8") + monkeypatch.delenv(fc.TOKEN_FILE_ENV, raising=False) + monkeypatch.delenv(fc.TOKEN_ENV, raising=False) + monkeypatch.setenv(fc.OPENBAO_ADDR_ENV, "https://openbao.test") + monkeypatch.setenv(fc.SECRET_PATH_ENV, "kv/data/forge") + monkeypatch.setenv(fc.OPENBAO_ROLE_ENV, "state-hub-forge-derivation") + monkeypatch.setenv(fc.OPENBAO_JWT_PATH_ENV, str(jwt)) + + class R: + def __init__(self, payload): + self._p = payload + + def raise_for_status(self): + return None + + def json(self): + return self._p + + class C: + def __enter__(self): + return self + + def __exit__(self, *a): + return False + + def post(self, url, json): + assert json["jwt"] == "jwt-value" + assert json["role"] == "state-hub-forge-derivation" + return R({"auth": {"client_token": "bao-token"}}) + + def get(self, url, headers): + assert headers["X-Vault-Token"] == "bao-token" + return R({"data": {"data": {"token": "forge-secret"}}}) + + monkeypatch.setattr(fc.httpx, "Client", lambda **kw: C()) + assert fc.forge_read_token() == "forge-secret" + + def test_credential_never_appears_in_argv(self, monkeypatch): + """`-c http.extraHeader=` would put the token in every ps listing.""" + seen = {} + + class P: + returncode = 0 + stdout = "" + stderr = "" + + def fake_run(cmd, **kw): + seen["cmd"] = cmd + seen["env"] = kw.get("env") or {} + return P() + + monkeypatch.setattr(fp.subprocess, "run", fake_run) + fp._run_git("clone", "url", "dir", token="s3cret") + assert not any("s3cret" in part for part in seen["cmd"]) + assert seen["env"]["GIT_CONFIG_COUNT"] == "1" + assert "s3cret" not in seen["env"]["GIT_CONFIG_KEY_0"] + + def test_credential_is_redacted_from_failures(self, monkeypatch): + class P: + returncode = 128 + stdout = "" + stderr = "fatal: auth failed using s3cret" + + monkeypatch.setattr(fp.subprocess, "run", lambda *a, **k: P()) + with pytest.raises(fp.ForgeDeriveError) as exc: + fp._run_git("clone", token="s3cret") + assert "s3cret" not in str(exc.value) and "***" in str(exc.value) + + def test_no_credential_still_runs(self, monkeypatch): + class P: + returncode = 0 + stdout = "ok" + stderr = "" + seen = {} + + def fake_run(cmd, **kw): + seen["env"] = kw.get("env") or {} + return P() + + monkeypatch.setattr(fp.subprocess, "run", fake_run) + assert fp._run_git("status") == "ok" + assert "GIT_CONFIG_COUNT" not in seen["env"] diff --git a/workplans/STATE-WP-0084-forge-read-for-private-repositories.md b/workplans/STATE-WP-0084-forge-read-for-private-repositories.md index 89fb80c..5ba4de1 100644 --- a/workplans/STATE-WP-0084-forge-read-for-private-repositories.md +++ b/workplans/STATE-WP-0084-forge-read-for-private-repositories.md @@ -112,7 +112,7 @@ covered by tests that fail if the retirement path is reachable from either. ```task id: STATE-WP-0084-T02 -status: wait +status: progress priority: medium state_hub_task_id: "d8d41a89-1ebc-5118-bb73-cf8d8c114e16" ``` @@ -129,11 +129,37 @@ Rotation must not require a chart change or a redeploy. Acceptance: the pod can read the credential; nothing in the repository contains it; rotating the token does not require a redeploy. +**Chart landed 2026-08-27; awaits the minted token.** The cluster has no OpenBao +agent injector and no secrets-store CSI driver — checked, not assumed — so +there is nothing to inject with. The pod authenticates to OpenBao itself, which +is what `MASON-WP-0003-T02`'s Kubernetes auth role was built for. + +- `templates/serviceaccount.yaml` creates ServiceAccount `state-hub`, and the + Deployment now sets `serviceAccountName`. The auth role binds to this name and + deliberately not to `default`, so until this ships the pod cannot authenticate + at all. **This changes the identity the running pod uses** — expect a pod + restart on upgrade. +- The OpenBao token is a *projected* ServiceAccount token with audience + `openbao`, not the legacy auto-mounted one. The auto-mounted token's audience + is the API server, so a copy of it is a credential for the cluster; this one + is only accepted by OpenBao, and the kubelet rotates it in place. +- `forgeRead.*` carries coordinates only — address, role, KV path, key. No + credential is a chart value, an image layer, or a Kubernetes Secret in this + release. Rotating the token in OpenBao needs no chart change and no redeploy: + the value is re-read every 5 minutes. +- Default `forgeRead.enabled: false`. A hub without the credential still derives + every public repository, so this is added capability, not a prerequisite. + +Remaining before this is `done`: the minted read-only Forgejo token in the KV +path (`paste_once_provision`, outside both repositories), and a deploy with +`forgeRead.enabled=true` plus the real address and path in +`deploy/railiance/apps/helm/state-hub-values.yaml`. + ## Teach the derivation to use it ```task id: STATE-WP-0084-T03 -status: wait +status: progress priority: medium state_hub_task_id: "b22b24d9-7ed3-533c-bda7-3130693cf4d2" ``` @@ -152,6 +178,43 @@ Acceptance: private repositories derive; a hub without the credential still derives public ones; no credential appears in logs, process listings, or recorded clone URLs. +**Code landed 2026-08-27**, ahead of T02 — writing the consumer does not need +the secret to exist. `api/services/forge_projection.py`: + +- `forge_read_token()` reads `FORGE_READ_TOKEN_FILE` in preference to + `FORGE_READ_TOKEN`. A mounted file is what lets T02 satisfy "rotation must + not require a redeploy"; the environment cannot be rotated in place. An + unreadable token *file* returns `None` rather than falling back to the + environment — falling back would let a broken mount look like success while + quietly using a stale value. +- Absent is a supported state, not a degraded one: with no credential the git + environment is untouched and public derivation runs exactly as before. +- The credential is passed as `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_0`/ + `GIT_CONFIG_VALUE_0` setting `http.extraHeader`, not as `-c` on the command + line and not as userinfo in the URL. Both alternatives put the token in the + process listing, where anything that can run `ps` reads it. +- `_run_git` redacts the token from `ForgeDeriveError` before raising, because + that message is logged, stored in a reset outcome, and returned over the API. +- `api/services/forge_credential.py` resolves the credential from a mounted + file, an environment variable, or OpenBao via Kubernetes auth, in that order. + Production uses the third; the first two make the code runnable and testable + outside the cluster. Configuring a file means *that* file and nothing else — + no fall-through, so a broken mount cannot silently resolve to a stale value. +- Every failure resolves to `None`, never an exception: no configuration, no + network, OpenBao down, permission denied. Raising would convert "nine + repositories are unreadable" into "the whole pass failed", which is the + outcome T01 exists to prevent. +- The resolved value is cached for 5 minutes — a fleet reset of 121 + repositories must not authenticate 121 times, and a rotated token must still + be picked up without a redeploy. + +Nine tests in `TestForgeCredential` cover each of those, including the two that +are silent when wrong: token-in-argv and token-in-exception. 43 pass in +`tests/test_forge_projection.py`; 717 pass across the suite. + +Remains `progress` because the acceptance clause "private repositories derive" +cannot be observed until T02 supplies a credential; T04 confirms it. + ## Confirm the nine ```task