Implement ACTIVITY-WP-0022/0023: safe sink default and gap closures
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 3s
Build and Publish Container Image / build-and-push (push) Successful in 28s

Default ISSUE_SINK_TYPE to state-hub (no silent Forgejo issues), hard-fail
prune apply without live-images protection, refresh-live-images script,
disable TaskExecutor stub by default, and document consumer/sink contracts.
This commit is contained in:
tegwick 2026-07-21 21:40:08 +02:00
parent 5c7a90ce7c
commit 4f5399df84
19 changed files with 525 additions and 155 deletions

View file

@ -1,19 +1,30 @@
# Issue-Core Emission Boundary
# Task emission boundary (IssueSink)
activity-core owns the decision to spawn a task and the audit trail that says
why it spawned. It does not own downstream task lifecycle state after emission.
## Current authoritative endpoint
## Sink matrix (ACTIVITY-WP-0022)
The current authoritative boundary is the issue-core REST API:
| `ISSUE_SINK_TYPE` | Destination | Default? |
| --- | --- | --- |
| **`state-hub`** | State Hub progress `activity_task_spawn` | **Yes** (safe for internal findings) |
| **`null`** | Synthetic `null-*` refs only | Dry-run / tests |
| **`rest`** | issue-core `POST /issues/` (may → Forgejo) | **Explicit opt-in only** |
Code default when unset: **`state-hub`** (`DEFAULT_ISSUE_SINK_TYPE`).
Unknown values fall back to `state-hub` and log a warning.
Consumer contract (fields, idempotency, Binky completion):
`docs/task-emission-consumer-contract.md`.
## REST endpoint (opt-in)
```text
POST {ISSUE_CORE_URL}/issues/
```
`IssueCoreRestSink` authenticates with the shared `ISSUE_CORE_API_KEY` env var
(same value as the issue-core server) via `Authorization: Bearer <key>` and
sends this payload:
`IssueCoreRestSink` uses `ISSUE_CORE_API_KEY` via `Authorization: Bearer <key>`:
```json
{
@ -30,110 +41,57 @@ sends this payload:
}
```
The expected response contains `issue_id` and may include `issue_url` and
`backend`. activity-core stores only the returned task reference in
`task_spawn_log`; issue-core remains authoritative for task status, assignment,
comments, closure, and cancellation.
Response: `issue_id` (required), optional `issue_url`, `backend`. Stored in
`task_spawn_log` only as a reference.
## State Hub sink (default)
Each TaskSpec becomes a progress event (`activity_task_spawn` by default).
No Forgejo issue is created. Suitable for Binky rhythm, internal hygiene, and
any definition that should not spam trackers.
## REST versus NATS
Keep REST as the active emission contract until issue-core publishes and owns a
durable NATS consumer for task-creation commands. NATS is still appropriate for
event intake into activity-core, but task creation needs an acknowledged,
idempotent command boundary. A future NATS sink must return or later correlate a
task reference before it can replace `IssueCoreRestSink`.
Keep REST as the issue-core transport until issue-core owns a durable NATS
command consumer. NATS remains appropriate for **event intake** into
activity-core.
## Safe operating modes
## Operating modes
- `ISSUE_SINK_TYPE=null`: dry-run/audit mode. Task specs are rendered and the
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. Railiance runtime ConfigMap uses this mode once
`ISSUE_CORE_API_KEY` is present in `actcore-runtime-secret`.
- `ISSUE_SINK_TYPE=state-hub`: ACTIVITY-WP-0021 path B. Each TaskSpec is posted
as a State Hub progress event (`activity_task_spawn` by default) instead of
creating a Forgejo issue. Use when issue-core→Forgejo is down or automated
Forgejo issues are policy-blocked.
- **`state-hub` (default):** fleet-visible spawns without Forgejo.
- **`null`:** dry-run / audit.
- **`rest`:** live issue-core; requires healthy backend and intentional policy.
Railiance production uses `state-hub` unless an overlay explicitly sets
`rest` for an experiment.
### Known production failure (2026-07-21)
### Known production failure (2026-07-21) — rest path
`POST /issues/` returned **HTTP 503** with:
`POST /issues/` returned **HTTP 503**:
```text
Failed to connect to backend 'forgejo-inbox': Failed to connect to Gitea API
```
Root cause on coulombcore issue-core: `GITEA_BACKEND_TOKEN` is rejected by
Forgejo (`/api/v1/user` → 401 user does not exist). Healthz stays 200.
`GITEA_BACKEND_TOKEN` on issue-core rejected by Forgejo. Fix is issue-core
token rotation (`warden route show issue-core-ingestion-api-key`), not
activity-core defaults.
**Operator fix (path A):** rotate `GITEA_BACKEND_TOKEN` in OpenBao path
`platform/workloads/issue-core/issue-core/issue-core-runtime` (see
`warden route show issue-core-ingestion-api-key`) using a valid Forgejo PAT for
the issue-core service identity, then restart `issue-core` so the entrypoint
rewrites backends.json. Smoke from the worker:
## Promotion to rest (one definition at a time)
```bash
# From actcore-worker (does not print secrets)
python -c "import os,httpx; r=httpx.post(os.environ['ISSUE_CORE_URL']+'/issues/',
json={...full TaskSpec payload...},
headers={'Authorization':'Bearer '+os.environ['ISSUE_CORE_API_KEY']}, timeout=30);
print(r.status_code, r.text[:200])"
```
Expect **201**, not 503.
Weekly SBOM staleness now posts a deterministic `sbom_staleness` progress report
even when task emission is disabled.
## 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`, apply
`k8s/railiance/15-externalsecret-issue-core.yaml` so External Secrets merges
`ISSUE_CORE_API_KEY` into `actcore-runtime-secret`, and restart
`actcore-worker` / `actcore-event-router` after the ExternalSecret is Ready.
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.
1. Dry-run with `null` or observe `state-hub` spawns.
2. Confirm issue-core smoke returns **201**.
3. Temporarily set `ISSUE_SINK_TYPE=rest` only if policy allows external issues.
4. Prefer per-definition future opt-in over global rest (WP-0022).
5. Roll back: `ISSUE_SINK_TYPE=state-hub` or `null` + worker restart.
## Verification
Local contract tests cover the rendered weekly SBOM task path and the REST
payload shape:
```bash
uv run pytest tests/test_integration_event_bridge.py tests/test_issue_sink.py
uv run pytest tests/test_issue_sink.py tests/test_integration_event_bridge.py -q
```
For a live environment, run with `ISSUE_SINK_TYPE=null` first and confirm
`task_spawn_log` contains the expected source id, condition, triggering event id,
and synthetic task reference. Then switch to `ISSUE_SINK_TYPE=rest` only after a
single known-safe rule match creates one issue-core task with the same fields.
## Side-effect resolvers (not IssueSink)
`forgejo_package_prune` with `apply: true` is a **declared platform side-effect**,
not task emission. It requires a non-empty `live_images_file` (ACTIVITY-WP-0023-T03).
See runbook § Weekly maintenance.