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.
This commit is contained in:
parent
d563d516f5
commit
1f1b698c35
7 changed files with 217 additions and 32 deletions
19
SCOPE.md
19
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
|
||||
|
|
|
|||
|
|
@ -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/<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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
121
scripts/smoke_issue_core_emission.py
Executable file
121
scripts/smoke_issue_core_emission.py
Executable 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue