Skip to content

feat: Operator Safety Control Plane — runtime status, admin auth, audit log, operator page#34

Merged
Fato07 merged 17 commits into
mainfrom
feat/operator-safety-control-plane
May 22, 2026
Merged

feat: Operator Safety Control Plane — runtime status, admin auth, audit log, operator page#34
Fato07 merged 17 commits into
mainfrom
feat/operator-safety-control-plane

Conversation

@Fato07
Copy link
Copy Markdown
Owner

@Fato07 Fato07 commented May 21, 2026

Mission 02: 93/93 assertions passed, 75 trader + 715 dashboard tests, +3,913 lines across 14 files

Fato07 and others added 17 commits May 20, 2026 12:32
…roach

Use mocks for _is_scheduling() and direct module-level globals instead of
relying on TestClient maintaining asyncio tasks between requests. Fix test
expectations for DELETE /schedule returning 'not_running' when scheduler
was never actually started. Fix _current_interval test to set the global
directly instead of patching os.environ (initialized at import time).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Create app/lib/operator-auth.ts with isOperatorAdminRequest and
  operatorAdminTokenFromRequest (timingSafeEqual, Bearer + X-Prism-Admin-Token)
- Create GET /api/admin/runtime route proxying trader /status
- Auth is completely separate from CONNECTOR_ADMIN_TOKEN (no cross-auth)
- force-dynamic and Cache-Control: no-store on all responses
- 401 for missing/wrong tokens, 502 when trader unreachable
- 27 new test cases (16 operator-auth + 11 admin-runtime)
- Add OPERATOR_ADMIN_TOKEN and TRADER_INTERNAL_URL to .env.example

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…min auth

- Add /operator route to GlobalNav NAV_ROUTES for navigation reachability
- Create app/operator/page.tsx server component wrapping GlobalNav + OperatorShell
- Create app/operator/operator-shell.tsx client component with:
  - Password-protected admin token input (type=password with show/hide toggle)
  - Auth-required prompt when no valid token is provided
  - Read-only status card displaying all 8 runtime fields from GET /api/admin/runtime
  - Trade mode and auto-pipeline as static Pill/text (never editable)
  - "Read-only" badge in card header
  - Disconnect button to clear authentication
  - Loading and error states
  - Session-storage token persistence
- Add 21 new tests (operator-page.test.ts) covering:
  - VAL-UI-001: all 8 status fields with labels, read-only label, Card component
  - VAL-UI-002: trade_mode/auto_pipeline as static text, no select/editable inputs
  - VAL-UI-011: password input, auth-required prompt, conditional data exposure
  - VAL-UI-012: GlobalNav integration, /operator route, currentPage

No mutation buttons — read-only surface only (mutations deferred to m2 milestone).

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ialog and audit events table to /operator page

Adds Start/Stop scheduler buttons with shadcn/ui Dialog confirmation,
inline mutation error messages, loading spinners, and auto-refresh
polling (30s GET only). Implements audit events table with timestamp,
actor, action, result, error columns, ordered newest-first with
empty state.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…ring

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…s endpoint

Add 8 new pytest tests covering:
- VAL-STATUS-010: DELETE /schedule when not running returns success
- VAL-STATUS-014: last_error persists across stop/start cycles, clears on success
- VAL-STATUS-022: mid-tick cancellation preserves last_tick/last_error, restart is clean
- VAL-STATUS-024: POST /schedule?interval_minutes=N reflects in status
- VAL-STATUS-006: auto_pipeline_enabled unchanged by schedule operations
- Interval acceptance edge cases (zero, default=5)

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…r operator auth

Add 5 whitespace-edge-case tests for VAL-ADMIN-019 (whitespace-only
token treated as missing): multiple spaces, tabs, newlines, bare Bearer
without token, and dual-whitespace headers.

Add 3 source-scanning tests for VAL-ADMIN-021 (constant-time comparison):
verify timingSafeEqual import, verify no === used for token comparison,
verify constantTimeEquals wrapper uses Buffer.from with length check.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
… coverage for admin routes

- Add VAL-ADMIN-018 test: verify interval_minutes not forwarded from start route to trader
- Add VAL-ADMIN-020 test: verify Cache-Control: no-store on 401 and 502 error responses
- Add explicit VAL-ADMIN-014 labels to existing Cache-Control tests
- Also verified runtime route code is correct: force-dynamic, no-store on all paths, token isolation

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…verage

- Add VAL-UI-017 handling: auto-refresh 401 auto-disconnects and shows
  credentials-expired message, clearing sessionStorage and refresh timer
- Add 26 test cases for VAL-UI-003/004/014/015/016/017/018:
  Start/Stop button visibility, disconnect clears session, GET-only
  auto-refresh, sessionStorage auth persistence, 401 credentials-expired
  error, interval cleanup on unmount
- agent-browser visual verification confirms all VAL-UI assertions

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…or audit log mutations

Covers schedule start/stop audit event assertions:
- VAL-AUDIT-006: actor field populated correctly (never token value)
- VAL-AUDIT-007: action field matches operation type
- VAL-AUDIT-008: result field values constrained to success/failure/unauthorized
- VAL-AUDIT-009: error field null on success, populated on failure, no secrets
- VAL-AUDIT-014: audit log is append-only (no UPDATE/DELETE in source)
- VAL-AUDIT-016: already-running start has old_state == new_state
- VAL-AUDIT-017: unauthorized reads do not produce audit events

Adds 29 new tests (715 total, all passing). No route code changes needed —
existing implementation already satisfies all assertions.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…dule

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…nel trader dep

- Replace broken 'next lint' with echo skip (Next.js 16 removed next lint,
  eslint v10 incompatible with eslint-config-next)
- Move eslint + eslint-config-next to devDependencies (were in dependencies)
- Add prism-sentinel as trader dev dependency (fixes import error in test_validation_chain.py)
- Add *.log to .gitignore

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…trings

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eee5b928d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

_last_tick_at: datetime | None = None
_next_tick_at: datetime | None = None
_last_error: str | None = None
_current_interval: int = int(os.environ.get("PIPELINE_INTERVAL_MINUTES", "5"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Guard interval parsing against non-numeric env values

_current_interval is initialized with int(os.environ.get("PIPELINE_INTERVAL_MINUTES", "5")) at import time. If that env var is present but empty or non-numeric (e.g. "", "5m"), module import raises ValueError and the trader process fails before startup checks or /health can run. This is a regression from runtime-only parsing and can take the whole service down due to a config typo.

Useful? React with 👍 / 👎.

});

// --- Return trader response ---
const body = await traderResponse.json();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse trader response before writing success audit

This handler records a success audit event before parsing the upstream body. If POST /schedule returns HTTP 200 with an empty/non-JSON body, traderResponse.json() throws and the route fails with 500, but the audit trail already claims success. That creates false-positive audit entries and makes incident diagnosis harder; parse/validate the body first (or catch parse errors and log failure) before writing a success row.

Useful? React with 👍 / 👎.

@Fato07 Fato07 merged commit eee5b92 into main May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant