--- id: ACTIVITY-WP-0024 type: workplan title: "Operator automation console (status API + control plane + thin UI)" domain: infotech repo: activity-core status: finished owner: codex topic_slug: activity-core created: "2026-07-21" updated: "2026-07-21" state_hub_workstream_id: "555c6bb2-73e2-579b-837b-a7df0daad621" --- # Operator automation console (status API + control plane + thin UI) ## Origin 2026-07-21 operator discussion after ACTIVITY-WP-0023 closeout. Operators need a first-class surface for scheduled automations without SSH + SQL + Temporal UI as the only path. Builds on: - **ACTIVITY-WP-0018** — own-infra automation status contract + CLI - **ACTIVITY-WP-0019** — automation inventory Make targets - **ACTIVITY-WP-0021** — prod automation status path (SSH helper) Locked product decisions (founder): | Fork | Choice | | --- | --- | | Surface | API-first, then thin UI **in activity-core** | | Auth / exposure | Port-forward + operator token; mutations **fail-closed** | | Mutations in MVP | **Run now + enable/disable/pause only** (no cron patch) | ## Goal Ship an **operator automation console**: HTTP APIs (and a thin UI) so an operator can: 1. See whether regular scheduled jobs ran properly in a time window 2. Review results (runs → evidence: tasks spawned, progress refs, report paths) 3. Run a defined activity instantly 4. Enable / disable / pause schedules without SSH + SQL This is **not** an end-user task tracker and **not** a second scheduler. Durable scheduling remains Temporal + ActivityDefinitions; the console is a consumer and control plane over that truth. ## Out of scope - Cron / timezone patching in the UI (schedules stay git-owned; edit files + sync) - Authoring or editing rules / instructions / templates in the UI - Public Ingress or full OIDC/key-cape (follow-up after MVP) - Hosting the console in the state-hub dashboard - Task lifecycle UI (create/assign/close tasks) - Replacing Temporal UI for workflow debugging (deep-link only) ## Architecture sketch ``` Operator (browser / curl) │ port-forward svc/actcore-api │ header: X-Operator-Token (or agreed scheme) ▼ actcore-api GET /ops/automations GET /ops/automations/status?since=&until= GET /ops/automations/{id} GET /ops/automations/{id}/runs POST /ops/automations/{id}/trigger POST /ops/automations/{id}/enable|disable|pause|unpause GET /ops/ui/… (static/HTMX after API stable) │ ├─ activity_core.automation_status + inventory ├─ activity_runs + activity_definitions ├─ Temporal schedule describe / pause └─ audit (progress event or ops_audit; no secrets) ``` Deep-links into State Hub / edge when evidence lives there (G9-aware). ## Auth policy (MVP) - Mutations require an operator token when `ACTIVITY_CORE_OPERATOR_TOKEN` (or ESO-backed secret) is configured. - If the token is **unset** under production posture, **refuse mutations** (fail-closed). Do not leave unauthenticated Run now on a reachable API. - Token never logged or returned by API. - Access path: `kubectl -n activity-core port-forward` to the API Service. - Ingress + OIDC is an explicit follow-up, not an MVP gate. - Credential custody via `warden route` / OpenBao — never git, chat, or workplan. ## Mutation policy (MVP) | Action | Behavior | | --- | --- | | Run now | Wrap existing trigger; return `workflow_id`; UI confirms for side-effect defs | | Disable / pause | Coherent model: definition enabled flag and/or Temporal schedule paused | | Enable / unpause | Inverse | | Change cron / timezone | **Out of MVP** — definition files + sync; UI shows schedule read-only | ## Tasks ## Task: Product brief and SCOPE clarification ```task id: ACTIVITY-WP-0024-T01 status: done priority: high state_hub_task_id: "697113b1-793e-59b4-a86c-ecb22ad127b6" ``` 1. Write a short product brief (personas, jobs-to-be-done, non-goals) in this workplan or `docs/` if it outgrows the file. 2. Update `SCOPE.md`: **operator automation console is in scope**; end-user task management remains out of scope. Clarify G10 (auth MVP vs public access). 3. Cross-link WP-0018/0019 as the evidence/inventory contracts this console wraps. **Done when:** SCOPE and this workplan tell one story; non-goals are explicit. ## Task: Operator token auth and fail-closed mutations ```task id: ACTIVITY-WP-0024-T02 status: done priority: high state_hub_task_id: "0e709610-2599-593c-9341-1ca5b6ddd23e" ``` 1. Design operator token check (e.g. `X-Operator-Token` header compared to env/secret) for all `/ops` **mutations**. 2. Fail-closed: when production requires auth and token is missing/misconfigured, mutations return 401/403 (not silent open). 3. Document env name, ESO/bootstrap option, and port-forward access in runbook. 4. Never put the token value in git, logs, or State Hub detail. **Done when:** unauthenticated mutation is rejected under the configured posture; runbook describes custody without leaking secrets. ## Task: REST inventory and status window ```task id: ACTIVITY-WP-0024-T03 status: done priority: high state_hub_task_id: "8adf73b7-d23e-5110-8dc4-a22fc0bde448" ``` 1. `GET /ops/automations` — inventory equivalent to WP-0019 (cron/scheduled defs, enabled, schedule expression, Temporal schedule id / paused hints). 2. `GET /ops/automations/status?since=&until=` — wraps WP-0018 automation-status semantics (JSON). 3. `GET /ops/automations/{id}` — single definition + schedule visibility. 4. Prefer reusing `activity_core.automation_status` / inventory modules; do not fork a second evidence contract. **Done when:** curl against the API matches CLI status/inventory for the same window within documented field mapping. ## Task: REST run history and evidence summary ```task id: ACTIVITY-WP-0024-T04 status: done priority: high state_hub_task_id: "26d50f76-67cc-5719-a94a-b2de9c1a1495" ``` 1. `GET /ops/automations/{id}/runs?since=` (and optional global runs list). 2. Each run: `run_id`, `scheduled_for`, `fired_at`, `tasks_spawned`, version, and evidence pointers when known (progress event types/ids, report paths). 3. No secrets, raw LLM output, or full prompts in responses. **Done when:** operator can list recent runs for a definition without SSH/SQL. ## Task: REST Run now and enable/disable/pause with audit ```task id: ACTIVITY-WP-0024-T05 status: done priority: high state_hub_task_id: "aff1ddc5-4e78-5f6f-98da-2b2e0af5f2f9" ``` 1. `POST /ops/automations/{id}/trigger` — wrap existing trigger path; require operator token. 2. `POST .../enable|disable|pause|unpause` — one coherent model documented in the PR (definition flag vs Temporal pause; prefer both consistent). 3. Audit trail: who/when/what (operator label optional; never store token). Prefer State Hub progress `event_type` or a small `ops_audit` table. 4. Side-effect definitions (e.g. forgejo prune apply): document confirmation expectation for UI; API may accept an explicit `confirm_side_effect=true`. **Done when:** authenticated Run now returns `workflow_id` and produces run evidence; pause/disable visible in inventory/status; unauthenticated mutate fails. ## Task: Contract and security tests ```task id: ACTIVITY-WP-0024-T06 status: done priority: high state_hub_task_id: "0c07917b-b195-5cbe-a739-bd3a1ae752bb" ``` 1. Contract tests for inventory/status/runs JSON shapes. 2. Auth tests: missing token → reject mutations; valid token → allow. 3. Response fixtures scanned so secrets/token values never appear. **Done when:** CI/unit tests cover the above without a live cluster. ## Task: Thin operator UI on actcore-api ```task id: ACTIVITY-WP-0024-T07 status: done priority: medium state_hub_task_id: "d2002e29-a1c5-54ff-b2a8-e63db7fe22bb" ``` 1. Static/HTMX (or minimal SPA) served from actcore-api under `/ops/ui/`. 2. Screens: automation list, status window, run history, Run now + enable/pause actions (token supplied via header or session form — no token in URLs). 3. Schedule fields **read-only** in MVP. 4. Confirm dialog for side-effect activities. **Done when:** usable via port-forward for the MVP jobs-to-be-done. ## Task: Prod access path (no public Ingress) ```task id: ACTIVITY-WP-0024-T08 status: done priority: medium state_hub_task_id: "3b595a46-38e0-51e1-bfee-814de684f8f5" ``` 1. Document port-forward (or equivalent) for railiance01. 2. Wire operator token into runtime secret path (bootstrap or ESO) without committing values. 3. Explicitly **do not** add public Ingress in this workplan. 4. Optional: NetworkPolicy note that API remains ClusterIP. **Done when:** runbook has a copy-paste operator access path; G10 public access still deferred with a clear follow-up note. ## Task: Runbook daily checklist mapping ```task id: ACTIVITY-WP-0024-T09 status: done priority: low state_hub_task_id: "36a3cac3-388b-5f58-94ca-814e494d08f3" ``` 1. Map “How did automations go since Sunday?” to `/ops/automations/status` and UI. 2. Map “Run X now” and “pause X” to mutation endpoints. 3. Keep CLI (`make automation-status`) as valid offline/alternate path. **Done when:** runbook section points operators at console first, CLI second. ## Success criteria - [x] `GET /ops/automations/status?since=…` matches CLI automation-status semantics - [x] Operator can list last N runs for a definition without SSH/SQL - [x] Authenticated Run now returns workflow_id and produces `activity_runs` evidence - [x] Pause/disable visible in inventory/status and Temporal schedule state - [x] Unauthenticated mutation returns 401/403 when token is configured - [x] Thin UI usable via port-forward for the above flows - [x] Runbook documents token custody without secrets in git ## Implementation order 1. T01 SCOPE/brief 2. T02 auth + T03/T04/T05 API (before UI) 3. T06 tests green 4. T07 thin UI 5. T08/T09 docs + prod token wiring 6. Deploy railiance01 image; no Ingress ## References - Plan: session plan “Operator dashboard for activity-core scheduled automations” - `src/activity_core/automation_status.py`, `src/activity_core/api.py` - `docs/runbook.md`, `SCOPE.md` G10 - ACTIVITY-WP-0018, ACTIVITY-WP-0019, ACTIVITY-WP-0021 ## Closeout 2026-07-21 Implemented `/ops` console: auth (fail-closed operator token), inventory/status/runs REST, Run now + enable/disable/pause with audit buffer + optional State Hub progress, thin HTML UI at `/ops/ui`, runbook + k8s access docs. Tests: `tests/test_ops_auth.py`, `tests/test_ops_console_api.py`.