# 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. ## Sink matrix (ACTIVITY-WP-0022) | `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` uses `ISSUE_CORE_API_KEY` via `Authorization: Bearer `: ```json { "title": "Run SBOM rescan for activity-core", "description": "", "target_repo": "activity-core", "priority": "medium", "labels": ["sbom", "security", "automated"], "due_in_days": null, "source_type": "rule", "source_id": "flag-stale-sbom", "triggering_event_id": "event-or-schedule-key", "activity_definition_id": "activity-definition-uuid" } ``` 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 issue-core transport until issue-core owns a durable NATS command consumer. NATS remains appropriate for **event intake** into activity-core. ## Operating modes - **`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, reconfirmed 2026-07-21 T06) — rest path `POST /issues/` returns **HTTP 503**: ```text Failed to connect to backend 'forgejo-inbox': Failed to connect to Gitea API ``` **Disposition (ACTIVITY-WP-0023-T06):** activity-core keeps global default `state-hub` and does **not** flip production to `rest`. Path A is owned by **issue-core**: rotate/fix `GITEA_BACKEND_TOKEN` (Forgejo backend PAT for the forgejo-inbox connector — not the activity-core `ISSUE_CORE_API_KEY` ingestion key). After issue-core proves `POST /issues/` → **201**, operators may opt in per definition / overlay only (WP-0022). Smoke from worker (does not change sink env): ```bash kubectl -n activity-core exec deploy/actcore-worker -- python -c ' # valid IssueCore body → expect 201 when path A is healthy, else 503 forgejo-inbox ' ``` ## Promotion to rest (one definition at a time) 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 ```bash uv run pytest tests/test_issue_sink.py tests/test_integration_event_bridge.py -q ``` ## 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.