From c084600082cac1f55b672d222da4cf38bbd3fe52 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 4 Oct 2026 22:44:01 +0200 Subject: [PATCH] Integrate verified State Hub dispatcher into lifecycle release Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a10780-6d41-7952-9718-dfbc1f41236f --- deploy/lifecycle/README.md | 31 ++- deploy/lifecycle/dispatcher.candidate.json | 214 ++++++++++++++++++ .../dispatcher-container.json | 16 ++ .../dispatcher-scan.json | 9 + .../manifest.json | 10 +- docs/requester-workload-owner-review.md | 12 + docs/specs/LifecycleProductionRuntime.md | 18 +- tests/test_lifecycle_review.py | 42 +++- ...FD-WP-0005-frictionless-decision-intake.md | 13 +- ...INFD-WP-0007-decision-lifecycle-service.md | 12 + 10 files changed, 361 insertions(+), 16 deletions(-) create mode 100644 deploy/lifecycle/dispatcher.candidate.json create mode 100644 docs/evidence/2026-10-04-lifecycle-release/dispatcher-container.json create mode 100644 docs/evidence/2026-10-04-lifecycle-release/dispatcher-scan.json diff --git a/deploy/lifecycle/README.md b/deploy/lifecycle/README.md index ae63a87..63db1fa 100644 --- a/deploy/lifecycle/README.md +++ b/deploy/lifecycle/README.md @@ -15,7 +15,8 @@ humans and system:serviceaccount:state-hub:state-hub. **Not deployed.** sibling JSON registries. The policy YAML is Helm-rendered from Access Engine's FLEX-WP-0033 candidate values; it is a review artifact, not a second deployment owner. Existing Helm releases must be updated through their owning workflow. -All 25 proposed objects passed Kubernetes server dry-run against railiance01. +The original 25 objects and the three dispatcher objects passed Kubernetes +server dry-run against railiance01. No Secret is included. Dry-run does not test reachability, credential delivery, policy caller identity, backup restoration or a human disposition. @@ -46,10 +47,11 @@ contract; neither is presented as a newly enforced control. under its separate live go, with exact identity/path negative proofs and mounted-file refresh/rotation evidence. Only the audit ExternalSecret was present in the live namespace at inspection. -2. INFD-WP-0005-T08: State Hub owner supplies production forwarding and separate - lifecycle/intake audience token projections. Its current dev fixture is not - an activated forwarder. Confirm notification receiver reachability; keep - resolution write-back disabled until its owner admission. +2. INFD-WP-0005-T08 / STATE-WP-0096: the private dispatcher and separate + lifecycle/intake token projections are prepared in dispatcher.candidate.json. + Deploy after intake prerequisites. Explicit enqueue requires trusted workload + execution; there is no new public delegate endpoint. Confirm notification + receiver reachability; keep resolution write-back disabled. 3. Refresh live image/config pins and unresolved-entry counts; complete the backup/restore steps below; obtain explicit approval of this concrete rollout. INFD-WP-0007-T07 and CCR-2026-0032 require this distinct go. @@ -98,3 +100,22 @@ broader browser packet/binding profiles remain explicit workplan tasks. The final Access Engine pin is the Forgejo image built from commit d2d0b6f (run https://forgejo.coulomb.social/coulomb/access-engine/actions/runs/316). It supersedes the locally built candidate for the proposed deployment. + + +## Private State Hub dispatcher continuation + +The dispatcher is a separate Recreate workload with 5m CPU / 24Mi memory requests, +one private 1Gi PVC, no Service/Ingress, and the existing state-hub service account. +Its only egress is DNS and the exact informed-decision peer on 8081. Projected +lifecycle and intake tokens each expire after 600 seconds. The dispatcher reads +only the intake token and never binds. Its pod omits app=state-hub, so it is not +selected by the existing Hub API Service. + +Image: forgejo.coulomb.social/coulomb/state-hub-memo-dispatch@sha256:410fb6b6c9fd7a7ee0b63d727fd3249fb3ae13943421d6a5ce88b7ad2520bd2f +Source: State Hub 4a59b23, built by the dedicated Forgejo workflow. +Source integration proves queue/restart/lost-reply handling (one create, replayed +receipt), subsequent human acceptance and independent audit delivery. All identity +and credential inputs in that test are synthetic. Native acceptance remains open. + +There is no new service-image build for this continuation. The service suite now +passes 663 tests (1 skipped); State Hub forwarding/notification tests pass 28. diff --git a/deploy/lifecycle/dispatcher.candidate.json b/deploy/lifecycle/dispatcher.candidate.json new file mode 100644 index 0000000..cd7197b --- /dev/null +++ b/deploy/lifecycle/dispatcher.candidate.json @@ -0,0 +1,214 @@ +{ + "apiVersion": "v1", + "kind": "List", + "items": [ + { + "apiVersion": "v1", + "kind": "PersistentVolumeClaim", + "metadata": { + "name": "state-hub-memo-dispatch", + "namespace": "state-hub", + "annotations": { + "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + } + }, + "spec": { + "accessModes": [ + "ReadWriteOnce" + ], + "storageClassName": "local-path", + "resources": { + "requests": { + "storage": "1Gi" + } + } + } + }, + { + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "state-hub-memo-dispatch", + "namespace": "state-hub", + "annotations": { + "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + } + }, + "spec": { + "replicas": 1, + "strategy": { + "type": "Recreate" + }, + "selector": { + "matchLabels": { + "app.kubernetes.io/name": "state-hub", + "app.kubernetes.io/component": "memo-dispatch" + } + }, + "template": { + "metadata": { + "labels": { + "app.kubernetes.io/name": "state-hub", + "app.kubernetes.io/component": "memo-dispatch" + } + }, + "spec": { + "serviceAccountName": "state-hub", + "automountServiceAccountToken": false, + "terminationGracePeriodSeconds": 15, + "securityContext": { + "runAsNonRoot": true, + "runAsUser": 10001, + "runAsGroup": 10001, + "fsGroup": 10001, + "fsGroupChangePolicy": "OnRootMismatch", + "seccompProfile": { + "type": "RuntimeDefault" + } + }, + "containers": [ + { + "name": "dispatcher", + "image": "forgejo.coulomb.social/coulomb/state-hub-memo-dispatch@sha256:410fb6b6c9fd7a7ee0b63d727fd3249fb3ae13943421d6a5ce88b7ad2520bd2f", + "securityContext": { + "allowPrivilegeEscalation": false, + "readOnlyRootFilesystem": true, + "capabilities": { + "drop": [ + "ALL" + ] + } + }, + "resources": { + "requests": { + "cpu": "5m", + "memory": "24Mi" + }, + "limits": { + "cpu": "100m", + "memory": "96Mi" + } + }, + "volumeMounts": [ + { + "name": "queue", + "mountPath": "/data" + }, + { + "name": "intake-caller", + "mountPath": "/var/run/secrets/state-hub/informed-decision", + "readOnly": true + } + ] + } + ], + "volumes": [ + { + "name": "queue", + "persistentVolumeClaim": { + "claimName": "state-hub-memo-dispatch" + } + }, + { + "name": "intake-caller", + "projected": { + "defaultMode": 288, + "sources": [ + { + "serviceAccountToken": { + "audience": "informed-decision-intake", + "expirationSeconds": 600, + "path": "intake-token" + } + }, + { + "serviceAccountToken": { + "audience": "informed-decision-lifecycle", + "expirationSeconds": 600, + "path": "lifecycle-token" + } + } + ] + } + } + ] + } + } + } + }, + { + "apiVersion": "networking.k8s.io/v1", + "kind": "NetworkPolicy", + "metadata": { + "name": "state-hub-memo-dispatch", + "namespace": "state-hub", + "annotations": { + "state-hub.coulomb.social/admission": "proposal; STATE-WP-0096 / INFD-WP-0007 approved rollout required" + } + }, + "spec": { + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "state-hub", + "app.kubernetes.io/component": "memo-dispatch" + } + }, + "policyTypes": [ + "Ingress", + "Egress" + ], + "ingress": [], + "egress": [ + { + "to": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "kube-system" + } + }, + "podSelector": { + "matchLabels": { + "k8s-app": "kube-dns" + } + } + } + ], + "ports": [ + { + "protocol": "UDP", + "port": 53 + }, + { + "protocol": "TCP", + "port": 53 + } + ] + }, + { + "to": [ + { + "namespaceSelector": { + "matchLabels": { + "kubernetes.io/metadata.name": "informed-decision" + } + }, + "podSelector": { + "matchLabels": { + "app.kubernetes.io/name": "informed-decision" + } + } + } + ], + "ports": [ + { + "protocol": "TCP", + "port": 8081 + } + ] + } + ] + } + } + ] +} diff --git a/docs/evidence/2026-10-04-lifecycle-release/dispatcher-container.json b/docs/evidence/2026-10-04-lifecycle-release/dispatcher-container.json new file mode 100644 index 0000000..b365bd3 --- /dev/null +++ b/docs/evidence/2026-10-04-lifecycle-release/dispatcher-container.json @@ -0,0 +1,16 @@ +{ + "status": "passed", + "image_id": "sha256:410fb6b6c9fd7a7ee0b63d727fd3249fb3ae13943421d6a5ce88b7ad2520bd2f", + "checks": [ + "non-root read-only worker starts without external network", + "explicit enqueue persists a lifecycle envelope without inventing a Hub relationship", + "SIGTERM stops without forced kill", + "restart retains the exact pending envelope; missing custody does not drop it", + "private queue and database modes enforced" + ], + "fixture_only": true, + "external_network": "none", + "native_identity_proven": false, + "envelope_sha256": "ce0b8e1c6e861fde2da584363b695932bd0c74f923d1688211cc8b3d9a2f8c2e", + "cleanup_complete": true +} diff --git a/docs/evidence/2026-10-04-lifecycle-release/dispatcher-scan.json b/docs/evidence/2026-10-04-lifecycle-release/dispatcher-scan.json new file mode 100644 index 0000000..63930bd --- /dev/null +++ b/docs/evidence/2026-10-04-lifecycle-release/dispatcher-scan.json @@ -0,0 +1,9 @@ +{ + "image": "forgejo.coulomb.social/coulomb/state-hub-memo-dispatch@sha256:410fb6b6c9fd7a7ee0b63d727fd3249fb3ae13943421d6a5ce88b7ad2520bd2f", + "source_commit": "4a59b23", + "scanner": "Trivy 0.74.0", + "created_at": "2026-10-04T20:38:44.831801093Z", + "high": 0, + "critical": 0, + "scope": "Image vulnerabilities in the scanner database at scan time; not universal security attestation." +} diff --git a/docs/evidence/2026-10-04-lifecycle-release/manifest.json b/docs/evidence/2026-10-04-lifecycle-release/manifest.json index 84fc724..eb99bfe 100644 --- a/docs/evidence/2026-10-04-lifecycle-release/manifest.json +++ b/docs/evidence/2026-10-04-lifecycle-release/manifest.json @@ -3,11 +3,12 @@ "status": "prepared-not-deployed", "server_dry_run": { "cluster": "railiance01", - "objects": 25, + "objects": 28, "passed": true }, "sha256": { "deploy/lifecycle/admission.candidate.json": "e3054aebb2338146b912121198eba0232973574b9f045710abc802e524fe43f6", + "deploy/lifecycle/dispatcher.candidate.json": "e64bdaabfc307caf5f65894ce7e37fd66abaaf9328fd35cfd9103e14bbf739c8", "deploy/lifecycle/intake-submitters.json": "58ba5ef87ecfd40bc7f0eaf2beb5e50f0c1f0e47315e79dbfa9e06411a17eba9", "deploy/lifecycle/lifecycle-callers.json": "0b66382a64bdf81ee75d14a70ba3b19290084922a1e0bc8a873af0f039dc7041", "deploy/lifecycle/policy.candidate.yaml": "850d96aa7807b0e81e28278d380543770012a006b5dc3ebb06a5a8f5fcb43c4a", @@ -15,5 +16,12 @@ "deploy/lifecycle/review-groups.json": "1d61d2b82230981130936651a7fc4230e06ecaa51416a8a1be2bb2eb6ec4c353", "deploy/lifecycle/review-policy.candidate.yaml": "1f680eb1184c4a26ae2638b7e0829e5df42237ee09550e7554a3e965edbc8674", "deploy/lifecycle/service.candidate.json": "bf36ecb0160bf893a60fe059a3baf27a63668e2bb7b9f17c9582d6de8e842545" + }, + "dispatcher_verification": { + "owner_tests_passed": 28, + "cross_service_tests_passed": 663, + "cross_service_tests_skipped": 1, + "installed_container_checks": 5, + "high_critical_findings": 0 } } diff --git a/docs/requester-workload-owner-review.md b/docs/requester-workload-owner-review.md index 0b75a3a..385d275 100644 --- a/docs/requester-workload-owner-review.md +++ b/docs/requester-workload-owner-review.md @@ -86,3 +86,15 @@ No repeated design decision is needed for this accepted boundary. The remaining human action is separate founder approval of the final concrete live packet and named verification/abort attendance, after these technical gaps are closed. No live apply, credential read, rotation, issuer mutation or deployment occurred. + + +## Runtime acceptance update — 2026-10-04 + +The private listener, CA-verified TokenReview, rotating file-backed requester, +attribute consumer and durable State Hub dispatch are now composed and tested. +The service image remains the scanned 2768bd1 candidate in deploy/lifecycle; +State Hub worker is the CI-built 410fb6b candidate. This closes the source/runtime +composition gap only. The live CCR remains proposed; its current SHA256 is +ab4cd7c15192ebe98e17b0c40a188ae67616c63cc796d9b807adba7c39f2edb3. +No new design decision is requested. Exact live delivery approval, attended +OpenBao login, native custody/rotation proof and cutover acceptance remain. diff --git a/docs/specs/LifecycleProductionRuntime.md b/docs/specs/LifecycleProductionRuntime.md index 059133d..8adffbe 100644 --- a/docs/specs/LifecycleProductionRuntime.md +++ b/docs/specs/LifecycleProductionRuntime.md @@ -56,11 +56,16 @@ live Approval Engine object, checks its binding pin, requester informed-decision and human_control=true before the PDP call. Store guards recheck retained intake metadata at the mutation boundary. A valid-looking UUID/digest alone is not proof. -State Hub forwarding currently exists as a dev fixture, not a production worker. -The selected identity still needs projected lifecycle/intake tokens and an owner -runtime integration. State Hub notifications/write-back require admitted receiver -routes; `INFD_HUB_WRITEBACK` stays off in this candidate. Pending rows remain -explicit. These are live INFD-WP-0005 dependencies, not claimed release evidence. +State Hub now has a private explicitly queued dispatcher (STATE-WP-0096), with a +separate standard-library-only image and PVC. It uses the selected existing +service account and distinct projected lifecycle/intake tokens; its transport +reads only the intake token. The Hub public API does not gain a credentialed +forwarding route. Queueing is trusted workload execution. New review envelopes +are data operations and do not change deployments. The exact lifecycle snapshot +id/key is preserved without inventing a Hub relationship when serves is absent. +Lost replies replay the same envelope; native-create uncertainty blocks the row. +Native deployment/acceptance still remains open. Notification/write-back receiver +admission is unchanged; INFD_HUB_WRITEBACK remains off. The image migrates schema 2 to schema 5 on open. Existing records, presentations, dispositions, submissions and outbox evidence must survive. Rollback to the old @@ -76,7 +81,8 @@ must be reconciled, never retried or discarded to make rollback convenient. evidence remain with RPF-WP-0051. - FLEX-WP-0033: publish scanned policy artifact; approve exact policy/RBAC/network objects and native caller/subject allow/refusal tests. -- INFD-WP-0005: production State Hub forwarding/receiver and token projections. +- INFD-WP-0005 / STATE-WP-0096: activate and verify the prepared private State Hub + dispatcher and its projected tokens; retain gated resolution write-back. - INFD-WP-0007-T06/T07: backup/restore, exact rollout approval, native human review and independent audit receipts across two decisions and a revision with unchanged release/configuration pins. Browser packet authoring and broader binding profiles diff --git a/tests/test_lifecycle_review.py b/tests/test_lifecycle_review.py index e0d0812..ef238e1 100644 --- a/tests/test_lifecycle_review.py +++ b/tests/test_lifecycle_review.py @@ -164,13 +164,26 @@ from test_approval_component import component, signing_key from test_durable_component import receiver -def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision(handoff, component, receiver): +@pytest.mark.parametrize("via_dispatch", [False, True]) +def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision(handoff, component, receiver, tmp_path, via_dispatch): import jwt from informed_decision.intake_approval import IntakeApprovalClient from informed_decision.approval_http import ApprovalHTTPClient from informed_decision.review import ReviewController from informed_decision.runtime import AuditPump app, actor, memo, options, intake, calls = handoff + if via_dispatch: + import os, sys + source = os.environ.get('INFD_STATE_HUB_SOURCE') + if not source: + pytest.skip('State Hub source required for private dispatcher integration') + sys.path.insert(0, source) + from api.services.memo_dispatch import Dispatcher + options = {**options, 'memo_class': 'decision-record'} + grants = json.loads(intake.submitters_path.read_text()) + for row in grants.values(): + row['memo_classes'] = ['decision-record'] + intake.submitters_path.write_text(json.dumps(grants)) client, engine, transport, human, signing = component groups = json.loads(intake.groups_path.read_text()) groups[options['review_group']]['subject'] = human.subject @@ -183,8 +196,31 @@ def test_actual_engine_human_entry_and_independent_audit_link_to_source_revision summary='Responded to research', key='revise') app.publish(actor, memo.id, 2, audience={'subjects': [human.subject], 'groups': []}, key='publish-second') receipt = app.request_review(actor, memo.id, 2, options=options, key='review-second') - status, loaded = request(intake, receipt['envelope']) - assert status == 201, loaded + if via_dispatch: + attempts = [] + now = [time.time()] + def send(method, url, body, headers): + assert headers['Authorization'] == 'Bearer projected-token' + result = request(intake, body) + attempts.append(result[0]) + if len(attempts) == 1: + raise OSError('lost reply after native creation') + return result + worker = Dispatcher(tmp_path / 'private-dispatch', send=send, + read_token=lambda: 'projected-token', clock=lambda: now[0]) + assert worker.enqueue(receipt['envelope']).status == 'pending' + assert not attempts + assert worker.once().status == 'pending' + now[0] += 10 + worker = Dispatcher(tmp_path / 'private-dispatch', send=send, + read_token=lambda: 'projected-token', clock=lambda: now[0]) + forwarded = worker.once() + assert forwarded.status == 'delivered' and attempts == [201, 200] + loaded = forwarded.receipt + assert worker.once() is None + else: + status, loaded = request(intake, receipt['envelope']) + assert status == 201, loaded controller = ReviewController(app.repository.store, policy(), lambda session: ApprovalHTTPClient('https://approval.test', session, transport=transport), intake_attributes=True) page = controller.open(human, receipt['review_memo_id']) diff --git a/workplans/INFD-WP-0005-frictionless-decision-intake.md b/workplans/INFD-WP-0005-frictionless-decision-intake.md index d5d1af2..fa23839 100644 --- a/workplans/INFD-WP-0005-frictionless-decision-intake.md +++ b/workplans/INFD-WP-0005-frictionless-decision-intake.md @@ -167,7 +167,7 @@ id: INFD-WP-0005-T08 status: wait priority: medium blocked_on: message-from:state-hub -blocking_reason: "Founder selected system:serviceaccount:state-hub:state-hub; production token projection and runtime forwarding remain, alongside T05/T06" +blocking_reason: "STATE-WP-0096 implements private queue/runtime and projected audience proposals; native forwarding acceptance waits on requester custody and approved rollout" state_hub_task_id: "ce2b36b4-ef82-53b7-aee7-e0063855370b" ``` @@ -271,3 +271,14 @@ registries and live runtime remain unchanged. T05/T06 remain waiting on native custody and policy deployment; T08 still needs the State Hub production forwarder, not merely its dev fixture. See docs/specs/LifecycleProductionRuntime.md and FLEX-WP-0033 for exact boundaries. + + +## Private State Hub runtime delivered in source — 2026-10-04 + +STATE-WP-0096 adds the private explicit queue dispatcher, immutable CI image, +separate lifecycle/intake token projections, and isolated PVC/network proposal. +Cross-service proof covers retained lifecycle envelope forwarding, lost reply +following one native approval create, restart, replay receipt, human acceptance +and Audit Core delivery. 663 service tests pass (1 skipped), including this path; +28 State Hub dispatch/contract/notification tests pass. T08 remains waiting only +for approved native delivery/acceptance, not implementation of its runtime. diff --git a/workplans/INFD-WP-0007-decision-lifecycle-service.md b/workplans/INFD-WP-0007-decision-lifecycle-service.md index 67d318f..2443b36 100644 --- a/workplans/INFD-WP-0007-decision-lifecycle-service.md +++ b/workplans/INFD-WP-0007-decision-lifecycle-service.md @@ -274,3 +274,15 @@ CCR-2026-0032 / RPF-WP-0051 owns requester delivery; INFD-WP-0005-T08 owns State Hub forwarding/token projections. The exact chosen identity is no longer an open question. Rollback must retain new evidence and must not run a schema-2 reader on the migrated schema-5 database. + + +## Private dispatcher production continuation — 2026-10-04 + +STATE-WP-0096 now supplies the prepared private State Hub worker and audience +projections. No credentialed forwarding route is exposed through the unauthenticated +Hub API. Fixed a consumer contract mismatch: immutable lifecycle review envelopes +may omit serves and retain their review-UUID idempotency key. Integration proves +lost-reply/restart recovery without duplicate native approval, followed by human +review and independent audit. Validation: 663 passed, 1 skipped; 28 Hub tests. +The exact three worker objects are added to deploy/lifecycle. Native credentials, +rollout approval and human acceptance remain the next gates; no live mutation.