diff --git a/Dockerfile b/Dockerfile index 034856d..a31cbc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ COPY .forgejo/workflows/image.yaml ./.forgejo/workflows/image.yaml COPY schemas/ ./schemas/ COPY k8s/ ./k8s/ COPY scripts/render_gitops.py scripts/check_gitops_promotion.py ./scripts/ -RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py +RUN uv sync --frozen --extra dev && uv run --no-sync python scripts/render_gitops.py --check --verify-frontend --verify-platform && uv run --no-sync pytest -q -p no:cacheprovider tests/test_frontend_patterns.py tests/test_instruction_evaluation.py tests/test_admin_sync_api.py tests/test_gitops_release.py tests/test_release_broker.py tests/test_release_transport.py tests/test_release_dispatch.py # Stage 2 — runtime image FROM python:3.12-slim@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea AS runtime diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index b39a7da..c683118 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -35,15 +35,15 @@ | workplan | ACTIVITY-WP-0029 | finished | — | workplans/ACTIVITY-WP-0029-hub-port-alignment.md | | workplan | ACTIVITY-WP-0030 | finished | — | workplans/ACTIVITY-WP-0030-daily-sbom-catchup.md | | workplan | ACTIVITY-WP-0031 | finished | — | workplans/ACTIVITY-WP-0031-production-execution-reliability-cleanup.md | -| workplan | ACTIVITY-WP-0032 | active | — | workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md | +| workplan | ACTIVITY-WP-0032 | blocked | — | workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md | | workplan | ACTIVITY-WP-0033 | finished | — | workplans/ACTIVITY-WP-0033-sbom-catchup-retry-boundary.md | | workplan | ACTIVITY-WP-0034 | finished | — | workplans/ACTIVITY-WP-0034-sbom-controlled-source-reference.md | | workplan | ACTIVITY-WP-0035 | finished | — | workplans/ACTIVITY-WP-0035-intent-boundary-guardrails.md | | workplan | ACTIVITY-WP-0036 | finished | — | workplans/ACTIVITY-WP-0036-queue-identity-and-lease-integrity.md | | workplan | ACTIVITY-WP-0038 | finished | — | workplans/ACTIVITY-WP-0038-repository-grant-close-reconciliation.md | | workplan | ACTIVITY-WP-0039 | finished | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md | -| workplan | ACTIVITY-WP-0040 | active | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | -| workplan | ACTIVITY-WP-0041 | active | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | +| workplan | ACTIVITY-WP-0040 | blocked | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | +| workplan | ACTIVITY-WP-0041 | blocked | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | | workplan | ACTIVITY-WP-ADHOC-2026-06-01 | finished | — | workplans/ADHOC-2026-06-01.md | | workplan | ACTIVITY-WP-ADHOC-2026-08-20 | finished | — | workplans/ADHOC-2026-08-20.md | | workplan | ACTIVITY-WP-ADHOC-2026-08-23 | finished | — | workplans/ADHOC-2026-08-23.md | @@ -235,11 +235,11 @@ | task | ACTIVITY-WP-0039-T03 | done | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md | | task | ACTIVITY-WP-0039-T04 | done | — | workplans/ACTIVITY-WP-0039-multi-worker-identity-and-token-custody.md | | task | ACTIVITY-WP-0040-T01 | done | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | -| task | ACTIVITY-WP-0040-T02 | progress | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | +| task | ACTIVITY-WP-0040-T02 | wait | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | | task | ACTIVITY-WP-0040-T03 | done | — | workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md | | task | ACTIVITY-WP-0041-T01 | done | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | -| task | ACTIVITY-WP-0041-T02 | progress | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | -| task | ACTIVITY-WP-0041-T03 | progress | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | +| task | ACTIVITY-WP-0041-T02 | wait | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | +| task | ACTIVITY-WP-0041-T03 | wait | — | workplans/ACTIVITY-WP-0041-gitops-adoption.md | | task | ACTIVITY-WP-ADHOC-2026-06-01-T01 | done | — | workplans/ADHOC-2026-06-01.md | | task | ACTIVITY-WP-ADHOC-2026-06-01-T02 | done | — | workplans/ADHOC-2026-06-01.md | | task | ACTIVITY-WP-ADHOC-2026-06-01-T03 | done | — | workplans/ADHOC-2026-06-01.md | diff --git a/docs/release-broker.md b/docs/release-broker.md index a76b21c..8635553 100644 --- a/docs/release-broker.md +++ b/docs/release-broker.md @@ -1,7 +1,7 @@ # Image-only release broker core ACTIVITY-WP-0041-T03 owns this implementation. **Not activated in production.** -The new modules are not wired to an API, worker, schedule or credential source. +The modules are not wired to a production API, worker, schedule or credential source. Construction requires `admitted=True` from trusted deployment configuration; that switch is a local guard, not proof that an identity has actually been admitted. The current deployed revision is unchanged, so this code does not restart soak. @@ -62,8 +62,8 @@ production credentials and live Git/ArgoCD rollback are **not** proven by these custody/rotation. The observer must measure continuous health; it must not manufacture a 24-hour interval from two snapshots. Preserve signing public keys for audit and protect the ledger from producer writes. -4. Connect the durable dispatcher through activity-core/Temporal and sanitized - evidence sinks. Prove authenticated transport failure, concurrency with other +4. Deploy/register the implemented Temporal dispatcher and supply its bounded, + idempotent sanitized evidence sink. Prove authenticated transport failure, concurrency with other publishers, restart, denial and rollback in an isolated deployment environment. Then finish the production observation gate and enable the bounded scope. @@ -103,6 +103,40 @@ snapshots cannot establish the interval. New observers start from their first ac sample; they do not backfill the earlier deployment timestamp. Neither module is connected to a production schedule, endpoint or credential source. -Trusted signer custody, real invariant probes, Temporal dispatch and isolated +Trusted signer custody, real invariant probes, production Temporal registration and isolated Kubernetes authorization/rollback tests remain required before activation. Production revision and the deployment observation clock are unchanged by this source-only work. + +## Temporal dispatch and durable audit delivery + +`release_dispatch.ReleaseDispatchWorkflow` resumes an already-admitted release ID. +`ReleaseActivities` binds its broker, backend factory and audit sink at trusted worker +construction; Temporal inputs cannot supply manifests, keys, transport configuration +or an admission flag. `release_worker` constructs a separate +`activity-core-release-tq` worker, which the admitted deployment must explicitly run. +It does not modify the ordinary orchestrator worker or start a workflow/schedule. +Use a stable workflow ID such as `activity-core-release:` when +starting it. Broker serialization remains authoritative even with duplicate dispatch. + +Adapter calls run outside the workflow and resume from the durable ledger after +activity retry or restart. Failed rollback keeps the release slot and is retried +with durable timers; continue-as-new bounds workflow history. Temporal receives +sanitized errors rather than raw transport exceptions. Disabling the broker's +trusted admission setting prevents subsequent activity calls; operational revocation +still requires the admitted worker/credential lifecycle, not a workflow input. + +The transition ledger is also the audit outbox. Acknowledgements are persisted only +after the configured sink returns. Delivery is at least once with stable event IDs; +the sink must durably upsert those IDs and use bounded I/O. It receives only release +ID, phase, time, candidate/rollback commits and the fixed application name. Sink +credentials, signed envelopes and transport output never enter these events. +Nonterminal sink failure retains pending events and permits recovery to proceed; +terminal workflow completion waits for acknowledged audit delivery. One ledger is +bound to one logical audit sink; changing sinks requires an explicit replay/migration +of delivery acknowledgements. The sink can fan out to approved evidence destinations. + +Tests exercise the real ledger through the activities and validate Temporal sandbox +construction, with in-memory transports/sinks and orchestration stubs. This does not +prove a deployed Temporal server, real Kubernetes authorization or production sinks. +Continuous observer scheduling, actual sink adapters, admission credentials and +authenticated deployment proof remain activation prerequisites in T03. diff --git a/src/activity_core/release_dispatch.py b/src/activity_core/release_dispatch.py new file mode 100644 index 0000000..a77d581 --- /dev/null +++ b/src/activity_core/release_dispatch.py @@ -0,0 +1,135 @@ +"""Explicitly registered release dispatcher; no production credentials or admission.""" +from __future__ import annotations + +import asyncio +import re +from datetime import timedelta + +from temporalio import activity, workflow +from temporalio.common import RetryPolicy +from temporalio.exceptions import ActivityError, ApplicationError + +with workflow.unsafe.imports_passed_through(): + from activity_core.release_broker import TERMINAL, Broker + + +class ReleaseActivities: + """Trusted worker construction binds the ledger, adapter factory and audit sink. + + The factory receives a ledger plan, never workflow-supplied configuration. + The sink must durably upsert by event_id and return only after acknowledgement. + This is at-least-once delivery: a lost acknowledgement can repeat an event. + """ + + def __init__(self, broker: Broker, backend_factory, sink): + if not broker.admitted: + raise ValueError("broker identity not admitted") + self.broker, self.backend_factory, self.sink = broker, backend_factory, sink + with broker.connect() as db: + db.execute("CREATE TABLE IF NOT EXISTS release_audit_delivered " + "(sequence INTEGER PRIMARY KEY)") + + def _plan(self, release_id): + import json + + if not self.broker.admitted: + raise ApplicationError("release identity disabled", non_retryable=True) + if not isinstance(release_id, str) or not re.fullmatch(r"[0-9a-f]{64}", release_id): + raise ApplicationError("invalid release id", non_retryable=True) + with self.broker.connect() as db: + row = db.execute("SELECT plan FROM releases WHERE id=?", (release_id,)).fetchone() + if row is None: + raise ApplicationError("unknown admitted release", non_retryable=True) + return json.loads(row[0]) + + def _advance(self, release_id, plan): + return self.broker.advance(release_id, self.backend_factory(plan)) + + def _deliver(self, release_id, plan): + with self.broker.connect() as db: + rows = db.execute( + "SELECT sequence, phase, observed_at FROM transitions " + "WHERE release_id=? AND sequence NOT IN " + "(SELECT sequence FROM release_audit_delivered) " + "ORDER BY sequence LIMIT 100", (release_id,), + ).fetchall() + for sequence, phase, observed_at in rows: + # Only normalized facts leave the ledger. No envelopes or adapter errors. + self.sink({ + "event_id": f"release:{release_id}:{sequence}", + "event_type": "release_transition", "release_id": release_id, + "application": "activity-core", "phase": phase, + "observed_at": observed_at, "candidate": plan["candidate"], + "rollback": plan["rollback"], + }) + with self.broker.connect() as db: + db.execute("INSERT OR IGNORE INTO release_audit_delivered VALUES (?)", + (sequence,)) + return len(rows) + + @activity.defn(name="advance_admitted_release") + async def advance(self, release_id: str) -> str: + plan = self._plan(release_id) + try: + return await asyncio.to_thread(self._advance, release_id, plan) + except Exception: + # Do not put transport exception text or credentials in Temporal history. + raise ApplicationError("release step unavailable", type="ReleaseStepUnavailable") from None + + @activity.defn(name="deliver_release_audit") + async def deliver(self, release_id: str) -> int: + plan = self._plan(release_id) + try: + return await asyncio.to_thread(self._deliver, release_id, plan) + except Exception: + raise ApplicationError("release audit unavailable", type="ReleaseAuditUnavailable") from None + + +@workflow.defn +class ReleaseDispatchWorkflow: + """Resume one admitted ledger entry; receipt admission stays outside Temporal.""" + + @workflow.run + async def run(self, release_id: str) -> str: + for _ in range(100): + phase = await workflow.execute_activity( + "advance_admitted_release", release_id, + start_to_close_timeout=timedelta(minutes=10), + retry_policy=RetryPolicy(maximum_interval=timedelta(seconds=30)), + ) + try: + await workflow.execute_activity( + "deliver_release_audit", release_id, + start_to_close_timeout=timedelta(minutes=1), + retry_policy=RetryPolicy( + maximum_interval=timedelta(seconds=30), + maximum_attempts=0 if phase in TERMINAL else 1, + ), + ) + except ActivityError: + if phase in TERMINAL: + raise + # A sink outage must not prevent rollback. Committed transitions + # stay pending and terminal completion waits for audit delivery. + workflow.logger.warning("Release audit pending; ledger retains transitions") + if phase in TERMINAL: + return phase + if phase == "rollback_synced": + await workflow.sleep(timedelta(seconds=30)) + workflow.continue_as_new(release_id) + + +def release_worker(client, activities: ReleaseActivities): + """Dedicated queue: never add privileged release activities to orchestrator-tq. + + Call only from an admitted deployment with durable shared ledger storage, + immutable transport configuration and a bounded, idempotent audit sink. + This helper starts no worker, workflow or schedule by itself. + """ + from temporalio.worker import Worker + + if not activities.broker.admitted: + raise ValueError("broker identity not admitted") + return Worker(client, task_queue="activity-core-release-tq", + workflows=[ReleaseDispatchWorkflow], + activities=[activities.advance, activities.deliver]) diff --git a/tests/test_railiance_ops_inventory_wiring.py b/tests/test_railiance_ops_inventory_wiring.py index acc36b8..4a76150 100644 --- a/tests/test_railiance_ops_inventory_wiring.py +++ b/tests/test_railiance_ops_inventory_wiring.py @@ -191,11 +191,18 @@ def test_worker_mounts_ops_inventory_configmap() -> None: pod_spec = deployment["spec"]["template"]["spec"] container = pod_spec["containers"][0] - mounts = {mount["name"]: mount for mount in container["volumeMounts"]} + # One ConfigMap backs both the inventory directory and the pinned retention + # script. Volume names are not unique within volumeMounts; paths are. + mounts = {mount["mountPath"]: mount for mount in container["volumeMounts"]} volumes = {volume["name"]: volume for volume in pod_spec["volumes"]} - assert mounts["ops-service-inventory"]["mountPath"] == "/etc/activity-core/ops" - assert mounts["ops-service-inventory"]["readOnly"] is True + inventory = mounts["/etc/activity-core/ops"] + assert inventory["name"] == "ops-service-inventory" + assert inventory["readOnly"] is True + script = mounts["/opt/railiance-platform/scripts/forgejo_package_prune.py"] + assert script["name"] == "ops-service-inventory" + assert script["subPath"] == "forgejo_package_prune.py" + assert script["readOnly"] is True assert volumes["ops-service-inventory"]["configMap"]["name"] == ( "actcore-ops-service-inventory" ) diff --git a/tests/test_release_dispatch.py b/tests/test_release_dispatch.py new file mode 100644 index 0000000..1019c35 --- /dev/null +++ b/tests/test_release_dispatch.py @@ -0,0 +1,140 @@ +"""Durable dispatch and audit recovery without production authority or transports.""" +import pytest +from temporalio.exceptions import ActivityError, ApplicationError +from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner +from temporalio.workflow import _Definition + +from activity_core.release_broker import Broker, TERMINAL +from activity_core.release_dispatch import ReleaseActivities, ReleaseDispatchWorkflow +from tests.test_release_broker import NOW, FakeBackend, admit, bundle # noqa: F401 + + +@pytest.fixture +def dispatch(bundle, monkeypatch): + broker, *_ = bundle + rid = admit(bundle) + backend = FakeBackend() + advance = Broker.advance + monkeypatch.setattr(Broker, 'advance', lambda self, rid, backend: advance(self, rid, backend, NOW)) + events = {} + sink = lambda event: events.update({event['event_id']: event}) + activities = ReleaseActivities(broker, lambda plan: backend, sink) + return activities, rid, backend, events, sink + + +async def test_restart_delivers_all_committed_transitions_and_deduplicates(dispatch): + activities, rid, backend, events, sink = dispatch + assert await activities.advance(rid) == 'publish_pending' + assert await activities.advance(rid) == 'published' + # Restart before audit delivery. The ledger contains both undelivered steps. + restarted = ReleaseActivities( + Broker(activities.broker.database, activities.broker.receipts, admitted=True), + lambda plan: backend, sink, + ) + assert await restarted.deliver(rid) == 3 + assert await restarted.deliver(rid) == 0 + while await restarted.advance(rid) not in TERMINAL: + pass + assert await restarted.deliver(rid) == 2 + assert [e['phase'] for e in events.values()] == [ + 'planned', 'publish_pending', 'published', 'synced', 'complete', + ] + assert all(set(e) == {'event_id', 'event_type', 'release_id', 'application', + 'phase', 'observed_at', 'candidate', 'rollback'} + for e in events.values()) + + +async def test_lost_audit_ack_replays_same_event_without_losing_it(dispatch): + activities, rid, _, events, sink = dispatch + def lost_ack(event): + sink(event) + raise RuntimeError('secret sink token must not reach Temporal') + activities.sink = lost_ack + with pytest.raises(ApplicationError) as error: + await activities.deliver(rid) + assert str(error.value) == 'ReleaseAuditUnavailable: release audit unavailable' + assert error.value.__suppress_context__ + activities.sink = sink + assert await activities.deliver(rid) == 1 + assert len(events) == 1 + + +async def test_lost_publication_response_resumes_and_rolls_back(dispatch): + activities, rid, backend, events, _ = dispatch + backend.lose_response = True + await activities.advance(rid) + with pytest.raises(ApplicationError, match='release step unavailable'): + await activities.advance(rid) + backend.fail_health = True + for _ in range(10): + phase = await activities.advance(rid) + await activities.deliver(rid) + if phase in TERMINAL: + break + assert phase == 'rolled_back' + assert backend.revision == 'b' * 40 + assert list(events.values())[-1]['phase'] == 'rolled_back' + + +@pytest.mark.parametrize('rid', ['untrusted', 'f' * 64, {'key': 'caller config'}]) +async def test_only_existing_admitted_id_accepted(dispatch, rid): + activities, _, backend, events, _ = dispatch + with pytest.raises(ApplicationError) as error: + await activities.advance(rid) + assert error.value.non_retryable + assert not backend.calls and not events + + +async def test_revocation_blocks_resume_and_delivery(dispatch): + activities, rid, backend, events, _ = dispatch + activities.broker.admitted = False + for call in (activities.advance, activities.deliver): + with pytest.raises(ApplicationError) as error: + await call(rid) + assert error.value.non_retryable + assert not backend.calls and not events + + +async def test_temporal_sandbox_and_workflow_rollback(dispatch, monkeypatch): + # Validate actual Temporal sandbox imports, then exercise orchestration with + # real ledger activities. No Temporal server or simulated passage of soak time. + SandboxedWorkflowRunner().prepare_workflow(_Definition.must_from_class(ReleaseDispatchWorkflow)) + activities, rid, backend, events, _ = dispatch + backend.fail_health = True + async def execute(name, arg, **kwargs): + return await (activities.advance(arg) if name == 'advance_admitted_release' + else activities.deliver(arg)) + async def sleep(_): + pass + monkeypatch.setattr('activity_core.release_dispatch.workflow.execute_activity', execute) + monkeypatch.setattr('activity_core.release_dispatch.workflow.sleep', sleep) + assert await ReleaseDispatchWorkflow().run(rid) == 'rolled_back' + assert list(events.values())[-1]['phase'] == 'rolled_back' + + +async def test_sink_outage_cannot_prevent_rollback(dispatch, monkeypatch): + activities, rid, backend, events, _ = dispatch + backend.fail_health = True + phases = [] + async def execute(name, arg, **kwargs): + if name == 'advance_admitted_release': + phase = await activities.advance(arg) + phases.append(phase) + return phase + if phases[-1] not in TERMINAL: + assert kwargs['retry_policy'].maximum_attempts == 1 + raise ActivityError('audit unavailable', scheduled_event_id=1, + started_event_id=2, identity='fixture', + activity_type=name, activity_id='fixture', retry_state=None) + assert kwargs['retry_policy'].maximum_attempts == 0 + return await activities.deliver(arg) + async def sleep(_): + pass + monkeypatch.setattr('activity_core.release_dispatch.workflow.execute_activity', execute) + monkeypatch.setattr('activity_core.release_dispatch.workflow.sleep', sleep) + # A plain logger avoids requiring a live workflow runtime for this unit test. + import logging + monkeypatch.setattr('activity_core.release_dispatch.workflow.logger', logging.getLogger(__name__)) + assert await ReleaseDispatchWorkflow().run(rid) == 'rolled_back' + assert backend.revision == 'b' * 40 + assert len(events) == len(phases) + 1 # Includes the admission transition. diff --git a/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md b/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md index 0321c89..defd150 100644 --- a/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md +++ b/workplans/ACTIVITY-WP-0032-glas-profile-execution-contract.md @@ -4,13 +4,13 @@ type: workplan title: "Adopt the Glas profile-driven execution contract" domain: infotech repo: activity-core -status: active +status: blocked flavor: implementation owner: claude topic_slug: activity-core priority: medium created: "2026-08-21" -updated: "2026-09-04" +updated: "2026-09-27" related: - ACT-ADR-006 - ACTIVITY-WP-0026 @@ -299,3 +299,15 @@ was requested or copied, and the disabled pilot remains untouched. - [x] `approach_hint` cannot override or substitute for a profile ref, proven by test - [x] Normalized Glas evidence is visible in production status - [ ] One definition proven on railiance01 end to end + +## Loose-end review — 2026-09-27 + +T05 remains blocked outside this repository. Current owner evidence supersedes +the September 4 description: sand-boxer has implemented owner-mediated execution +and protected runtime placement, but GLAS-WP-0012 remains blocked. The original +1.0.0 pilot profile is still explicitly blocked; 1.1.1 is unverified and needs +its updated runtime and combined production proof. Native credential admission, +provider execution and profile readiness remain owner gates. Do not rerun the +old pilot or silently change its profile. Resume T05 after Glas supplies an +admitted profile and matching railiance01 runtime/credential evidence. Workplan +state is now blocked; the four completed implementation tasks remain done. diff --git a/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md b/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md index b56897d..f4616fd 100644 --- a/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md +++ b/workplans/ACTIVITY-WP-0040-frontend-patterns-feedback.md @@ -4,7 +4,7 @@ type: workplan title: "Deterministic frontend-patterns feedback collection and reports" domain: infotech repo: activity-core -status: active +status: blocked owner: codex topic_slug: activity-core created: "2026-09-27" @@ -35,7 +35,7 @@ Dockerfile.frontend-patterns overlays only two Python files on the deployed work ```task id: ACTIVITY-WP-0040-T02 -status: progress +status: wait priority: high state_hub_task_id: "0d0d1d05-9b2d-5eac-b54a-c557b1690afc" ``` @@ -88,3 +88,13 @@ FEP-WP-0008 retains consumers and improvement execution; ACTIVITY-WP-0041 record the permanent GitOps/adoption/standing-authority work needed to prevent exceptions. Do not close this workplan until cadence evidence is observed or explicitly handed off to another live task. No real-consumer value or autonomous edits are claimed. + +## Loose-end review — 2026-09-27 + +T02 is now wait and the workplan blocked on natural cadence evidence. The +September 27 activation/sink smoke already completed the deployable repo work; +first daily/weekly/monthly executions are due September 28 / September 29 / +October 1. A manual rerun cannot satisfy this acceptance condition. Resume T02 +when the three natural executions have matching run/report/sink receipts. Keep +existing schedules enabled and retain FEP-WP-0008 ownership of consumer value +and improvement execution. No new task or artificial success evidence was created. diff --git a/workplans/ACTIVITY-WP-0041-gitops-adoption.md b/workplans/ACTIVITY-WP-0041-gitops-adoption.md index a0b0b43..b8a30fa 100644 --- a/workplans/ACTIVITY-WP-0041-gitops-adoption.md +++ b/workplans/ACTIVITY-WP-0041-gitops-adoption.md @@ -4,7 +4,7 @@ type: workplan title: "Remove recurring deployment exceptions through governed GitOps adoption" domain: infotech repo: activity-core -status: active +status: blocked owner: codex topic_slug: activity-core created: "2026-09-27" @@ -44,7 +44,7 @@ server-dry-run the proposed managed set; no unintended spec change or pruning. ```task id: ACTIVITY-WP-0041-T02 -status: progress +status: wait priority: high state_hub_task_id: "af09865d-8cc9-5571-9c34-20ae679a1e4e" ``` @@ -67,7 +67,7 @@ one-time governance decision. ```task id: ACTIVITY-WP-0041-T03 -status: progress +status: wait priority: high state_hub_task_id: "8e2b4ed9-d455-5367-85d9-8e4222ce75f3" ``` @@ -191,3 +191,29 @@ T03 remains progress for admitted identity/custody, authenticated isolated Kuber verification, real build/review/retention issuers, production invariant probes, Temporal observer/dispatcher registration and evidence sinks. The implemented observer cannot retroactively assert the earlier soak interval. See docs/release-broker.md. + +## Loose-end implementation and blockers — 2026-09-27 + +Completed another repo-side portion of T03: `release_dispatch.py` supplies a +Temporal workflow and explicitly constructed dedicated worker for resuming an +already-admitted ledger release. Requests carry only its ID; source, transport, +keys and sinks come from trusted worker configuration. Durable transition audit +acknowledgements survive restart, repeat the same event ID after a lost response, +and export only normalized facts. Nonterminal audit outages do not prevent +rollback; terminal completion waits for audit delivery. Adapter errors are +sanitized before reaching Temporal history. The ordinary worker remains unchanged. + +Tests cover restart, lost publication and audit responses, rollback, unknown IDs, +identity disablement and Temporal sandbox construction. The image CI test target +includes the new suite. This is isolated implementation evidence, not authenticated +Kubernetes proof or production activation. + +T02 and T03 are now wait and the workplan blocked. T02 cannot close before the +September 28 16:06:22 Berlin observation eligibility and healthy owner evidence +(RPF-WP-0048-T02). T03 still requires admitted identity/custody, real independent +receipt issuers, bounded production report/schedule probes, authenticated isolated +transport/denial/rollback proof, and deployment/registration of the observer, +dispatcher and audit sink. Local code cannot supply those trust inputs. The new +worker factory neither enables a schedule nor grants an identity; deployment and +continuous health observation must be established through the existing owner lane. +No new workplan/task, production mutation or authority expansion was introduced.