Package rmgr into the worker image and prepare owner-return/freshness rollout (memo 2, memo 3)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 2m13s

Install repo-manager e77f8b4 from its own uv.lock into an isolated venv in
the builder stage (commit identity asserted via git), copy it into the
runtime image and set ACTIVITY_CORE_RMGR_BIN. Add a pin test and the
founder-executed rollout package (Argo CD manifest diff, PVC, token route
blocker, trial runbooks). Nothing deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 692292@bnt-lap001
Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
This commit is contained in:
tegwick 2026-10-04 15:52:10 +02:00
parent b6222af2cf
commit 34d1dc575c
3 changed files with 564 additions and 0 deletions

View file

@ -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"]

View file

@ -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-<full pushed head sha>` 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-<pushed head>`. 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 <package-commit> "$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@<DIGEST>
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@<DIGEST>
```
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/<file>`,
which maps to `/etc/activity-core/owner-return/<file>` 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 `<DIGEST>`, 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 <DIGEST>; 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.*/=<set>/"; 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/<repo>.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 ~/<repo> && 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 <repo> --json > /tmp/<repo>-c25.json
python3 -c "import json;r=json.load(open('/tmp/<repo>-c25.json'));print(r.get('source_revision'),r.get('result'))" # full 40-hex, not error
SHA256=$(sha256sum /tmp/<repo>-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/<repo>-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/<repo>-c25.json
kubectl -n activity-core exec deploy/actcore-worker -- sha256sum /etc/activity-core/owner-return/<repo>-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: <repo>
report_file: activity-core://owner-return/<repo>-c25.json
report_sha256: <SHA256>
submit: false
expected_source_revision: <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-<date>.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-<date>.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.

View file

@ -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