diff --git a/Dockerfile b/Dockerfile index d1e1ae9..e43201e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,19 @@ COPY pyproject.toml uv.lock ./ COPY src/ ./src/ RUN uv sync --no-dev --frozen +# Repo Manager CLI for the bounded operation repo_manager_freshness +# (RMGR-WP-0020-T04, ACTIVITY-WP-0044). Isolated venv from repo-manager's own +# lock at the founder-reviewed commit; git verifies the commit identity. Git is +# needed only in this build stage and does not enter the runtime image. +ARG REPO_MANAGER_COMMIT=e77f8b457647ff23697e45a74615edde5f42d39d +RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/* \ + && git init -q /opt/repo-manager \ + && git -C /opt/repo-manager fetch -q --depth 1 https://forgejo.coulomb.social/coulomb/repo-manager.git "${REPO_MANAGER_COMMIT}" \ + && git -C /opt/repo-manager checkout -q --detach FETCH_HEAD \ + && test "$(git -C /opt/repo-manager rev-parse HEAD)" = "${REPO_MANAGER_COMMIT}" \ + && cd /opt/repo-manager && uv sync --no-dev --frozen --no-editable \ + && /opt/repo-manager/.venv/bin/rmgr freshness run --help > /dev/null + # Isolated CI checks; development dependencies do not enter the runtime image. FROM builder AS test # Real-Git transport fixtures require Git only in the isolated test image. @@ -23,6 +36,7 @@ FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1 WORKDIR /app COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/src /app/src +COPY --from=builder /opt/repo-manager/.venv /opt/repo-manager/.venv COPY alembic.ini ./ COPY migrations/ ./migrations/ COPY scripts/ ./scripts/ @@ -31,4 +45,5 @@ COPY event-types/ ./event-types/ COPY tasks/ ./tasks/ ENV PATH="/app/.venv/bin:$PATH" ENV PYTHONPATH="/app/src" +ENV ACTIVITY_CORE_RMGR_BIN="/opt/repo-manager/.venv/bin/rmgr" CMD ["python", "-m", "activity_core.worker"] diff --git a/docs/rollout-owner-return-and-freshness-worker.md b/docs/rollout-owner-return-and-freshness-worker.md new file mode 100644 index 0000000..0c8d69b --- /dev/null +++ b/docs/rollout-owner-return-and-freshness-worker.md @@ -0,0 +1,538 @@ +# Rollout package: owner-return pickup + bounded freshness in the worker + +> **REQUIRES FOUNDER EXECUTION.** Prepared, not executed. The founder runs every +> Argo CD, kubectl, secret and trigger step in an attended terminal. Coding +> agents cannot run production deploys here. While preparing this package: +> no Argo sync, no `kubectl apply/patch/create/exec/cp`, no secret read, no +> automation trigger. Agents only ran read-only `kubectl get`, `helm list`, +> `docker buildx imagetools inspect` and a local image build. + +Prepared 2026-10-04 from activity-core `main` (`b6222af` + this package commit). +Authorization: informed-decision batch `docs/batches/2026-10-04-state-hub-wp0094`, +all four memos accepted 2026-10-04. This package covers: + +- **memo 2** `owner-return-pickup-plan-b6222af`: plan approved, plus one + operator-triggered trial for one public repository, first `submit: false`, + then once `submit: true`. +- **memo 3** `freshness-runner-deploy-trial-e77f8b4`: deploy repo-manager + `e77f8b457647ff23697e45a74615edde5f42d39d` into the worker, then dry runs and + at most three `--limit 3` apply fires. + +It does **not** cover memo 4 (schedule enable / sweep retirement). Both +definitions stay `enabled: false` throughout, and no schedule is created. + +The shape follows `state-hub/docs/rollout-dependency-edge-receiving.md`. +**Activity Core is not deployed by Helm.** `helm list -n activity-core` is +empty. Activity Core is deployed by **Argo CD** with manual sync: +application `argocd/activity-core`, source `k8s/gitops`, pinned `targetRevision`. +That is why every "Helm values diff" below is a manifest diff in +`k8s/railiance/20-runtime.yaml`. You regenerate it into `k8s/gitops/runtime.yaml` +with `scripts/render_gitops.py`, and the deploy step is an Argo CD revision +bump plus sync. See `docs/gitops-release.md`. + +## 1. Live state (read-only, observed 2026-10-04) + +| Item | Value | +| --- | --- | +| Deployer | Argo CD app `activity-core`, `Synced/Healthy`, `targetRevision` **`ce9a8f8edf4ec162bae3aa087803bd57a1b33a66`**, last operation 2026-09-28T10:19:42Z, manual sync (no `automated`), options `ApplyOutOfSyncOnly, PruneLast, FailOnSharedResource` | +| Managed set | 9 resources: Deployments `actcore-api`, `actcore-worker`, `actcore-event-router`; Services; ConfigMaps `actcore-runtime-config`, `actcore-external-activity-definitions`, `actcore-report-schemas`, `actcore-ops-service-inventory`. PVCs, ExternalSecrets, Temporal, NATS, DBs and the edge relay are **outside** Argo | +| Worker | `deploy/actcore-worker` (rev 64), 1/1, image `activity-core@sha256:75e240ac…4d3886` (built locally 2026-09-28 12:08 CEST, no revision label, pinned since `eda44d9`), runs as uid/gid 1000 with fsGroup 1000, RollingUpdate 25%/25% | +| API | `actcore-api`, image `@sha256:713bddad…3ccd` (built 2026-09-23, no revision label) | +| Event router | `@sha256:cd4e924c…42f4`, not touched by this rollout | +| Bounded operations | Temporal activity `execute_bounded_operation`, registered by `activity_core.worker` on `orchestrator-tq`. It runs in **`actcore-worker`** | +| Ops-run queue | `OPS_RUN_QUEUE_ENABLED=true` (ConfigMap `actcore-runtime-config`), lease 900 s, max attempts 3 | +| State Hub URL in worker | `STATE_HUB_URL=http://actcore-statehub-edge-relay:8000`. The relay forwards every GET upstream and caches only allowlisted GETs, so owner-return `/decisions/{id}` reads fail closed (unknown means stop) | +| State Hub primary | `svc/state-hub` in ns `state-hub`, ClusterIP `10.43.68.154:8000`, revision 73 (memo 1 deployed). No NetworkPolicy exists in `state-hub` | +| ESO | ClusterSecretStore `openbao-activity-core`. Five ExternalSecrets are synced (`actcore-forgejo-admin`, `actcore-issue-core-runtime`, …). None holds a read-only repo-manager Forgejo token | +| Existing PVCs | `actcore-statehub-edge-outbox`, `actcore-working-memory` (`local-path`, RWO, 1Gi). No receipt or report volume | + +The live worker and API images predate `5a90124`, so **neither operation is +live**. Their exact source commit cannot be proven because they were built +locally without a revision label. Promoting a CI image of `main` therefore +ships every source change since then. `git log ce9a8f8..HEAD -- src migrations` +lists 7 commits: `7b38f05`, `dcb5f7d`, `5a90124`, `c6df768`, `ba4ef00`, +`b2c6c56`, `f89ebe9`. **No migration changes.** + +## 2. Image + +### How `rmgr` gets into the worker (decision, implemented in this commit) + +`rmgr` is installed in the **builder stage** into its own virtualenv, +`/opt/repo-manager/.venv`. It is built from **repo-manager's own `uv.lock`** +at the reviewed commit, and the venv is copied into the runtime image. +`ACTIVITY_CORE_RMGR_BIN=/opt/repo-manager/.venv/bin/rmgr` is set in the image, +and the code-fixed argv in `context_resolvers/repo_manager.py` already honours it. + +- The commit is pinned by `ARG REPO_MANAGER_COMMIT=e77f8b45…d39d`. The build + fetches that SHA from `https://forgejo.coulomb.social/coulomb/repo-manager.git` + (public, anonymous) and asserts `git rev-parse HEAD` equals it. Git's content + addressing is the identity check. +- `uv sync --no-dev --frozen --no-editable` installs from the lock, so the + install is reproducible. The resulting venv does not need the source tree. +- The build stage runs `rmgr freshness run --help`, so a broken install fails CI. +- Git is installed only in the builder stage. The runtime image has no git, + which was verified. +- The dependency sets stay isolated. Activity Core's lock is unchanged, so + there is no resolver coupling between the two projects. +- `tests/test_gitops_release.py::test_repo_manager_cli_is_pinned_and_isolated` + asserts the pin, the identity check, the venv copy and the env var. + +Alternatives rejected: + +- **Git or URL dependency in Activity Core's `pyproject`.** It couples the two + locks and needs git in the builder anyway. +- **Separate image or sidecar.** The operation calls a subprocess in the worker + container, so a sidecar would need new code. + +Local verification: `docker build --target runtime .` succeeded. In the +container, `$ACTIVITY_CORE_RMGR_BIN freshness run --help` works and +`build_freshness_argv({"limit":3,"max_seconds":240,"apply":False})` returns +`['/opt/repo-manager/.venv/bin/rmgr','freshness','run','--limit','3','--max-seconds','240','--dry-run']`. +The focused tests passed: `tests/test_gitops_release.py` and +`tests/test_repo_manager_freshness_definition.py`, 34 tests. + +### CI image + +The image workflow (`.forgejo/workflows/image.yaml`) builds on pushes to `main` +that touch `Dockerfile`, `src/**`, `tests/**` and similar paths. It runs the +`test` stage first and publishes only `git-` and its +digest. + +Existing CI images, verified read-only with `docker buildx imagetools inspect`: + +| Tag | Digest | Has both ops | Has rmgr | +| --- | --- | --- | --- | +| `git-f89ebe9c821d8c984c1974b712c17a730a497ab4` | `sha256:ce7e585d46d8aa33a466f699f724102d8b54d914d8c042a9f1c26eba4db1c113` | yes | **no** | +| `git-b6222af…` | none (docs-only push) | - | - | + +**Required image:** the push of this package commit, made by +`statehub fix-consistency --repo activity-core`, builds +`git-`. That head may be a later `chore(consistency)` commit on +top of it. Founder steps: + +```bash +cd ~/activity-core && git fetch && HEAD_SHA=$(git rev-parse origin/main) +git merge-base --is-ancestor "$HEAD_SHA" && echo contains-package +docker buildx imagetools inspect forgejo.coulomb.social/coulomb/activity-core:git-$HEAD_SHA +# record DIGEST=sha256:... (deploy by digest only, never by tag) +``` + +If no image exists for that head (for example, the build was skipped), run the +`image.yaml` workflow with `workflow_dispatch` on `main` in Forgejo. + +Retention: protect the alias for both the new digest and the rollback digests +`75e240ac…` (worker) and `713bddad…` (API), as `gitops-release.md` requires. + +## 3. Runtime config (manifest diff, NOT applied) + +These changes are outside the `ACTIVITY-WP-0041-image-only-v1` routine +authority because they touch env, mounts and a new volume. They are covered by +memos 2 and 3 as an owner-reviewed change. `check_gitops_promotion.py` will +correctly refuse them as "widened", so do not use the bounded promotion path. + +### 3a. Objects outside Argo (founder applies with kubectl) + +```yaml +# PVC for freshness receipts + operator-supplied C-25 reports +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: actcore-ops-artefacts + namespace: activity-core + labels: {app.kubernetes.io/name: activity-core, app.kubernetes.io/part-of: activity-core} +spec: + accessModes: [ReadWriteOnce] + storageClassName: local-path + resources: {requests: {storage: 1Gi}} +``` + +The Forgejo token is a **blocker**; see 3c. Once its route exists, add an +ExternalSecret `actcore-repo-manager-forgejo-read` modelled on +`k8s/railiance/15-externalsecret-forgejo-admin.yaml`. Use store +`openbao-activity-core`, a **separate** target Secret, and one key +`FORGEJO_READ_TOKEN`. It must not merge into `actcore-runtime-secret`, so the +token is never in the worker's env. + +### 3b. Worker Deployment diff (`k8s/railiance/20-runtime.yaml`, then `render_gitops.py`) + +```diff + # Deployment actcore-worker, container worker + env: + - name: ISSUE_SINK_TYPE + value: state-hub + - name: KUBECONFIG_R01 + value: /kube/config-hosteurope + - name: KUBECONFIG_CORE + value: /kube/config ++ # repo-manager freshness runner (memo 3) ++ - name: STATE_HUB_API_BASE ++ value: http://state-hub.state-hub.svc.cluster.local:8000 ++ - name: RM_FRESHNESS_RECEIPTS_PATH ++ value: /var/lib/activity-core/ops/freshness/receipts.jsonl ++ - name: REPO_MANAGER_FORGEJO_TOKEN_FILE # add ONLY once 3c is resolved ++ value: /var/run/secrets/repo-manager/FORGEJO_READ_TOKEN ++ # ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED: deliberately absent (memo 2 plan-only first) +- image: forgejo.coulomb.social/coulomb/activity-core@sha256:75e240ac23c1c6d7daae14691c2b4137d2d639a9af694aaf0d4c774a7e4d3886 ++ image: forgejo.coulomb.social/coulomb/activity-core@ + volumeMounts: + ... + - mountPath: /kube + name: kubeconfigs + readOnly: true ++ - mountPath: /var/lib/activity-core/ops/freshness ++ name: ops-artefacts ++ subPath: freshness ++ - mountPath: /etc/activity-core/owner-return ++ name: ops-artefacts ++ subPath: owner-return ++ - mountPath: /var/run/secrets/repo-manager # only once 3c is resolved ++ name: repo-manager-forgejo-read ++ readOnly: true + volumes: + ... ++ - name: ops-artefacts ++ persistentVolumeClaim: ++ claimName: actcore-ops-artefacts ++ - name: repo-manager-forgejo-read # only once 3c is resolved ++ secret: ++ secretName: actcore-repo-manager-forgejo-read ++ defaultMode: 0440 +``` + +```diff + # Deployment actcore-api, container api (the parser must know both operations for /admin/sync) +- image: forgejo.coulomb.social/coulomb/activity-core@sha256:713bddad10a41950f446100a8b370fca8ccdd0b8969cccae751e3870c9c63ccd ++ image: forgejo.coulomb.social/coulomb/activity-core@ +``` + +Notes: + +- **`STATE_HUB_API_BASE`.** Use the service DNS name rather than the edge relay. + rmgr verifies `instance_role=primary` and label `railiance01`, and it must + talk to the primary directly. `http://10.43.68.154:8000` is equivalent but + breaks if the Service is recreated. +- **Worker `STATE_HUB_URL`.** It stays on the edge relay, which is used by + owner-return reads and existing callers. The relay passes GETs through and + fails closed on upstream errors. +- **Report file.** It is resolved as `activity-core://owner-return/`, + which maps to `/etc/activity-core/owner-return/` because + `ACTIVITY_CORE_ROOT=/etc/activity-core`. +- **Receipt ownership.** The PVC subPaths are created by kubelet. The pod's + fsGroup 1000 and local-path's 0777 dirs let uid 1000 write receipts. Check + this in the post-checks. +- **Definitions.** `owner-return-pickup` and `repo-manager-bounded-freshness` + stay `enabled: false`, and the image carries them that way. + +### 3c. Forgejo read token: route (recorded, no value handled) + +`warden route find "forgejo read token for repo-manager freshness runner" --json` +was run 2026-10-04. **No entry exists for the Activity Core worker or for +repo-manager freshness.** The closest matches: + +1. `state-hub-forge-derivation-read` (owner ops-mason, OpenBao + Forgejo, + active, high risk). This is an organisation-wide repository-read token at + `platform/workloads/state-hub/forge-derivation` field `FORGE_READ_TOKEN`. + Its Kubernetes auth role binds **ServiceAccount `state-hub/state-hub` + only**, so it is not routable to `activity-core`. +2. `policy-nexus-forgejo-source-read` (railiance-platform), and + `core-hub repo-manager publisher` (`platform/workloads/core-hub/forgejo-source-read`). + Both are bound to other namespaces. + +The required action is a credential change request in `railiance-platform`, +following `CCR-2026-0014`. It must add a read-only org-wide repository-read +token at a new path, for example +`platform/workloads/activity-core/repo-manager-forgejo-read`, and grant the +`openbao-activity-core` store role read on it. Then register a warden catalog +route. The minting and the `paste_once_provision` delivery are founder steps. + +**Interim option (founder choice):** deploy without the token lines, so Forge +reads are anonymous. Dry runs will then show private repositories as forge +fetch failures. Do **not** run apply fires in that mode: the private-repo +refusals would fall under the packet's "unexplained refusal" stop condition. + +## 4. Deploy (founder, attended) + +### 0. Preconditions + +```bash +kubectl -n argocd get application activity-core -o jsonpath='{.spec.source.targetRevision} {.status.sync.status} {.status.health.status}{"\n"}' +# expect ce9a8f8edf4ec162bae3aa087803bd57a1b33a66 Synced Healthy -> record PREV_REV +kubectl -n activity-core get deploy actcore-worker actcore-api -o jsonpath='{range .items[*]}{.metadata.name}={.spec.template.spec.containers[0].image}{"\n"}{end}' +# record PREV_WORKER_IMAGE (75e240ac…), PREV_API_IMAGE (713bddad…) +kubectl -n activity-core get pods --field-selector=status.phase!=Running # stop on unrelated pending pods +curl -s http://10.43.68.154:8000/state/health # primary answers +``` + +### 1. Out-of-Argo objects + +```bash +kubectl apply -f actcore-ops-artefacts-pvc.yaml # 3a +# (when 3c is resolved) kubectl apply -f actcore-repo-manager-forgejo-read-externalsecret.yaml +kubectl -n activity-core get pvc actcore-ops-artefacts # Pending until first consumer (WaitForFirstConsumer) is OK +# kubectl -n activity-core get externalsecret actcore-repo-manager-forgejo-read # SecretSynced/True +``` + +### 2. Manifest commit (agent or founder, no deploy effect) + +Apply diff 3b with ``, then run: + +```bash +uv run python scripts/render_gitops.py # regenerate k8s/gitops/runtime.yaml +uv run python scripts/render_gitops.py --check --verify-frontend --verify-platform +git commit -am "Promote worker+api to ; rmgr receipts volume and env (memo 3)" +statehub fix-consistency --repo activity-core # sanctioned push +NEW_REV=$(git rev-parse origin/main) +``` + +### 3. Server dry run (expected diff) + +```bash +argocd app diff activity-core --revision "$NEW_REV" +# or without the argocd CLI: +git -C ~/activity-core show "$NEW_REV":k8s/gitops/runtime.yaml > /tmp/ac-new.yaml +kubectl diff -f /tmp/ac-new.yaml +kubectl apply -f /tmp/ac-new.yaml --dry-run=server -o name +``` + +Expected diff vs. live, and nothing else: + +- `actcore-worker`: the image digest, 2–3 env entries, 2–3 volumeMounts and + 1–2 volumes. +- `actcore-api`: the image digest. + +The `ConfigMap`s and `actcore-event-router` show no change. Any other change +stops the rollout. Note that `kubectl diff` may also show server-managed field +noise; ignore that only if it is identical against `PREV_REV`. + +### 4. Upgrade (the authorized action) + +```bash +kubectl -n argocd patch application activity-core --type merge \ + -p "{\"spec\":{\"source\":{\"targetRevision\":\"$NEW_REV\"}}}" +argocd app sync activity-core --prune=false --timeout 300 \ + || kubectl -n argocd patch application activity-core --type merge -p '{"operation":{"sync":{"prune":false,"revision":"'"$NEW_REV"'"}}}' +kubectl -n activity-core rollout status deploy/actcore-worker --timeout=180s +kubectl -n activity-core rollout status deploy/actcore-api --timeout=180s +``` + +There is no `--atomic` equivalent. If either rollout does not become Ready +within 180 s, roll back immediately; see step 7. + +### 5. Post-checks + +```bash +kubectl -n argocd get application activity-core -o jsonpath='{.status.sync.revision} {.status.sync.status} {.status.health.status}{"\n"}' # NEW_REV Synced Healthy +kubectl -n activity-core get pods -l app.kubernetes.io/name=actcore-worker -o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}' # == DIGEST +kubectl -n activity-core exec deploy/actcore-worker -- sh -c '$ACTIVITY_CORE_RMGR_BIN freshness run --help >/dev/null && echo rmgr-ok; env | grep -E "^(STATE_HUB_API_BASE|RM_FRESHNESS_RECEIPTS_PATH|REPO_MANAGER_FORGEJO_TOKEN_FILE|ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED)=" | sed "s/=.*TOKEN.*/=/"; touch /var/lib/activity-core/ops/freshness/.w && rm /var/lib/activity-core/ops/freshness/.w && echo receipts-writable; ls -ld /etc/activity-core/owner-return' +# expect: rmgr-ok; ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED absent; receipts-writable +kubectl -n activity-core exec deploy/actcore-worker -- python -c "import urllib.request,json;print(json.load(urllib.request.urlopen('http://state-hub.state-hub.svc.cluster.local:8000/state/health')))" +kubectl -n activity-core port-forward svc/actcore-api 8010:8010 & +curl -s http://127.0.0.1:8010/health +curl -s -X POST 'http://127.0.0.1:8010/admin/sync?definitions=true&schedules=true' # errors[] empty +curl -s http://127.0.0.1:8010/ops/automations | python3 -c "import json,sys;[print(a.get('id'),a.get('name'),a.get('enabled')) for a in (json.load(sys.stdin).get('automations') or []) if a.get('id') in ('3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a','d7addc37-4cfe-550c-98e4-f54e4bd4e8ec')]" +# both present, enabled False; no Temporal schedule active for either +``` + +The definition ids are deterministic: + +- `owner-return-pickup` is `3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a`. +- `repo-manager-bounded-freshness` is `d7addc37-4cfe-550c-98e4-f54e4bd4e8ec`. + +### 6. Evidence + +Write `docs/evidence/2026-10-XX-owner-return-freshness-worker-rollout.json` +in the shape of state-hub's `2026-10-04-edge-receiving-rollout.json`. Include: + +- authorization: the memo ids and approval ids from the review store +- `previous_revision`, `previous_images` and `revision` +- image tag and digest +- dry-run diff summary and post-checks +- `executed_by` + +### 7. Rollback + +```bash +kubectl -n argocd patch application activity-core --type merge \ + -p '{"spec":{"source":{"targetRevision":"ce9a8f8edf4ec162bae3aa087803bd57a1b33a66"}}}' +argocd app sync activity-core --prune=false +``` + +This restores digests `75e240ac…` and `713bddad…` and removes the env and +mounts. Keep the PVC and its receipts; never delete them. Projections are +idempotent per commit, so there is no data rollback. Open owner-return ops +runs are closed through the normal ops-run close path, never deleted. + +## 5. Trial runbooks + +### How trial params reach the operation + +`POST /ops/automations/{id}/trigger` accepts only `confirm_side_effect`. The +parameters come from the synced definition row. Every trial parameter set (the +report sha256, `submit`, `limit`, `apply`) is therefore a **reviewed definition +change**. Make it as a ConfigMap-projected copy of the definition with the same +`id`, in `actcore-external-activity-definitions`: +`k8s/railiance/20-runtime.yaml` regenerated into `k8s/gitops`. + +`/admin/sync` scans `/app/activity-definitions` (the image) **before** +`ACTIVITY_DEFINITION_DIRS`. It upserts in that order inside one transaction +(`sync_activity_definitions.sync`), so the projected copy deterministically +overrides the baked one. + +Each trial step is: + +1. Commit the projection. +2. Push with `fix-consistency`. +3. Bump and sync the Argo revision (a ConfigMap-only diff). +4. Wait about 60 s for the volume refresh. +5. Run `/admin/sync?definitions=true&schedules=true`. +6. Trigger. + +Keep `enabled: false` in every copy. Manual trigger does not require +`enabled: true`. Remove the copies after the trials, which restores the baked +definitions. + +### 5A. Memo 2: owner-return pickup, plan-only (`submit: false`) + +1. **Choose one public repository.** Anonymous Forge reads must succeed, for + example `git ls-remote https://forgejo.coulomb.social/coulomb/.git` + without credentials. It should have at least one C-25 owner-return finding. + `state-hub` is public and is the natural first target. +2. **Produce the report on a clean checkout.** The checker must be state-hub + `150cb0b` or later, and the run is read-only (no `--fix`): + ```bash + cd ~/state-hub && git merge-base --is-ancestor 150cb0b HEAD + cd ~/ && git status --porcelain workplans/ | wc -l # must be 0 + git fetch && test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" # HEAD must be Forge default-branch HEAD + cd ~/state-hub && python3 scripts/consistency_check.py --repo --json > /tmp/-c25.json + python3 -c "import json;r=json.load(open('/tmp/-c25.json'));print(r.get('source_revision'),r.get('result'))" # full 40-hex, not error + SHA256=$(sha256sum /tmp/-c25.json | cut -d' ' -f1) + ``` + Stop if `source_revision` is null (`source_revision_reason`), or if the text + output has a C-25 "reply collection … is incomplete" finding. +3. **Copy the file into the mount** (founder; this is a kubectl write): + ```bash + kubectl -n activity-core cp /tmp/-c25.json "$(kubectl -n activity-core get pod -l app.kubernetes.io/name=actcore-worker -o name | cut -d/ -f2)":/etc/activity-core/owner-return/-c25.json + kubectl -n activity-core exec deploy/actcore-worker -- sha256sum /etc/activity-core/owner-return/-c25.json # == $SHA256 + ``` +4. **Project the definition copy** (`owner-return-pickup.md`, `enabled: false`) + with this context source, following the steps above: + ```yaml + context_sources: + - type: state-hub-owner-return + query: pickup + operation: owner_return_pickup + required: true + bind_to: context.owner_return + params: + target_repo: + report_file: activity-core://owner-return/-c25.json + report_sha256: + submit: false + expected_source_revision: + predecessor_repos: {"STATE-WP-": state-hub, "ACTIVITY-WP-": activity-core, "RMGR-WP-": repo-manager} + ``` +5. **Trigger** (founder, with SSO or an operator token held by the founder): + ```bash + curl -sS -X POST http://127.0.0.1:8010/ops/automations/3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a/trigger \ + -H 'Content-Type: application/json' -H "X-Operator-Token: $OP_TOKEN" \ + -d '{"confirm_side_effect": true}' + curl -s http://127.0.0.1:8010/ops/automations/3ca6e5aa-dbfc-5618-80bb-0b6d1e59b86a/runs | python3 -m json.tool + ``` +6. **Evidence.** The result is bound to `context.owner_return` and holds IDs and + counts only: + - `mode: plan-only`, report revision and `report_sha256` + - `current_tasks` revision and count, and HEAD verified + - planner dispositions and stop reasons + - candidate IDs, with `submitted_count: 0` + - `select count(*) from ops_runs where source_type='state_hub_owner_return'` + unchanged + + Store it as `docs/evidence/ACTIVITY-WP-0043-plan-only-.md`, together + with a hub progress event. **The founder reviews this evidence before 5B.** + +### 5B. Memo 2: `submit: true` and duplicate re-run + +1. Preconditions: + - 5A was reviewed. + - The repo HEAD is unchanged. If it changed, regenerate the report (step 2) + and re-pin it. +2. Set the gate. This is a worker env change through the same manifest path + (3b plus Argo sync, which restarts the worker): + `ACTIVITY_CORE_OWNER_RETURN_PICKUP_ENABLED=true`. The pod restart keeps the + PVC report. +3. Project the definition copy with `submit: true` and otherwise identical + params. Sync and trigger as in 5A.5. +4. Evidence: receipts with `triggering_event_id`, `idempotency_key`, + `status=inserted`, `ops_run_id` and `state`. Also the new + `state_hub_owner_return` rows. +5. **Duplicate re-run.** Trigger once more with an unchanged definition and + report. Expect every prior `inserted` to become `duplicate`, and the ops-run + count for `state_hub_owner_return` to be unchanged. +6. Unset the flag again (step 2 in reverse) unless the founder decides + otherwise. Close the trial rows through the normal ops-run close path. Write + `docs/evidence/ACTIVITY-WP-0043-submit-trial-.md` and a hub progress + event. + +Stop and roll back (unset the flag) on any of these: + +- a write outside the expected rows +- a cleared wait, status or human gate +- a message body in the evidence +- an insert on the re-run + +### 5C. Memo 3: freshness dry run, then up to three `--limit 3` apply fires + +1. Project a `repo-manager-bounded-freshness.md` copy (`enabled: false`) with + params `{limit: 3, max_seconds: 240, apply: false}`. Sync, then trigger + `d7addc37-4cfe-550c-98e4-f54e4bd4e8ec` with `confirm_side_effect: true`. + The resulting argv is + `rmgr freshness run --limit 3 --max-seconds 240 --dry-run`. + + Check: + - the hub verified as primary `railiance01` + - the planned repositories + - no receipts written (the receipt-file line count is unchanged) +2. Change the projection to `apply: true` (still `limit: 3`). Then fire at most + **three** times, one at a time. After each fire, review: + ```bash + curl -s http://127.0.0.1:8010/ops/automations/d7addc37-4cfe-550c-98e4-f54e4bd4e8ec/runs | python3 -m json.tool # counts per outcome, exit_code + kubectl -n activity-core exec deploy/actcore-worker -- sh -c 'wc -l < $RM_FRESHNESS_RECEIPTS_PATH; tail -n 3 $RM_FRESHNESS_RECEIPTS_PATH' + kubectl -n activity-core exec deploy/actcore-worker -- /opt/repo-manager/.venv/bin/rmgr freshness receipts --latest + ``` +3. **Stop conditions** (from the packet). Stop firing and remove the projection + on any of these: + - any receipt from an unverified primary + - any checkout read + - a fire exceeding `max_seconds` twice + - any refusal not explained by known holes: + - RMGR-WP-0019-T03 duplicate projections + - RMGR-WP-0014/0015 refusals + - without the token (3c), private-repository fetch failures +4. **Evidence per fire:** + - fire id and Temporal workflow id + - start, end and duration + - outcome counts (`applied`, `noop`, `queued`, `failed`, `refused`, + `skipped`) and exit code + - receipt lines added, each with repo, Forge commit, outcome and hub + `instance_role`/label + - a refusal-reason classification + + Store it under repo-manager `RMGR-WP-0020-T04` evidence and an Activity Core + `ACTIVITY-WP-0044` note, with a hub progress event. +5. **After the trials,** remove both projected copies, sync and run + `/admin/sync`. The baked definitions (`enabled: false`) return. Schedules + stay off until memo 4's own runbook. + +## Blockers and open points + +1. **Forgejo read token route missing** (3c). This needs a railiance-platform + CCR and a warden catalog entry. Until then, run only dry runs, or apply + fires restricted to public-only outcomes, at the founder's choice. +2. **CI image.** It does not exist until this package is pushed; record the + digest from the pushed head. +3. **Image bump scope.** The live images have no revision label. The bump ships + all source since roughly 2026-09-23 (API) and 2026-09-28 (worker). There are + no migrations. +4. **Per-trial definition projection.** Each param change needs a commit and an + Argo sync, because the trigger takes no params. This is by design (the + reviewed params pin the report digest) but costs one sync per step. diff --git a/tests/test_gitops_release.py b/tests/test_gitops_release.py index 7770a26..2e12fab 100644 --- a/tests/test_gitops_release.py +++ b/tests/test_gitops_release.py @@ -85,3 +85,14 @@ def test_retention_projection_is_pinned_and_mounted(tmp_path): assert any(m.get('subPath')=='forgejo_package_prune.py' and m['readOnly'] is True for m in mounts) source.write_text('tampered') with pytest.raises(ValueError):renderer.verify_platform(source) + + +def test_repo_manager_cli_is_pinned_and_isolated(): + # RMGR-WP-0020-T04: rmgr enters the worker from repo-manager's own lock at the + # founder-reviewed commit, in its own venv, and is the code-fixed executable. + text=(ROOT/'Dockerfile').read_text() + assert 'ARG REPO_MANAGER_COMMIT=e77f8b457647ff23697e45a74615edde5f42d39d' in text + assert 'rev-parse HEAD)" = "${REPO_MANAGER_COMMIT}"' in text + assert 'uv sync --no-dev --frozen --no-editable' in text + assert 'COPY --from=builder /opt/repo-manager/.venv /opt/repo-manager/.venv' in text + assert 'ENV ACTIVITY_CORE_RMGR_BIN="/opt/repo-manager/.venv/bin/rmgr"' in text