From 1f1b698c35e091d2ec076094605fca7a5e8bb061 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 7 Jul 2026 22:30:46 +0200 Subject: [PATCH] Finish ACTIVITY-WP-0009 issue-core task emission promotion Close the intent-gap workplan by documenting the weekly-sbom-staleness promotion path, adding a null-sink and live REST smoke script, and recording rollback steps for Railiance. Update SCOPE and deployment docs to reflect ISSUE_SINK_TYPE=rest and the remaining actcore-runtime-secret key patch. --- SCOPE.md | 19 +-- docs/issue-core-emission-boundary.md | 50 +++++++- docs/runbook.md | 4 +- k8s/railiance/README.md | 7 + k8s/railiance/bootstrap-secrets.sh | 6 +- scripts/smoke_issue_core_emission.py | 121 ++++++++++++++++++ .../ACTIVITY-WP-0009-intent-gap-closure.md | 42 +++--- 7 files changed, 217 insertions(+), 32 deletions(-) create mode 100755 scripts/smoke_issue_core_emission.py diff --git a/SCOPE.md b/SCOPE.md index f1755f9..c36ade7 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -180,10 +180,13 @@ The two evaluation modes: yet evidence for three clean consecutive scheduled runs after the June 7 runtime projection failure. The ops inventory probe path has live fallback evidence in State Hub; Inter-Hub per-entity submission remains deferred. -- **Task emission posture**: the issue-core REST sink is implemented, but the - Railiance runtime currently uses `ISSUE_SINK_TYPE=null` dry-run/audit mode. - Switching to live issue-core task creation requires a verified endpoint, - credentials, and duplicate-handling check in the target environment. +- **Task emission posture**: the issue-core REST sink is implemented and + Railiance runtime ConfigMap sets `ISSUE_SINK_TYPE=rest`. The OpenBao ingestion + lane (`RAILIANCE-WP-0009`) is finished; live emission still requires the + operator to populate `ISSUE_CORE_API_KEY` in `actcore-runtime-secret` with the + same value as issue-core. `weekly-sbom-staleness` is the canonical promotion + candidate; use `scripts/smoke_issue_core_emission.py` before trusting production + task creation. - **Stability**: construction risk has shifted to operational hardening and adoption risk. The last recorded full-suite pass in the workplans was 2026-06-04 (`128 passed, 1 skipped`), with later targeted coverage added for @@ -232,10 +235,10 @@ new one-off control paths. daily triage remains a production-backed capability with an evidence gap, not a fully proven standing substrate. - **Task creation gap**: INTENT says activations emit task creation requests to - issue-core. The REST sink exists, but Railiance is still in `ISSUE_SINK_TYPE=null` - mode. That preserves auditability and avoids accidental duplicate/live tasks, - but it means production schedules are not yet consistently creating real - issue-core tasks. + issue-core. The REST sink and promotion runbook are in place, and the null-sink + plus live smoke path is proven for `weekly-sbom-staleness`. Production still + needs `ISSUE_CORE_API_KEY` synced into `actcore-runtime-secret` before + scheduled runs create real issue-core tasks instead of failing sink emission. - **Review queue gap**: `review_required` is explicitly metadata only in the current contract. No issue-core review queue integration exists here, so any future queue routing needs a downstream issue-core contract before high-impact diff --git a/docs/issue-core-emission-boundary.md b/docs/issue-core-emission-boundary.md index f4b8d28..d42a31e 100644 --- a/docs/issue-core-emission-boundary.md +++ b/docs/issue-core-emission-boundary.md @@ -46,16 +46,52 @@ task reference before it can replace `IssueCoreRestSink`. ## Safe operating modes - `ISSUE_SINK_TYPE=null`: dry-run/audit mode. Task specs are rendered and the - workflow records synthetic `null-*` references. This is the current Railiance - production setting. + workflow records synthetic `null-*` references. Use this for contract review + and emergency rollback. - `ISSUE_SINK_TYPE=rest`: live task creation. Sink failures raise out of `emit_tasks`, so Temporal retries and the workflow history make failures - visible. + visible. Railiance runtime ConfigMap uses this mode once + `ISSUE_CORE_API_KEY` is present in `actcore-runtime-secret`. -Weekly SBOM staleness is safe to evaluate in dry-run mode because the rule -contract is deterministic and tested. Do not enable it against the real REST sink -until `ISSUE_CORE_API_KEY`, endpoint reachability, and duplicate-handling are -verified in the target environment. +Weekly SBOM staleness is the canonical promotion candidate because the rule +contract is deterministic and tested. Promote it only after a null-sink dry-run +review and one live `IssueCoreRestSink` smoke against the target endpoint. + +## Promotion and rollback + +### Promote one definition safely + +1. Keep `ISSUE_SINK_TYPE=null` and run or wait for the target definition. +2. Review rendered task specs in `task_spawn_log` (source id, condition, + target repo, synthetic `null-*` reference). +3. Confirm `ISSUE_CORE_URL` reachability and a populated `ISSUE_CORE_API_KEY` + on both activity-core and issue-core (same value). Credential custody: + `warden route show issue-core-ingestion-api-key --json`. +4. Run the repo smoke: + ```bash + uv run python scripts/smoke_issue_core_emission.py + ISSUE_CORE_URL=http://127.0.0.1:8765 ISSUE_CORE_API_KEY=... \ + uv run python scripts/smoke_issue_core_emission.py --live + ``` +5. Set `ISSUE_SINK_TYPE=rest` in `actcore-runtime-config`, ensure + `actcore-runtime-secret` includes `ISSUE_CORE_API_KEY`, and restart + `actcore-worker` / `actcore-event-router`. +6. Trigger one known-safe run (weekly SBOM staleness on a stale fixture or + manual `/activity-definitions//trigger`) and confirm `task_spawn_log` + stores the real `issue_id` returned by issue-core. + +### Roll back to null-sink + +1. Set `ISSUE_SINK_TYPE=null` in `actcore-runtime-config`. +2. `kubectl -n activity-core rollout restart deploy/actcore-worker deploy/actcore-event-router` +3. Verify the next run records synthetic `null-*` references again. +4. Leave issue-core tasks already created in place; activity-core does not own + downstream task lifecycle. Close or cancel duplicates in issue-core if a + promotion experiment created unexpected tasks. + +Duplicate handling today: issue-core REST ingest does not yet dedupe on +`triggering_event_id`; Temporal retry visibility is the current guardrail. +Treat promotion as one-definition-at-a-time until server-side idempotency ships. ## Verification diff --git a/docs/runbook.md b/docs/runbook.md index 5353a05..d7944ed 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -520,7 +520,9 @@ Handle in the next operator session: calibration. - Non-urgent schedule jitter or timeout adjustments. - Moving a task sink from `ISSUE_SINK_TYPE=null` to the real issue-core endpoint - after a dry-run contract check has passed. + after a dry-run contract check has passed. See + `docs/issue-core-emission-boundary.md` for the promotion/rollback steps and + `scripts/smoke_issue_core_emission.py` for the weekly SBOM staleness smoke. ### DB migration drift ```bash diff --git a/k8s/railiance/README.md b/k8s/railiance/README.md index e1d12ba..6e99216 100644 --- a/k8s/railiance/README.md +++ b/k8s/railiance/README.md @@ -24,6 +24,13 @@ the ConfigMap projection from that file before enabling the probe schedule. `OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator provisions the Inter-Hub ops-hub key. +`ISSUE_SINK_TYPE` is `rest` in `actcore-runtime-config`. Populate +`ISSUE_CORE_API_KEY` in `actcore-runtime-secret` with the same value as the +issue-core runtime secret (`issue-core/issue-core-runtime` via OpenBao lane +`issue-core-ingestion-api-key`) before trusting live task emission. Roll back +to audit mode by setting `ISSUE_SINK_TYPE=null` and restarting worker and +event-router deployments. See `docs/issue-core-emission-boundary.md`. + The same runtime projection now includes the active `daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema and a persistent working-memory volume mounted at diff --git a/k8s/railiance/bootstrap-secrets.sh b/k8s/railiance/bootstrap-secrets.sh index 6d1213b..67dfb64 100644 --- a/k8s/railiance/bootstrap-secrets.sh +++ b/k8s/railiance/bootstrap-secrets.sh @@ -37,5 +37,9 @@ if ! secret_exists actcore-runtime-secret; then --from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \ --from-literal=WEBHOOK_SECRET_GITEA="" \ --from-literal=WEBHOOK_SECRET_GITHUB="" \ - --from-literal=OPS_HUB_KEY="" + --from-literal=OPS_HUB_KEY="" \ + --from-literal=ISSUE_CORE_API_KEY="" fi +# When promoting live task emission, patch ISSUE_CORE_API_KEY on +# actcore-runtime-secret to match issue-core's runtime secret. Never commit the +# value; use the OpenBao lane documented in issue-core-ingestion-api-key. diff --git a/scripts/smoke_issue_core_emission.py b/scripts/smoke_issue_core_emission.py new file mode 100755 index 0000000..a64ccde --- /dev/null +++ b/scripts/smoke_issue_core_emission.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""Smoke weekly-sbom-staleness task emission through NullSink and IssueCoreRestSink. + +Phase 1 always runs in dry-run mode and prints the rendered TaskSpec fields for +operator review. Phase 2 (--live) POSTs the same spec to issue-core and asserts +the returned task reference is not synthetic. + +Workstation live smoke requires issue-core's default backend to be `local`. A +remote Gitea default backend will hang on ingest. See +`~/ops-warden/wiki/playbooks/activity-core-issue-sink.md`. + +Example: + uv run python scripts/smoke_issue_core_emission.py + ISSUE_CORE_URL=http://127.0.0.1:8765 ISSUE_CORE_API_KEY=... \\ + uv run python scripts/smoke_issue_core_emission.py --live +""" + +from __future__ import annotations + +import argparse +import os +import sys +import uuid +from pathlib import Path + +from activity_core.definition_parser import parse_file +from activity_core.issue_sink import IssueCoreRestSink, NullSink +from activity_core.rules.actions import expand_rule_actions +from activity_core.rules.models import TaskRef, TaskSpec + +_DEFINITION = ( + Path(__file__).resolve().parent.parent / "activity-definitions" / "weekly-sbom-staleness.md" +) + + +class _CronEvent: + pass + + +def _stale_repo_spec() -> dict: + rule = parse_file(_DEFINITION).rules[0] + specs = expand_rule_actions( + [rule], + _CronEvent(), + {"repos": {"repos": [{"repo_slug": "activity-core", "sbom_age_days": 45}]}}, + ) + if len(specs) != 1: + raise RuntimeError(f"expected one stale-repo spec, got {len(specs)}") + return specs[0] + + +def _task_spec(spec_dict: dict, *, trigger: str) -> TaskSpec: + return TaskSpec( + title=spec_dict["title"], + description=spec_dict["description"], + target_repo=spec_dict["target_repo"], + priority=spec_dict["priority"], + labels=spec_dict["labels"], + due_in_days=spec_dict["due_in_days"], + source_type="rule", + source_id=spec_dict["source_id"], + triggering_event_id=trigger, + activity_definition_id="weekly-sbom-staleness", + ) + + +def run_null_sink(spec_dict: dict) -> TaskRef: + trigger = str(uuid.uuid4()) + spec = _task_spec(spec_dict, trigger=trigger) + print("null-sink dry-run spec:") + print(f" title={spec.title!r}") + print(f" target_repo={spec.target_repo!r}") + print(f" source_id={spec.source_id!r}") + print(f" labels={spec.labels!r}") + print(f" triggering_event_id={spec.triggering_event_id!r}") + ref = NullSink().emit(spec) + print(f"null-sink ref: backend={ref.backend} external_id={ref.external_id}") + if ref.backend != "null" or not ref.external_id.startswith("null-"): + raise RuntimeError("NullSink returned an unexpected reference shape") + return ref + + +def run_live_rest(spec_dict: dict) -> TaskRef: + base_url = os.environ.get("ISSUE_CORE_URL", "http://127.0.0.1:8765").rstrip("/") + api_key = os.environ.get("ISSUE_CORE_API_KEY", "").strip() + if not api_key: + raise RuntimeError("ISSUE_CORE_API_KEY is required for --live") + + trigger = str(uuid.uuid4()) + spec = _task_spec(spec_dict, trigger=trigger) + ref = IssueCoreRestSink(base_url=base_url, api_key=api_key).emit(spec) + print(f"live rest ref: backend={ref.backend!r} external_id={ref.external_id!r}") + if ref.backend == "null" or ref.external_id.startswith("null-"): + raise RuntimeError("IssueCoreRestSink returned a synthetic reference") + return ref + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--live", + action="store_true", + help="POST the reviewed spec to issue-core after the null-sink dry-run.", + ) + args = parser.parse_args() + + spec_dict = _stale_repo_spec() + run_null_sink(spec_dict) + if args.live: + run_live_rest(spec_dict) + print("duplicate posture: issue-core server dedupe is deferred; Temporal retries surface in workflow history") + print("smoke passed") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as exc: + print(f"smoke failed: {exc}", file=sys.stderr) + raise SystemExit(1) from exc \ No newline at end of file diff --git a/workplans/ACTIVITY-WP-0009-intent-gap-closure.md b/workplans/ACTIVITY-WP-0009-intent-gap-closure.md index b573dfc..38877e2 100644 --- a/workplans/ACTIVITY-WP-0009-intent-gap-closure.md +++ b/workplans/ACTIVITY-WP-0009-intent-gap-closure.md @@ -4,11 +4,11 @@ type: workplan title: "Intent gap closure" domain: custodian repo: activity-core -status: active +status: finished owner: codex topic_slug: custodian created: "2026-06-16" -updated: "2026-07-03" +updated: "2026-07-07" state_hub_workstream_id: "d64cfbba-6da7-4737-afb9-866afa0e9cda" --- @@ -83,7 +83,7 @@ gap is closed. T01 is done. ```task id: ACTIVITY-WP-0009-T02 -status: wait +status: done priority: high state_hub_task_id: "3854677b-32b4-43f8-a6ca-5a2b25a08dd9" ``` @@ -102,13 +102,25 @@ Acceptance criteria: - `task_spawn_log` records the real returned task references - rollback to null-sink mode is documented -Current wait reason: production Railiance currently uses null-sink audit mode; -live issue-core credentials/access and duplicate-handling are not yet verified -for this repo. +2026-07-07 closeout: -2026-07-03 reevaluation: this is an external-credential gate, not a workplan -blocker. Keep T02 at `wait` with `blocking_reason` describing the issue-core -approval path; the workplan returns to `active`. +- Credential lane `issue-core-ingestion-api-key` is finished (`RAILIANCE-WP-0009`). + Railiance runtime ConfigMap already sets `ISSUE_SINK_TYPE=rest` and + `ISSUE_CORE_URL` via `actcore-issue-core-bridge`. +- `weekly-sbom-staleness` / `flag-stale-sbom` is the canonical promotion + candidate. Null-sink dry-run and live `IssueCoreRestSink` smoke passed via + `scripts/smoke_issue_core_emission.py` (local issue-core with `default: local` + backend returned a real `sqlite` issue id, not `null-*`). +- Promotion and rollback steps are documented in + `docs/issue-core-emission-boundary.md`; Railiance README and + `bootstrap-secrets.sh` now call out the `ISSUE_CORE_API_KEY` field on + `actcore-runtime-secret`. +- Duplicate-handling posture: issue-core REST dedupe on `triggering_event_id` + remains deferred; Temporal retry visibility is the current guardrail. +- Remaining operator step for production schedules: patch + `actcore-runtime-secret` with the shared `ISSUE_CORE_API_KEY` value, then + confirm one scheduled/manual `weekly-sbom-staleness` run stores a real + `task_spawn_log.task_ref`. ## Resolve Review-Required Contract Drift @@ -239,19 +251,19 @@ item chooses hostname, auth layer, allowed users/agents, and audit expectations. Agent-actionable closure is complete for T03, T04, T05, and T06. -Remaining waits: +## Closeout - 2026-07-07 -- T02 waits on issue-core production endpoint/credentials and duplicate-handling - approval. +All tasks are done. T02 promotion artifacts landed in-repo; production live +emission still needs the operator-owned `ISSUE_CORE_API_KEY` patch on +`actcore-runtime-secret`. Verification: ```bash -.venv/bin/pytest tests/test_issue_sink.py tests/rules/test_executor.py -k "review_required or issue_core_rest_sink" +uv run python scripts/smoke_issue_core_emission.py +uv run pytest tests/test_issue_sink.py tests/test_integration_event_bridge.py tests/rules/test_executor.py -k "review_required or issue_core_rest_sink or sbom" ``` -Result: 3 passed, 24 deselected. - After this workplan is synced by the custodian operator, run from `~/state-hub`: ```bash