INFD-WP-0010-T02: release candidate for present_existing with compact-sitting v14
Candidate files: review policy pin v14 (sha256:8911458f...), the nk-bind-executor present_existing registry row (daily limit 10), and an 8081 ingress peer for exactly sso + app.kubernetes.io/name=bounded-executor. The state-hub row is unchanged and outcome_read stays off. The image pin stays at the live f5588f49 because no owner-CI full-service image exists yet; the founder runbook (deploy/lifecycle/RELEASE-INFD-WP-0010-T02.md) gates the release on it. Adds a side-effect-free present_existing probe and release tests. Nothing applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
3b7031ae39
commit
0ef57537ac
9 changed files with 457 additions and 6 deletions
|
|
@ -210,3 +210,13 @@ container checks pass. Both scans have zero HIGH/CRITICAL findings. Fresh
|
|||
schema-5 backup: 8ac7bbe79de7cdcff5ebc9fa8adc9c49498bf8a3298c8fbc9bd7501be1a379bb.
|
||||
Rollback, if needed: owner Helm prior revision 12 and Argo source dfcef87 together,
|
||||
retaining the current schema-5 database. Evidence: docs/evidence/2026-10-05-lifecycle-discovery/.
|
||||
|
||||
## Present-existing release candidate — prepared 2026-11-01 (not applied)
|
||||
|
||||
INFD-WP-0010-T02, together with access-engine compact-sitting v14. The candidate files carry the v14 review pin
|
||||
(`sha256:8911458f…fc300fde`), the `nk-bind-executor` present_existing registry row (daily limit 10) and an 8081
|
||||
ingress peer for exactly `sso` + `app.kubernetes.io/name=bounded-executor`. The state-hub row is unchanged and
|
||||
outcome_read stays off. **The image pin is still the live `f5588f49…`, because no owner-CI full-service image
|
||||
exists yet. Do not sync this candidate until it is replaced:** the live image rejects the new registry row,
|
||||
which would fail all intake. Founder runbook, verification and rollback:
|
||||
[RELEASE-INFD-WP-0010-T02.md](RELEASE-INFD-WP-0010-T02.md).
|
||||
|
|
|
|||
219
deploy/lifecycle/RELEASE-INFD-WP-0010-T02.md
Normal file
219
deploy/lifecycle/RELEASE-INFD-WP-0010-T02.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# Release runbook: present_existing for nk-bind-executor + compact-sitting v14 (INFD-WP-0010-T02)
|
||||
|
||||
Prepared 2026-11-01 by the informed-decision owner agent. **Nothing in this file has been applied.**
|
||||
Founder approval (2026-11-01, relayed by the net-kingdom coordinator): release present_existing together with
|
||||
access-engine compact-sitting v14. Every command below is founder-run, with
|
||||
`export KUBECONFIG=$HOME/.kube/config-railiance01`.
|
||||
|
||||
## Status: blocked on the service image (gate G0)
|
||||
|
||||
No CI image exists for current `main`. This repository has no Forgejo workflow that builds the full service
|
||||
image. Its two workflows build single-purpose overlays on fixed base digests:
|
||||
|
||||
- `.forgejo/workflows/purpose-overlay-image.yaml` swaps one module, `policy.py`, on base `7f5c6cb2…`, and
|
||||
checks it against the pinned hash `9d7c1be4…`. Commit `80ef840` changed `policy.py` (now `912ad3a3…`), so
|
||||
run 5 (head `e617bb5`, 2026-11-01T03:07Z) failed at that check. That is the intended behaviour. Even if the
|
||||
hash were updated, the overlay could not carry this release: it touches nine modules (`intake.py`,
|
||||
`intake_service.py`, `intake_approval.py`, `outcome.py` (new), `policy.py`, `requester.py`, `review.py`,
|
||||
`store.py`, `workload.py`).
|
||||
- `.forgejo/workflows/zlib-overlay-image.yaml` upgrades zlib only, on base `763c3640…`.
|
||||
|
||||
The registry has no tag for `3b7031a`, `main` or `latest`. `docker buildx imagetools inspect` returns
|
||||
not found for each.
|
||||
|
||||
Before this release can go out, an owner-CI full-image build from the release source must exist. It needs
|
||||
its own workflow, and it must keep zlib at `1.3.2-r1` or later. A plain rebuild of `Containerfile` uses the
|
||||
pinned `python:3.12-alpine@sha256:b64631e0…` base and only adds `libuuid`. That would reintroduce the zlib
|
||||
finding that the live image `f5588f49…` remediated. Then pin the image as in step 0.
|
||||
|
||||
**Do not sync this candidate with the current image.** The live image `f5588f49…` accepts only the four basic
|
||||
registry keys: `set(row) != {submitter_id, memo_classes, review_groups, daily_limit}`. It would reject the
|
||||
executor row, so every intake request would fail with 503 `intake_configuration_unavailable`, State Hub's
|
||||
included. It also does not send `approval_action`.
|
||||
|
||||
## Pins
|
||||
|
||||
| | Current, live (rollback target) | Candidate |
|
||||
| --- | --- | --- |
|
||||
| Argo `informed-decision` targetRevision | `312ab97e876e17341bf16f9e95397e14c2ac2720` | `<RELEASE_SHA>` (step 0) |
|
||||
| Service image | `…/informed-decision@sha256:f5588f496b1f267d07cdc9f5208e6f2f241f74b739c23924b03702dec13a5c23` | `<IMAGE_DIGEST>` (G0) |
|
||||
| Runtime ConfigMap | `informed-decision-runtime-6c221878d051` | `informed-decision-runtime-b488b96a0953` |
|
||||
| Review policy pin | compact-sitting `v13` `sha256:31be5985c0c1e6b0c3a039ed7310d1e94daf523a94f6d926e6e8d759d3711873` | `v14` `sha256:8911458f36f1a60598aa19592a2d752a74dffe2617fd86238407ac87bc300fde` |
|
||||
| Helm `flex-auth-informed-decision-sitting` (access-engine) | revision 13, image `6c6dc263…`, v13 policy path | access-engine's v14 release (`2de0f86`), prepared in parallel |
|
||||
| Lifecycle policy pin | `informed-decision.lifecycle` v2 `da01e045…` | unchanged |
|
||||
| Store schema | 5 | 5 (no migration) |
|
||||
|
||||
Candidate source changes in this commit:
|
||||
|
||||
- `admission.candidate.json`: the policy pin moves to v14, and the workload peer `{namespace: sso, app: bounded-executor}`
|
||||
is added. `service.candidate.json` is re-rendered with `tools/render_deployment.py`.
|
||||
- `informed-decision-review` NetworkPolicy: the 8081 ingress rule `from` now lists exactly two peers:
|
||||
`state-hub/app.kubernetes.io/name=state-hub` (unchanged) and `kubernetes.io/metadata.name=sso` AND
|
||||
`app.kubernetes.io/name=bounded-executor`. No other sso pod is admitted, and nothing new is admitted on 8080.
|
||||
- `intake-submitters.json` (projected into `registries.candidate.json`): new row
|
||||
`system:serviceaccount:sso:nk-bind-executor` → `nk-bind-executor`, memo class `bounded-execution`, review group
|
||||
`net-kingdom-admins`, `daily_limit: 10`, `present_existing {approval_actor: service:nk-bind-executor, actions:
|
||||
[bounded-execution]}`. The state-hub row is byte-identical, and `outcome_read` stays off (T05 waits for STATE-WP-0101).
|
||||
|
||||
## Step 0: pin the image (after G0; owner agent or founder)
|
||||
|
||||
```bash
|
||||
cd ~/informed-decision
|
||||
docker buildx imagetools inspect forgejo.coulomb.social/coulomb/informed-decision:<CI_TAG> # read the digest
|
||||
# set "image" in deploy/lifecycle/admission.candidate.json to forgejo.coulomb.social/coulomb/informed-decision@<IMAGE_DIGEST>
|
||||
python3 tools/render_deployment.py --input deploy/lifecycle/admission.candidate.json \
|
||||
--output deploy/lifecycle/service.candidate.json
|
||||
.venv/bin/python -m pytest -q # expect 0 failures
|
||||
python3 tools/smoke_container.py --image forgejo.coulomb.social/coulomb/informed-decision@<IMAGE_DIGEST> \
|
||||
--receipt docs/evidence/<date>-present-existing-release/container.json --workload
|
||||
git commit -am "INFD-WP-0010-T02: pin service image <IMAGE_DIGEST> for the v14 release" && git push
|
||||
RELEASE_SHA=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
Only the image line and the re-rendered `service.candidate.json` change. The runtime ConfigMap name stays
|
||||
`…b488b96a0953` because the image is not part of runtime.json.
|
||||
|
||||
## Step 1: preflight (read-only)
|
||||
|
||||
```bash
|
||||
export KUBECONFIG=$HOME/.kube/config-railiance01
|
||||
kubectl -n argocd get application informed-decision \
|
||||
-o jsonpath='{.spec.source.targetRevision} {.status.sync.status} {.status.health.status}{"\n"}'
|
||||
# expect 312ab97e876e17341bf16f9e95397e14c2ac2720, Healthy (OutOfSync is the known orphan ConfigMap
|
||||
# informed-decision-runtime-c81577741a09; prune stays off)
|
||||
kubectl -n informed-decision get deploy informed-decision -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
|
||||
helm -n flex-auth history flex-auth-informed-decision-sitting --max 3 # expect revision 13 deployed
|
||||
kubectl -n sso get sa nk-bind-executor
|
||||
kubectl -n informed-decision exec deploy/informed-decision -- \
|
||||
informed-decision-admin inspect --db /data/private/review.sqlite # record counts (schema 5)
|
||||
# Server dry-run of the exact release source (no persistence):
|
||||
git -C ~/informed-decision checkout --detach "$RELEASE_SHA"
|
||||
kubectl apply --dry-run=server -k ~/informed-decision/deploy/lifecycle
|
||||
kubectl diff -k ~/informed-decision/deploy/lifecycle # expect: image, runtime ConfigMap name/new ConfigMap,
|
||||
# registry ConfigMap, review NetworkPolicy (+ Argo annotations)
|
||||
git -C ~/informed-decision checkout main
|
||||
```
|
||||
|
||||
## Step 2: backup (schema is unchanged, but the release convention still requires it)
|
||||
|
||||
```bash
|
||||
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
kubectl -n informed-decision exec deploy/informed-decision -- \
|
||||
informed-decision-admin backup --db /data/private/review.sqlite --output /data/private/review-$STAMP-pre-v14.sqlite
|
||||
mkdir -p -m 0700 ~/informed-decision/.private-backups/$STAMP-pre-v14
|
||||
kubectl -n informed-decision exec deploy/informed-decision -- cat /data/private/review-$STAMP-pre-v14.sqlite \
|
||||
> ~/informed-decision/.private-backups/$STAMP-pre-v14/review.sqlite
|
||||
chmod 0600 ~/informed-decision/.private-backups/$STAMP-pre-v14/review.sqlite
|
||||
sha256sum ~/informed-decision/.private-backups/$STAMP-pre-v14/review.sqlite
|
||||
```
|
||||
|
||||
## Step 3: one window, access-engine first, then informed-decision
|
||||
|
||||
The service checks the PDP's provenance on every review call: `policy_package`, `policy_version`,
|
||||
`policy_package_digest` and `matched_policy_version` must all equal its pin. Between the two steps, the
|
||||
version mismatch makes human review reads and acts **fail closed**. No review is lost, but pages refuse.
|
||||
Workload intake (`POST /v1/memos`) does not consult the PDP, so State Hub posing continues throughout. Keep
|
||||
the gap to minutes.
|
||||
|
||||
**3a. access-engine v14** follows access-engine's own release runbook (FLEX-WP-0040-T07,
|
||||
`access-engine/docs/compact-sitting-v14-present-existing.md`, "Live steps"). It runs a Helm upgrade of
|
||||
`flex-auth-informed-decision-sitting` with `--atomic`: a new image built from `2de0f86` or later, and
|
||||
`--policy /opt/flex-auth/examples/informed-decision-present-existing-review/policy.md`. Check:
|
||||
|
||||
```bash
|
||||
helm -n flex-auth history flex-auth-informed-decision-sitting --max 2 # expect revision 14 deployed
|
||||
kubectl -n flex-auth rollout status deploy/flex-auth-informed-decision-sitting --timeout=180s
|
||||
```
|
||||
|
||||
If the `--atomic` upgrade rolls itself back, stop here. Do not run 3b. Nothing on the informed-decision side has changed.
|
||||
|
||||
**3b. informed-decision**, through the manually pinned Argo application:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd patch application informed-decision --type merge \
|
||||
-p "{\"spec\":{\"source\":{\"targetRevision\":\"$RELEASE_SHA\"}}}"
|
||||
kubectl -n argocd patch application informed-decision --type merge \
|
||||
-p "{\"operation\":{\"initiatedBy\":{\"username\":\"founder\"},\"sync\":{\"revision\":\"$RELEASE_SHA\",\"prune\":false,\"syncOptions\":[\"ApplyOutOfSyncOnly=true\"]}}}"
|
||||
kubectl -n argocd get application informed-decision \
|
||||
-o jsonpath='{.status.operationState.phase} {.status.sync.revision} {.status.health.status}{"\n"}'
|
||||
kubectl -n informed-decision rollout status deploy/informed-decision --timeout=300s # Recreate: short restart
|
||||
```
|
||||
|
||||
## Step 4: verification
|
||||
|
||||
Possible without the executor:
|
||||
|
||||
1. **Pins live.** Check the Deployment image and that its `configuration` volume names
|
||||
`informed-decision-runtime-b488b96a0953`:
|
||||
```bash
|
||||
kubectl -n informed-decision get deploy informed-decision \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image} {.spec.template.spec.volumes[?(@.name=="configuration")].configMap.name}{"\n"}'
|
||||
kubectl -n informed-decision get networkpolicy informed-decision-review -o jsonpath='{.spec.ingress[1].from}{"\n"}'
|
||||
# expect exactly the state-hub peer and the sso + app.kubernetes.io/name=bounded-executor peer
|
||||
```
|
||||
2. **Public surface.** `curl -s -o /dev/null -w '%{http_code}\n' https://decisions.coulomb.social/healthz` and
|
||||
`/readyz` return 200. `/v1/memos` and `/v1/decisions` on the public origin return 404.
|
||||
3. **v14 pin match (human, aal2).** Open `https://decisions.coulomb.social/` and one existing memo you were
|
||||
presented. The page loads only if the PDP's v14 provenance equals the new pin. A refusal mentioning policy
|
||||
or provenance means 3a and 3b disagree: roll back (step 5).
|
||||
4. **Store unchanged.** Run the `informed-decision-admin inspect` command from step 1 again. Schema 5, and
|
||||
the counts are no lower than the preflight.
|
||||
5. **Executor path, synthetic and side-effect free.** Run this from a temporary pod that carries the
|
||||
executor's identity and label. It is a founder-run kubectl write: a short-lived pod in sso, removed by
|
||||
`--rm`. The probe pins a deliberately wrong binding digest, so even a successful approval read ends in a
|
||||
409. No memo is stored, no budget is spent, and nothing is created, entered or consumed in approval-engine.
|
||||
```bash
|
||||
cd ~/informed-decision
|
||||
IMAGE=forgejo.coulomb.social/coulomb/informed-decision@<IMAGE_DIGEST>
|
||||
NAME=infd-probe-$(date -u +%H%M%S) # --overrides (merge patch) replaces the generated container: it carries name and image
|
||||
OVR='{"spec":{"serviceAccountName":"nk-bind-executor","automountServiceAccountToken":false,
|
||||
"securityContext":{"runAsNonRoot":true,"runAsUser":10001,"seccompProfile":{"type":"RuntimeDefault"}},
|
||||
"containers":[{"name":"'$NAME'","image":"'$IMAGE'","command":["python","-"],"stdin":true,"stdinOnce":true,
|
||||
"securityContext":{"allowPrivilegeEscalation":false,"readOnlyRootFilesystem":true,"capabilities":{"drop":["ALL"]}},
|
||||
"volumeMounts":[{"name":"intake","mountPath":"/var/run/secrets/informed-decision-intake","readOnly":true}]}],
|
||||
"volumes":[{"name":"intake","projected":{"sources":[{"serviceAccountToken":{"path":"token",
|
||||
"audience":"informed-decision-intake","expirationSeconds":600}}]}}]}}'
|
||||
.venv/bin/python tools/present_existing_probe.py --approval-id "$(python3 -c 'import uuid;print(uuid.uuid4())')" --program \
|
||||
| kubectl -n sso run "$NAME" --rm -i --restart=Never --image="$IMAGE" \
|
||||
--labels=app.kubernetes.io/name=bounded-executor,app.kubernetes.io/component=release-probe --overrides="$OVR"
|
||||
```
|
||||
Expected result for a random UUID: `503 {"error": "approval_unreadable"}`. Getting there proves the
|
||||
NetworkPolicy admits the sso executor label on 8081, TokenReview accepts `nk-bind-executor` with audience
|
||||
`informed-decision-intake`, the reloaded registry parses (so the state-hub row is also loaded), the
|
||||
`present_existing` profile and envelope validation pass, and the reservation is released.
|
||||
- If you know an approval id in approval-engine that no informed-decision memo has presented yet (for example
|
||||
one created during NK-WP-0056 executor testing), use it in place of the random UUID. Expect a 409 with an
|
||||
`approval_*` reason (`approval_digest_mismatch` or an earlier one). That proves the read through KeyCape
|
||||
`approval:read` as well.
|
||||
- Negative check: rerun with `"serviceAccountName":"default"` and expect `403 submitter_unknown`.
|
||||
- Other outcomes: `401 workload_authentication_failed` points to the token or audience. A timeout points to the
|
||||
NetworkPolicy (check the executor egress policy too). `503 intake_configuration_unavailable` means the
|
||||
registry did not parse: roll back.
|
||||
|
||||
Needs the executor (this is T02's own done-when): the executor creates an approval as `service:nk-bind-executor` and
|
||||
presents it, the founder accepts at aal2 on `/review`, and the executor consumes it. That is the
|
||||
NK-WP-0056 pilot run. It is not part of this release window.
|
||||
|
||||
## Step 5: rollback to the current release
|
||||
|
||||
The database is retained and stays on schema 5, so this is not a downgrade. Do both halves in the same window:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd patch application informed-decision --type merge \
|
||||
-p '{"spec":{"source":{"targetRevision":"312ab97e876e17341bf16f9e95397e14c2ac2720"}}}'
|
||||
kubectl -n argocd patch application informed-decision --type merge \
|
||||
-p '{"operation":{"initiatedBy":{"username":"founder"},"sync":{"revision":"312ab97e876e17341bf16f9e95397e14c2ac2720","prune":false,"syncOptions":["ApplyOutOfSyncOnly=true"]}}}'
|
||||
kubectl -n informed-decision rollout status deploy/informed-decision --timeout=300s
|
||||
helm -n flex-auth rollback flex-auth-informed-decision-sitting 13 --wait # access-engine's v13 release
|
||||
```
|
||||
|
||||
This restores image `f5588f49…`, runtime ConfigMap `…6c221878d051` (v13 pin), the registry without the executor
|
||||
row, and the NetworkPolicy without the sso peer. The new immutable ConfigMap `…b488b96a0953` is left in place
|
||||
because prune is off. It is harmless. Any present-existing memo posed during the window stays in the store but
|
||||
cannot be reviewed under v13 (`unknown_submitter`): it fails closed. Record it, and do not delete it.
|
||||
|
||||
## Step 6: record
|
||||
|
||||
Write `docs/evidence/<date>-present-existing-release/` (pins, Argo revision, Helm revision, backup sha256,
|
||||
probe outputs without tokens) and append a "Present-existing release" section to `deploy/lifecycle/README.md`.
|
||||
Then update INFD-WP-0010-T02.
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
"policy": {
|
||||
"origin": "http://flex-auth-informed-decision-sitting.flex-auth.svc.cluster.local:8080",
|
||||
"package": "informed-decision.compact-sitting",
|
||||
"version": "v13",
|
||||
"package_digest": "sha256:31be5985c0c1e6b0c3a039ed7310d1e94daf523a94f6d926e6e8d759d3711873",
|
||||
"version": "v14",
|
||||
"package_digest": "sha256:8911458f36f1a60598aa19592a2d752a74dffe2617fd86238407ac87bc300fde",
|
||||
"pod_name": "flex-auth-informed-decision-sitting"
|
||||
},
|
||||
"keycape_egress_ips": [
|
||||
|
|
@ -34,6 +34,10 @@
|
|||
{
|
||||
"namespace": "state-hub",
|
||||
"app": "state-hub"
|
||||
},
|
||||
{
|
||||
"namespace": "sso",
|
||||
"app": "bounded-executor"
|
||||
}
|
||||
],
|
||||
"api_egress": [
|
||||
|
|
|
|||
|
|
@ -8,5 +8,21 @@
|
|||
"net-kingdom-admins"
|
||||
],
|
||||
"daily_limit": 50
|
||||
},
|
||||
"system:serviceaccount:sso:nk-bind-executor": {
|
||||
"submitter_id": "nk-bind-executor",
|
||||
"memo_classes": [
|
||||
"bounded-execution"
|
||||
],
|
||||
"review_groups": [
|
||||
"net-kingdom-admins"
|
||||
],
|
||||
"daily_limit": 10,
|
||||
"present_existing": {
|
||||
"approval_actor": "service:nk-bind-executor",
|
||||
"actions": [
|
||||
"bounded-execution"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
},
|
||||
"data": {
|
||||
"lifecycle-callers.json": "{\n \"system:serviceaccount:state-hub:state-hub\": {\n \"tenant\": \"tenant:platform\"\n }\n}\n",
|
||||
"intake-submitters.json": "{\n \"system:serviceaccount:state-hub:state-hub\": {\n \"submitter_id\": \"state-hub\",\n \"memo_classes\": [\n \"decision-record\"\n ],\n \"review_groups\": [\n \"net-kingdom-admins\"\n ],\n \"daily_limit\": 50\n }\n}\n",
|
||||
"intake-submitters.json": "{\n \"system:serviceaccount:state-hub:state-hub\": {\n \"submitter_id\": \"state-hub\",\n \"memo_classes\": [\n \"decision-record\"\n ],\n \"review_groups\": [\n \"net-kingdom-admins\"\n ],\n \"daily_limit\": 50\n },\n \"system:serviceaccount:sso:nk-bind-executor\": {\n \"submitter_id\": \"nk-bind-executor\",\n \"memo_classes\": [\n \"bounded-execution\"\n ],\n \"review_groups\": [\n \"net-kingdom-admins\"\n ],\n \"daily_limit\": 10,\n \"present_existing\": {\n \"approval_actor\": \"service:nk-bind-executor\",\n \"actions\": [\n \"bounded-execution\"\n ]\n }\n }\n}\n",
|
||||
"review-groups.json": "{\n \"net-kingdom-admins\": {\n \"subject\": \"uid=platform-root,ou=people,dc=netkingdom,dc=local\",\n \"display_name\": \"Platform reviewer\"\n }\n}\n"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@
|
|||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "informed-decision-runtime-6c221878d051",
|
||||
"name": "informed-decision-runtime-b488b96a0953",
|
||||
"namespace": "informed-decision",
|
||||
"annotations": {
|
||||
"informed-decision.coulomb.social/admission": "review-only; INFD-WP-0001-T08"
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
},
|
||||
"immutable": true,
|
||||
"data": {
|
||||
"runtime.json": "{\"approval_origin\":\"http://approval-engine.approval-engine.svc.cluster.local:8080\",\"audit\":{\"origin\":\"http://audit-core.audit-core.svc.cluster.local:8080\",\"sender_token_file\":\"/var/run/secrets/informed-decision/audit/token\"},\"evidence_db\":\"/data/private/review.sqlite\",\"intake_attributes\":true,\"intake_delivery\":{\"hub_origin\":\"http://state-hub.state-hub.svc.cluster.local:8000\"},\"lifecycle_delivery\":true,\"lifecycle_policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-lifecycle.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.lifecycle\",\"package_digest\":\"sha256:da01e045fc108adec6b489e5d51f7e57366793016c5a64776963780a6aae457c\",\"version\":\"v2\"},\"ordinary_approval_ids\":[\"2ce76d7f-01c3-4b63-8476-8d1230679769\",\"c2af4bcf-15b4-4305-aac1-acc7e4dcb099\",\"dc22666a-d5d7-46bc-9490-ebe9fe09dc38\"],\"policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-sitting.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.compact-sitting\",\"package_digest\":\"sha256:31be5985c0c1e6b0c3a039ed7310d1e94daf523a94f6d926e6e8d759d3711873\",\"version\":\"v13\"},\"schema\":\"informed-decision.review-runtime.v1\",\"workload_api\":{\"intake\":{\"groups_file\":\"/workload-registry/review-groups.json\",\"secret_file\":\"/var/run/secrets/informed-decision/requester/client-secret\",\"submitters_file\":\"/workload-registry/intake-submitters.json\"},\"lifecycle_callers_file\":\"/workload-registry/lifecycle-callers.json\",\"token_review\":{\"ca_file\":\"/var/run/secrets/informed-decision/kubernetes/ca.crt\",\"origin\":\"https://kubernetes.default.svc:443\",\"token_file\":\"/var/run/secrets/informed-decision/kubernetes/token\"}}}"
|
||||
"runtime.json": "{\"approval_origin\":\"http://approval-engine.approval-engine.svc.cluster.local:8080\",\"audit\":{\"origin\":\"http://audit-core.audit-core.svc.cluster.local:8080\",\"sender_token_file\":\"/var/run/secrets/informed-decision/audit/token\"},\"evidence_db\":\"/data/private/review.sqlite\",\"intake_attributes\":true,\"intake_delivery\":{\"hub_origin\":\"http://state-hub.state-hub.svc.cluster.local:8000\"},\"lifecycle_delivery\":true,\"lifecycle_policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-lifecycle.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.lifecycle\",\"package_digest\":\"sha256:da01e045fc108adec6b489e5d51f7e57366793016c5a64776963780a6aae457c\",\"version\":\"v2\"},\"ordinary_approval_ids\":[\"2ce76d7f-01c3-4b63-8476-8d1230679769\",\"c2af4bcf-15b4-4305-aac1-acc7e4dcb099\",\"dc22666a-d5d7-46bc-9490-ebe9fe09dc38\"],\"policy\":{\"caller_token_file\":\"/var/run/secrets/informed-decision/policy/token\",\"origin\":\"http://flex-auth-informed-decision-sitting.flex-auth.svc.cluster.local:8080\",\"package\":\"informed-decision.compact-sitting\",\"package_digest\":\"sha256:8911458f36f1a60598aa19592a2d752a74dffe2617fd86238407ac87bc300fde\",\"version\":\"v14\"},\"schema\":\"informed-decision.review-runtime.v1\",\"workload_api\":{\"intake\":{\"groups_file\":\"/workload-registry/review-groups.json\",\"secret_file\":\"/var/run/secrets/informed-decision/requester/client-secret\",\"submitters_file\":\"/workload-registry/intake-submitters.json\"},\"lifecycle_callers_file\":\"/workload-registry/lifecycle-callers.json\",\"token_review\":{\"ca_file\":\"/var/run/secrets/informed-decision/kubernetes/ca.crt\",\"origin\":\"https://kubernetes.default.svc:443\",\"token_file\":\"/var/run/secrets/informed-decision/kubernetes/token\"}}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
{
|
||||
"name": "configuration",
|
||||
"configMap": {
|
||||
"name": "informed-decision-runtime-6c221878d051",
|
||||
"name": "informed-decision-runtime-b488b96a0953",
|
||||
"defaultMode": 292
|
||||
}
|
||||
},
|
||||
|
|
@ -397,6 +397,18 @@
|
|||
"app.kubernetes.io/name": "state-hub"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"namespaceSelector": {
|
||||
"matchLabels": {
|
||||
"kubernetes.io/metadata.name": "sso"
|
||||
}
|
||||
},
|
||||
"podSelector": {
|
||||
"matchLabels": {
|
||||
"app.kubernetes.io/name": "bounded-executor"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"ports": [
|
||||
|
|
|
|||
89
tests/test_present_existing_release.py
Normal file
89
tests/test_present_existing_release.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""INFD-WP-0010-T02 release candidate: registry row, network admission, policy pin and probe."""
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from informed_decision.intake_service import IntakeError, configuration
|
||||
from test_intake_present_existing import ACTOR, approval, setup # noqa: F401 (fixture)
|
||||
from test_intake_service import request
|
||||
|
||||
ROOT = Path(__file__).parents[1]
|
||||
LIFECYCLE = ROOT / 'deploy' / 'lifecycle'
|
||||
|
||||
|
||||
def _tool(name):
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / 'tools' / f'{name}.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_registry_rows_and_projection():
|
||||
submitters, _ = configuration(LIFECYCLE / 'intake-submitters.json', LIFECYCLE / 'review-groups.json')
|
||||
assert submitters['system:serviceaccount:state-hub:state-hub'] == {
|
||||
'submitter_id': 'state-hub', 'memo_classes': ['decision-record'],
|
||||
'review_groups': ['net-kingdom-admins'], 'daily_limit': 50} # unchanged; outcome_read waits on T05
|
||||
assert submitters['system:serviceaccount:sso:nk-bind-executor'] == {
|
||||
'submitter_id': 'nk-bind-executor', 'memo_classes': ['bounded-execution'],
|
||||
'review_groups': ['net-kingdom-admins'], 'daily_limit': 10,
|
||||
'present_existing': {'approval_actor': 'service:nk-bind-executor', 'actions': ['bounded-execution']}}
|
||||
assert len(submitters) == 2
|
||||
projection = json.loads((LIFECYCLE / 'registries.candidate.json').read_text())['data']
|
||||
for name, raw in projection.items():
|
||||
assert (LIFECYCLE / name).read_text() == raw
|
||||
|
||||
|
||||
def test_candidate_render_pins_v14_and_admits_only_the_executor_on_8081():
|
||||
render = _tool('render_deployment').render
|
||||
admission = json.loads((LIFECYCLE / 'admission.candidate.json').read_text())
|
||||
rendered = render(admission)
|
||||
assert rendered == json.loads((LIFECYCLE / 'service.candidate.json').read_text())
|
||||
config = json.loads(next(i for i in rendered['items'] if i['kind'] == 'ConfigMap')['data']['runtime.json'])
|
||||
assert config['policy']['version'] == 'v14'
|
||||
assert config['policy']['package_digest'] == (
|
||||
'sha256:8911458f36f1a60598aa19592a2d752a74dffe2617fd86238407ac87bc300fde')
|
||||
netpol = next(i for i in rendered['items'] if i['metadata']['name'] == 'informed-decision-review')
|
||||
private = [r for r in netpol['spec']['ingress'] if r['ports'] == [{'protocol': 'TCP', 'port': 8081}]]
|
||||
assert len(private) == 1
|
||||
assert private[0]['from'] == [
|
||||
{'namespaceSelector': {'matchLabels': {'kubernetes.io/metadata.name': ns}},
|
||||
'podSelector': {'matchLabels': {'app.kubernetes.io/name': app}}}
|
||||
for ns, app in (('state-hub', 'state-hub'), ('sso', 'bounded-executor'))]
|
||||
|
||||
|
||||
def _ledger_rows(store):
|
||||
with store._transaction() as db:
|
||||
return db.execute('SELECT COUNT(*) FROM intake_ledger').fetchone()[0]
|
||||
|
||||
|
||||
def test_probe_envelope_is_refused_after_a_read_and_leaves_nothing(setup): # noqa: F811
|
||||
service, store, created, reads, current = setup
|
||||
probe = _tool('present_existing_probe')
|
||||
approval_id = str(uuid.uuid4())
|
||||
current['body'] = approval(id=approval_id, **{'binding.actor': 'informed-decision'})
|
||||
status, body = request(service, probe.build(approval_id, '20261101000000'), token='executor-token')
|
||||
assert (status, body.get('error')) == (409, 'approval_actor_mismatch')
|
||||
assert reads == [approval_id] and created == [] and _ledger_rows(store) == 0
|
||||
|
||||
|
||||
def test_probe_envelope_with_unreadable_approval(setup): # noqa: F811
|
||||
service, store, created, reads, current = setup
|
||||
probe = _tool('present_existing_probe')
|
||||
service.reader = lambda approval_id: (_ for _ in ()).throw(IntakeError(503, 'approval_unreadable'))
|
||||
status, body = request(service, probe.build(str(uuid.uuid4()), '20261101000001'), token='executor-token')
|
||||
assert (status, body.get('error')) == (503, 'approval_unreadable')
|
||||
assert created == [] and _ledger_rows(store) == 0
|
||||
|
||||
|
||||
def test_probe_program_is_self_contained_python(capsys, monkeypatch):
|
||||
probe = _tool('present_existing_probe')
|
||||
monkeypatch.setattr(sys, 'argv', ['probe', '--approval-id', str(uuid.uuid4()), '--program', '--stamp', '20261101000002'])
|
||||
probe.main()
|
||||
program = capsys.readouterr().out
|
||||
compile(program, 'probe', 'exec')
|
||||
namespace = {}
|
||||
exec(program.split('\n', 1)[0], namespace) # first line only: the envelope literal
|
||||
assert json.loads(namespace['ENVELOPE'])['memo_class'] == 'bounded-execution'
|
||||
assert probe.WORKLOAD.endswith(':8081/v1/memos') and 'Bearer' in program
|
||||
88
tools/present_existing_probe.py
Normal file
88
tools/present_existing_probe.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"""Build one synthetic present_existing intake envelope (INFD-WP-0010-T02 release check).
|
||||
|
||||
The envelope pins an approval id the caller names and a deliberately wrong binding
|
||||
digest, so a successful approval read always ends in a 409 refusal and no memo is
|
||||
stored, no budget is spent and no approval is created, entered or consumed. It only
|
||||
prints JSON; posting it is a separate, founder-run step (see
|
||||
deploy/lifecycle/RELEASE-INFD-WP-0010-T02.md). The content is synthetic and secret-free.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
|
||||
ACTOR = "service:nk-bind-executor"
|
||||
SUBMITTER = "nk-bind-executor"
|
||||
PROFILE_ACTIONS = ["bounded-execution"]
|
||||
WRONG_DIGEST = "sha256:" + "0" * 64
|
||||
WORKLOAD = "http://informed-decision-workload.informed-decision.svc.cluster.local:8081/v1/memos"
|
||||
TOKEN_FILE = "/var/run/secrets/informed-decision-intake/token"
|
||||
|
||||
# Runs inside the founder's probe pod (python from the service image). Prints status and the
|
||||
# refusal body only; never prints the token.
|
||||
POSTER = """import json, urllib.error, urllib.request
|
||||
token = open(%r).read().strip()
|
||||
request = urllib.request.Request(%r, data=ENVELOPE.encode(), method="POST",
|
||||
headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"})
|
||||
try:
|
||||
response = urllib.request.urlopen(request, timeout=20)
|
||||
print(response.status, response.read().decode()[:400])
|
||||
except urllib.error.HTTPError as error:
|
||||
print(error.code, error.read().decode()[:400])
|
||||
""" % (TOKEN_FILE, WORKLOAD)
|
||||
|
||||
|
||||
def build(approval_id, stamp):
|
||||
memo_id = "probe-" + stamp
|
||||
item_id = memo_id + "-packet"
|
||||
content = ("Synthetic release probe for the present_existing intake profile. "
|
||||
"It is refused by design and must never be shown for review.\n")
|
||||
packet_hash = "sha256:" + hashlib.sha256(content.encode()).hexdigest()
|
||||
memo = {
|
||||
"id": memo_id, "version": 1, "sealed": False, "locale": "en",
|
||||
"question": "Synthetic release probe: is this refused before review?",
|
||||
"requested_act": "approve", "binding_level": "organizational", "step_kind": "approve",
|
||||
"brief": "Synthetic present_existing release probe; never bound.",
|
||||
"binding": {"justification": None, "terms": None,
|
||||
"principal": {"id": "pending-human-session", "kind": "person", "role": "reviewer",
|
||||
"display_name": "Named reviewer in net-kingdom-admins", "identifiers": []},
|
||||
"target": {"environment": "prod", "id": "probe:" + memo_id, "kind": "bounded-execution",
|
||||
"label": "synthetic release probe", "requires_new_bind": False}},
|
||||
"packet": [{"item_id": item_id, "label": item_id, "hash": packet_hash}],
|
||||
"highlights": [],
|
||||
"approval_id": approval_id, "approval_binding_digest": WRONG_DIGEST,
|
||||
}
|
||||
return {
|
||||
"kind": "informed-decision.memo-intake", "version": 1,
|
||||
"idempotency_key": memo_id, "memo_class": "bounded-execution",
|
||||
"review_group": "net-kingdom-admins", "drafted_by": "informed-decision-release-probe",
|
||||
"approval_intent": {"action": "bounded-execution", "principal": "admin",
|
||||
"purpose": "Synthetic present_existing release probe",
|
||||
"target": {"admission_id": "PROBE-" + stamp, "admission_digest": WRONG_DIGEST}},
|
||||
"memo": memo,
|
||||
"packet": [{"item_id": item_id, "media_type": "text/plain", "content": content}],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--approval-id", required=True, help="existing approval UUID, or a random UUID")
|
||||
parser.add_argument("--program", action="store_true",
|
||||
help="print a self-contained python program that posts the envelope (for `python -`)")
|
||||
parser.add_argument("--stamp", default=datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S"))
|
||||
args = parser.parse_args()
|
||||
envelope = build(args.approval_id, args.stamp)
|
||||
# Offline self-check against the same validator intake uses.
|
||||
from informed_decision.intake import PresentExisting, SubmitterGrant, validate_submission
|
||||
validate_submission(envelope, submitter_id=SUBMITTER,
|
||||
grant=SubmitterGrant(SUBMITTER, frozenset({"bounded-execution"}),
|
||||
frozenset({"net-kingdom-admins"})),
|
||||
present_existing=PresentExisting(ACTOR, frozenset(PROFILE_ACTIONS)))
|
||||
raw = json.dumps(envelope, separators=(",", ":"))
|
||||
sys.stdout.write("ENVELOPE = %r\n%s" % (raw, POSTER) if args.program else raw + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -95,6 +95,19 @@ prepared `approval:read` (`82b471f`, source only); the sitting-requester checks
|
|||
consume (commit `12311d1`). The v14 pin is not in the candidate files: by this repo's release convention the pin
|
||||
moves together with the image in the founder-run release commit.
|
||||
|
||||
**2026-11-01, release candidate (founder approval relayed by the net-kingdom coordinator).** KeyCape
|
||||
`approval:read` is live (key-cape `82b471f`, hub decision `21a557a9`). Candidate files now carry the v14 pin
|
||||
(`admission.candidate.json`, re-rendered `service.candidate.json`; runtime ConfigMap `…b488b96a0953`), the
|
||||
`system:serviceaccount:sso:nk-bind-executor` present_existing row (`bounded-execution`, `net-kingdom-admins`,
|
||||
daily limit 10), and an 8081 ingress peer for exactly `sso` + `app.kubernetes.io/name=bounded-executor`. The state-hub
|
||||
row is unchanged and outcome_read stays off (T05). The founder runbook covers the order relative to access-engine v14,
|
||||
verification (including the side-effect-free `tools/present_existing_probe.py`) and rollback to Argo `312ab97` / Helm
|
||||
revision 13: `deploy/lifecycle/RELEASE-INFD-WP-0010-T02.md`. Tests: 704 passed, 83 skipped (`.venv`).
|
||||
**Blocked on the image:** no owner CI workflow builds the full service image. Only the policy.py and zlib overlay
|
||||
workflows exist; purpose-overlay run 5 failed by design on the changed `policy.py` hash. The image pin is therefore
|
||||
still the live `f5588f49…`, and the candidate must not be synced until the pin is replaced by an owner-CI full image
|
||||
that keeps zlib ≥ 1.3.2-r1. Nothing applied.
|
||||
|
||||
Then here: the policy pin, a registry row, a release, and a native proof with synthetic BEA content. Optional: for
|
||||
`bounded-execution`, require a packet item whose digest equals `target.admission_digest`, once the admission
|
||||
digest's canonical form is confirmed.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue