Finish ACTIVITY-WP-0009 issue-core task emission promotion
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 53s
Build and Publish Container Image / build-and-push (push) Successful in 51s

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.
This commit is contained in:
tegwick 2026-07-07 22:30:46 +02:00
parent d563d516f5
commit 1f1b698c35
7 changed files with 217 additions and 32 deletions

View file

@ -180,10 +180,13 @@ The two evaluation modes:
yet evidence for three clean consecutive scheduled runs after the June 7 yet evidence for three clean consecutive scheduled runs after the June 7
runtime projection failure. The ops inventory probe path has live fallback runtime projection failure. The ops inventory probe path has live fallback
evidence in State Hub; Inter-Hub per-entity submission remains deferred. evidence in State Hub; Inter-Hub per-entity submission remains deferred.
- **Task emission posture**: the issue-core REST sink is implemented, but the - **Task emission posture**: the issue-core REST sink is implemented and
Railiance runtime currently uses `ISSUE_SINK_TYPE=null` dry-run/audit mode. Railiance runtime ConfigMap sets `ISSUE_SINK_TYPE=rest`. The OpenBao ingestion
Switching to live issue-core task creation requires a verified endpoint, lane (`RAILIANCE-WP-0009`) is finished; live emission still requires the
credentials, and duplicate-handling check in the target environment. 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 - **Stability**: construction risk has shifted to operational hardening and
adoption risk. The last recorded full-suite pass in the workplans was 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 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 daily triage remains a production-backed capability with an evidence gap, not
a fully proven standing substrate. a fully proven standing substrate.
- **Task creation gap**: INTENT says activations emit task creation requests to - **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` issue-core. The REST sink and promotion runbook are in place, and the null-sink
mode. That preserves auditability and avoids accidental duplicate/live tasks, plus live smoke path is proven for `weekly-sbom-staleness`. Production still
but it means production schedules are not yet consistently creating real needs `ISSUE_CORE_API_KEY` synced into `actcore-runtime-secret` before
issue-core tasks. scheduled runs create real issue-core tasks instead of failing sink emission.
- **Review queue gap**: `review_required` is explicitly metadata only in the - **Review queue gap**: `review_required` is explicitly metadata only in the
current contract. No issue-core review queue integration exists here, so any current contract. No issue-core review queue integration exists here, so any
future queue routing needs a downstream issue-core contract before high-impact future queue routing needs a downstream issue-core contract before high-impact

View file

@ -46,16 +46,52 @@ task reference before it can replace `IssueCoreRestSink`.
## Safe operating modes ## Safe operating modes
- `ISSUE_SINK_TYPE=null`: dry-run/audit mode. Task specs are rendered and the - `ISSUE_SINK_TYPE=null`: dry-run/audit mode. Task specs are rendered and the
workflow records synthetic `null-*` references. This is the current Railiance workflow records synthetic `null-*` references. Use this for contract review
production setting. and emergency rollback.
- `ISSUE_SINK_TYPE=rest`: live task creation. Sink failures raise out of - `ISSUE_SINK_TYPE=rest`: live task creation. Sink failures raise out of
`emit_tasks`, so Temporal retries and the workflow history make failures `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 Weekly SBOM staleness is the canonical promotion candidate because the rule
contract is deterministic and tested. Do not enable it against the real REST sink contract is deterministic and tested. Promote it only after a null-sink dry-run
until `ISSUE_CORE_API_KEY`, endpoint reachability, and duplicate-handling are review and one live `IssueCoreRestSink` smoke against the target endpoint.
verified in the target environment.
## 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/<id>/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 ## Verification

View file

@ -520,7 +520,9 @@ Handle in the next operator session:
calibration. calibration.
- Non-urgent schedule jitter or timeout adjustments. - Non-urgent schedule jitter or timeout adjustments.
- Moving a task sink from `ISSUE_SINK_TYPE=null` to the real issue-core endpoint - 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 ### DB migration drift
```bash ```bash

View file

@ -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 `OPS_HUB_KEY` is created only as an empty Secret placeholder until the operator
provisions the Inter-Hub ops-hub key. 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 The same runtime projection now includes the active
`daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema `daily-statehub-wsjf-triage.md` ActivityDefinition plus its JSON output schema
and a persistent working-memory volume mounted at and a persistent working-memory volume mounted at

View file

@ -37,5 +37,9 @@ if ! secret_exists actcore-runtime-secret; then
--from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \ --from-literal=ACTCORE_DB_URL="$ACTCORE_DB_URL" \
--from-literal=WEBHOOK_SECRET_GITEA="" \ --from-literal=WEBHOOK_SECRET_GITEA="" \
--from-literal=WEBHOOK_SECRET_GITHUB="" \ --from-literal=WEBHOOK_SECRET_GITHUB="" \
--from-literal=OPS_HUB_KEY="" --from-literal=OPS_HUB_KEY="" \
--from-literal=ISSUE_CORE_API_KEY=""
fi 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.

View file

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

View file

@ -4,11 +4,11 @@ type: workplan
title: "Intent gap closure" title: "Intent gap closure"
domain: custodian domain: custodian
repo: activity-core repo: activity-core
status: active status: finished
owner: codex owner: codex
topic_slug: custodian topic_slug: custodian
created: "2026-06-16" created: "2026-06-16"
updated: "2026-07-03" updated: "2026-07-07"
state_hub_workstream_id: "d64cfbba-6da7-4737-afb9-866afa0e9cda" state_hub_workstream_id: "d64cfbba-6da7-4737-afb9-866afa0e9cda"
--- ---
@ -83,7 +83,7 @@ gap is closed. T01 is done.
```task ```task
id: ACTIVITY-WP-0009-T02 id: ACTIVITY-WP-0009-T02
status: wait status: done
priority: high priority: high
state_hub_task_id: "3854677b-32b4-43f8-a6ca-5a2b25a08dd9" state_hub_task_id: "3854677b-32b4-43f8-a6ca-5a2b25a08dd9"
``` ```
@ -102,13 +102,25 @@ Acceptance criteria:
- `task_spawn_log` records the real returned task references - `task_spawn_log` records the real returned task references
- rollback to null-sink mode is documented - rollback to null-sink mode is documented
Current wait reason: production Railiance currently uses null-sink audit mode; 2026-07-07 closeout:
live issue-core credentials/access and duplicate-handling are not yet verified
for this repo.
2026-07-03 reevaluation: this is an external-credential gate, not a workplan - Credential lane `issue-core-ingestion-api-key` is finished (`RAILIANCE-WP-0009`).
blocker. Keep T02 at `wait` with `blocking_reason` describing the issue-core Railiance runtime ConfigMap already sets `ISSUE_SINK_TYPE=rest` and
approval path; the workplan returns to `active`. `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 ## 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. 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 All tasks are done. T02 promotion artifacts landed in-repo; production live
approval. emission still needs the operator-owned `ISSUE_CORE_API_KEY` patch on
`actcore-runtime-secret`.
Verification: Verification:
```bash ```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`: After this workplan is synced by the custodian operator, run from `~/state-hub`:
```bash ```bash