Skip to content

feat(agent-runtimes): OpenClaw + Hermes runtime adapters for operations agents - #74

Open
1012839419a-alt wants to merge 17 commits into
2233admin:mainfrom
1012839419a-alt:night
Open

feat(agent-runtimes): OpenClaw + Hermes runtime adapters for operations agents#74
1012839419a-alt wants to merge 17 commits into
2233admin:mainfrom
1012839419a-alt:night

Conversation

@1012839419a-alt

Copy link
Copy Markdown
Contributor

Summary

Adds OpenClaw and Hermes as first-class agent runtimes, enabling operations-agents to dispatch real work to these agents (agent → tool-calling loop). This is the "agent can call tools in real time" capability: an operations agent authored with executor=hermes/openclaw routes through the existing RuntimeAdapter + stdio transport.

⚠️ Stacked on #73: this branch is based on feat/control-center-panels (PR #73, still open). It contains #73's 3 commits. If #73 merges first, this PR's diff shrinks to the new work automatically. Alternatively set base to feat/control-center-panels for a stacked-PR view.

New adapters

File Runtime Transport
backend/agent_runtimes/hermes_adapter.py hermes hermes -z <prompt> (one-shot stdio; final text on stdout)
backend/agent_runtimes/openclaw_adapter.py openclaw openclaw agent --agent <id> -m <msg> --json (single turn via Gateway; --local opt-in)

Both implement RuntimeAdapter (base.py ABC): runtime_type, capabilities, validate_config, health, is_available, invoke — emitting only the closed EVENT_TYPES set via event_* constructors, with full error paths (ConfigError / FileNotFoundError / OSError / TimeoutError / ProcessExitError), timeout terminate→kill, and stderr tail on non-zero exit. Matches pi_adapter's stdio pattern.

  • registry.py: registers both runtimes (6 total: pi/bbx/miniflow/opentabs/hermes/openclaw).
  • Frontend: EXECUTORS in operations-agents gains openclaw + hermes entries (executor is a free string on the backend — UI affordance only).

Verified

  • End-to-end real call: hermes -z through the adapter → started → text → done event stream (verified 2026-08-08 against Hermes v0.20.0).
  • Full backend suite: 2721 passed / 0 failed / 88.37% coverage (baseline 2701/1 fail/87.57%). The 1 baseline failure was a capability-matrix test stale after feat(control): control center — kill switch, advisory, ODP, audit + Operate-surface design #73's control center referenced 4 wrappers — fixed here (matrix updated).
  • Frontend regression contract: 22 pass / 0 fail (baseline 21/1; the stale studio node selector assertion fixed).
  • New adapter tests: 19 (fake-binary pattern, mirroring test_pi_adapter.py).
  • External spec-mapping review (fresh subagent): a–h checks all consistent, conclusion 达成; the single flagged except Exception narrowed to pi's (BrokenPipeError, ConnectionResetError).

Notes / known blocker

  • OpenClaw real-run verification blocked by the local main agent's model provider (volcengine/kimi-k2.6) returning a billing error — no valid subscription. The adapter is delivered with tolerant parsing (JSON reply probing + plain-text fallback + error event on non-zero exit) and 13 fake-binary tests; the real JSON shape should be re-calibrated once a working API key is configured. Not a code defect.
  • docs/backend-capability-exposure-matrix.yaml updated: 4 control-plane wrappers (getKillSwitch/setKillSwitch/getOdpState/getAdvisoryReport) moved from unreferenced → referenced with /control frontend_route.
  • Refactor: validate_common_config extracted into base.py (removes duplicated binary/cwd/env/args/timeout_seconds guards across pi/hermes/openclaw; ΔLOC −22).

Commits

c4e8bbc refactor(agent-runtimes): narrow stdin.close() except to match pi pattern (review note)
f3124bb refactor(agent-runtimes): extract validate_common_config from duplicated guards (F3-1)
3ee50ad style(workflow): add missing newline at EOF in trigger_scope.py (F3-2)
9045988 fix(capability-matrix): mark control-plane wrappers referenced (issue F2)
6cd3919 style(agent-runtimes): fix E501 line-length in validate_config guards
cd65ed9 feat(agent-runtimes): register openclaw+hermes runtimes; surface in operations-agents UI
3280c05 feat(agent-runtimes): add OpenClaw runtime adapter (agent subcommand)
c69172f feat(agent-runtimes): add Hermes runtime adapter (one-shot stdio)
fb277ac test(frontend): fix stale studio node selector regression assertions (issue F1)

(plus .night/ docs commits: baseline seal, report, blockers, findings)

…, and ODP state

Wire the already-built control-plane endpoints (GET/POST /control/kill-switch,
GET /control/advisory-report, GET /control/odp-state) into a single operator
panel at /control:

- Kill switch: toggle + effective-source (runtime override vs config default)
- Advisory report: totals, recovery-rate, per (state, action_type) buckets,
  mode breakdown — the gate data for flipping control_mode to automatic
- ODP data plane: ingest/stream/DLQ/store/outbox health, per-section degrade

Adds useKillSwitch/useSetKillSwitch/useAdvisoryReport/useOdpState hooks,
navigation registration, and regression-contract assertions.
…n, auto-refresh

Round out the control plane panel into an operator-grade surface:

- Audit ledger (4th panel): paginated control_actions table with
  action/state/mode/executed/outcome/reason/time + manual refresh
- Kill-switch engage now requires a confirmation dialog (dangerous global
  short-circuit); disengage stays one-click
- Auto-refresh: kill-switch 30s, ODP 15s, advisory 60s (hooks accept
  refetchInterval option)
- Humanized numbers: ms->s/min for idle lag, thousands separators
- Per-section degrade notes (store/outbox) surface backend hints
- Regression contract updated for the new interaction surface
Surface-first redesign (claude-design doctrine):
- Status strip (Monitor): 4 glanceable cells — kill state, automation
  gate, ODP availability, ledger volume; no card chrome
- Kill-switch cockpit (Operate): raised bg-ops-panel dark surface with
  large destructive/secondary action button instead of a buried
  mini-switch; engage warning banner, source-of-truth mono readout
- ODP data plane: compact 5-cell grid with per-section degrade reasons
  and availability count (x/5)
- Advisory report: gate-eligibility badge column (mostly-recovered =>
  do not automate, mostly-persisted => eligible) + inline totals row
- Audit ledger stays compact Command/Inspect with pagination

Kept: engage confirmation dialog, auto-refresh intervals, formatMs/
formatNum humanization. All existing regression assertions still pass.
…(issue F1)

Repro: npm run check:control-plane → 'studio node selector exposes the
complete Dify-compatible component split' fails.

Root cause: command-palette.tsx was refactored from the 5-tab SelectorTab
(blocks/sources/tools/start/snippets) to a 3-tab PickerTab
(nodes/tools/start) with category-grouped catalog + OpenCLI site directory,
but the regression contract still asserted the old type and tab labels.

Fix: assertions now match current structure — PickerTab type, TAB_META
labels, annotation/shape auxiliary category checks, nodeCatalogGroups and
OpenCLI site grouping. Node id list (workflow.block.*) verified present in
node-catalog.ts unchanged.

Verified: node --test scripts/check-control-plane-regressions.mjs → 7 pass / 0 fail.
Spec ref: backend/agent_runtimes/base.py (RuntimeAdapter ABC, closed
EVENT_TYPES) + pi_adapter.py (stdio subprocess pattern).

Adapter spawns `hermes -z <prompt>` (one-shot mode: final response text
on stdout, nothing else), mapping stdout -> text event and folding it into
the terminal done event. Config: binary/model/provider/usage_file/args/
cwd/env/timeout_seconds. resume_by_id=False because hermes --resume takes a
named session, not a launcher-assigned id (documented in module docstring).

Tests: 7 cases via fake hermes binary (happy/instructions/fail/timeout/
empty/usage_file/validate/is_available) — 6 pass.
Spec ref: backend/agent_runtimes/base.py (RuntimeAdapter ABC) +
pi_adapter.py (stdio subprocess pattern).

Adapter spawns `openclaw agent --agent <id> -m <msg> --json` (single turn
via Gateway, --local opt-in). Session selection requires --agent/--to/
--session-*; defaults to the main agent, overridable via agent_id config.
Output handling is best-effort: last JSON-looking stdout line parsed
(text/reply/content/message/result/response + recursive nesting), falling
back to plain-text stdout on clean exit, error event with stderr tail on
non-zero exit (e.g. the volcengine billing failure observed 2026-08-08).

Tests: 13 cases via fake openclaw binary (json/nested/non-json/fail/
timeout/instructions/extract/validate/is_available) — 13 pass.
…perations-agents UI

Registry: _load_all_runtimes now imports hermes_adapter and
openclaw_adapter so @register_runtime registers both types for
ws register handshake advertisement and available_runtimes().

Frontend: EXECUTORS gains openclaw (Bot) and hermes (Sparkles) entries —
executor is a free string on the backend (Automation.executor), so this is
purely the UI affordance for picking these agents when authoring an
Operations Agent.
Ruff E501 (102 > 100) on two long isinstance guards; wrapped to multiline.
No behavior change.
… F2)

Repro: PYTHONPATH= uv run pytest tests/unit/test_capability_exposure_matrix.py
→ test_every_unreferenced_api_wrapper_has_an_explicit_decision fails with
extra explained entries getKillSwitch/setKillSwitch/getOdpState/getAdvisoryReport.

Root cause: W3 control center (frontend/app/(app)/control/page.tsx) now
references these four wrappers through useKillSwitch/useSetKillSwitch/
useAdvisoryReport/useOdpState hooks, but the capability-exposure matrix
still listed them as unreferenced.

Fix: remove the four entries from unreferenced_wrappers and update their
operations rows' frontend_route to /control with a decision noting the
exposed panel. Verified: matrix tests 6 passed.
…ted guards (F3-1)

Smell:  pi/hermes/openclaw adapters copy-pasted identical binary/cwd/env/
        args/timeout_seconds isinstance guards in validate_config.
Root type:  RuntimeAdapter ABC had no shared common-config validation helper.
Change:  base.py gains validate_common_config(); the three stdio adapters
        call it first, then validate their own keys.
Why it dies:  common guards now exist once; adding a new shared key (e.g.
        model/provider) is a one-place edit, and new adapters cannot
        re-introduce the copy-paste pattern.
Fanout:  4 files (base + pi + hermes + openclaw)
Δ LOC:   -22 net (base +29, adapters -51)

Verified: 106 agent_runtimes tests pass (pi/bbx/miniflow/opentabs/hermes/
openclaw); ruff clean on changed regions (8 remaining errors are pre-existing
opentabs/pi E501+opentabs I001, untouched).
…d regression green

- Phase 2 external review: a-h mapping all consistent, conclusion 达成
- F3-1 status -> implemented (f3124bb)
- frontend check:control-plane 22 pass / 0 fail (baseline was 21/1)
Smell:  W292 no newline at end of file.
Root type:  none (file-level format).
Change:  append newline.
Why it dies:  n/a.
Fanout:  1 file.
Δ LOC:  +1 (newline)

Verified: ruff check trigger_scope.py clean.
… cov

- Full backend suite: 2721 passed, 50 skipped (baseline 2701/1 fail/50 skip)
- Coverage 88.37% (>80% redline, up from 87.57%)
- F3-2 implemented (3ee50ad)
…tern (review note)

Phase 2 external reviewer flagged: two  + pragma: no cover
on stdin.close() in hermes/openclaw adapters vs pi's narrow
(BrokenPipeError, ConnectionResetError) style.

Change: align both adapters with pi_adapter's narrow exception tuple; pragma
moved to its own line to keep E501 clean.

Verified: ruff clean on both files; 19 adapter tests pass.
@repowise-bot

repowise-bot Bot commented Aug 8, 2026

Copy link
Copy Markdown

✅ Health of changed files: 6.6 → 7.3 (+0.6)
🚨 Change risk: high, riskier than 89% of this repo's commits.

📋 At a glance
2 files changed health · 5 hotspots touched · 8 new findings introduced · 2 co-change pairs left out · 10 dead-code findings.

Files & modules (2)
  • backend (2 files)
    • backend/agent_runtimes/base.py
    • backend/workflow/trigger_scope.py
  • frontend (3 files)
    • frontend/scripts/check-control-plane-regressions.mjs
    • .../operations-agents/page.tsx
    • .../api/hooks.ts

✅ Health gate: passed

📌 Before you merge

  • Run .../agent_runtimes/test_base.py, .../agent_runtimes/test_bbx_adapter.py, .../agent_runtimes/test_miniflow_adapter.py, .../agent_runtimes/test_pi_adapter.py (+2 more): they import the changed files
  • .../api/types.ts changed together with .../api/hooks.ts in 12 past commits and isn't in this PR
  • .../api/endpoints.ts changed together with .../api/hooks.ts in 11 past commits and isn't in this PR
🔎 More signals (4)

🗺️ Change map

flowchart LR
  subgraph PR ["Changed in this PR (7 with dependents)"]
    f_backend_agent_runtimes_base_py["backend/agent_runtimes/base.py 🔥"]:::changed
    f_backend_workflow_trigger_scope_py["backend/workflow/trigger_scope.py 🔥"]:::changed
    f_frontend_lib_api_hooks_ts[".../api/hooks.ts 🔥"]:::changed
    f_frontend_scripts_check_control_plane_regressions_mjs["frontend/scripts/check-control-plane-regressions.mjs 🔥"]:::changed
    f_backend_agent_runtimes_pi_adapter_py["backend/agent_runtimes/pi_adapter.py"]:::changed
    f_backend_agent_runtimes_registry_py["backend/agent_runtimes/registry.py"]:::changed
    f_frontend_lib_navigation_ts["frontend/lib/navigation.ts"]:::changed
  end
  f_backend_agent_runtimes_bbx_adapter_py["backend/agent_runtimes/bbx_adapter.py"]
  f_backend_agent_runtimes_base_py --> f_backend_agent_runtimes_bbx_adapter_py
  f_backend_agent_runtimes_miniflow_adapter_py["backend/agent_runtimes/miniflow_adapter.py"]
  f_backend_agent_runtimes_base_py --> f_backend_agent_runtimes_miniflow_adapter_py
  f_backend_agent_runtimes_opentabs_adapter_py["backend/agent_runtimes/opentabs_adapter.py"]
  f_backend_agent_runtimes_base_py --> f_backend_agent_runtimes_opentabs_adapter_py
  f_backend_agent_server_py["backend/agent_server.py"]
  f_backend_agent_runtimes_base_py --> f_backend_agent_server_py
  f_backend_api_v1_studio_lifecycle_py[".../v1/studio_lifecycle.py"]
  f_backend_workflow_trigger_scope_py --> f_backend_api_v1_studio_lifecycle_py
  f_backend_workflow_opencli_hda_tracer_py["backend/workflow/opencli_hda_tracer.py"]
  f_backend_workflow_trigger_scope_py --> f_backend_workflow_opencli_hda_tracer_py
  f_backend_api_v1_workflows_py[".../v1/workflows.py"]
  f_frontend_lib_api_hooks_ts --> f_backend_api_v1_workflows_py
  f_backend_models___init___py["backend/models/__init__.py"]
  f_frontend_lib_api_hooks_ts --> f_backend_models___init___py
  f_backend_schemas_workflow_py["backend/schemas/workflow.py"]
  f_frontend_lib_api_hooks_ts --> f_backend_schemas_workflow_py
  f_backend_agent_runtimes_registry_py --> f_backend_agent_runtimes_bbx_adapter_py
  f_backend_agent_runtimes_registry_py --> f_backend_agent_runtimes_miniflow_adapter_py
  f_backend_agent_runtimes_registry_py --> f_backend_agent_runtimes_opentabs_adapter_py
  f_backend_agent_runtimes_registry_py --> f_backend_agent_server_py
  f_frontend_lib_navigation_ts --> f_backend_api_v1_workflows_py
  f_frontend_lib_navigation_ts --> f_backend_models___init___py
  f_frontend_lib_navigation_ts --> f_backend_schemas_workflow_py
  more(["+76 more dependents"])
  PR --> more
  w_frontend_lib_api_types_ts(["⚠️ .../api/types.ts changed together 12×, not in PR"]):::warn
  f_frontend_lib_api_hooks_ts -.- w_frontend_lib_api_types_ts
  w_frontend_lib_api_endpoints_ts(["⚠️ .../api/endpoints.ts changed together 11×, not in PR"]):::warn
  f_frontend_lib_api_hooks_ts -.- w_frontend_lib_api_endpoints_ts
  t_tests_unit_agent_runtimes_test_base_py(["✅ .../agent_runtimes/test_base.py"]):::guard
  t_tests_unit_agent_runtimes_test_base_py -.-> f_backend_agent_runtimes_base_py
  t_tests_integration_test_trigger_scoped_workflow_execution_py(["✅ tests/integration/test_trigger_scoped_workflow_execution.py"]):::guard
  t_tests_integration_test_trigger_scoped_workflow_execution_py -.-> f_backend_workflow_trigger_scope_py
  t_tests_unit_agent_runtimes_test_pi_adapter_py(["✅ .../agent_runtimes/test_pi_adapter.py"]):::guard
  t_tests_unit_agent_runtimes_test_pi_adapter_py -.-> f_backend_agent_runtimes_pi_adapter_py
  classDef changed fill:#dbeafe,stroke:#1d4ed8,color:#1e3a5f
  classDef warn fill:#fef3c7,stroke:#b45309,color:#78350f
  classDef guard fill:#dcfce7,stroke:#15803d,color:#14532d
Loading

Solid arrows: code that imports the changed files (85 direct dependents, from the last indexed snapshot). Dashed: history/tests.

🔥 Hotspots touched (5)

  • .../operations-agents/page.tsx: 2 commits/90d, 0 dependents · primary owner: 2233admin (100%)
  • frontend/scripts/check-control-plane-regressions.mjs: 6 commits/90d, 2 dependents · primary owner: 2233admin (100%)
  • backend/agent_runtimes/base.py: 4 commits/90d, 14 dependents · primary owner: Curry (95%)
2 more
  • backend/workflow/trigger_scope.py: 1 commits/90d, 3 dependents · primary owner: 2233admin (100%)
  • .../api/hooks.ts: 15 commits/90d, 58 dependents · primary owner: 2233admin (62%)

🔗 Hidden coupling (1 file)

  • .../api/hooks.ts co-changes with these files (not in this PR):
    • .../api/types.ts (12×, 🟡 notable)
    • .../api/endpoints.ts (11×, 🟡 notable)

💀 Dead code (10 findings)

  • 💀 .../api/hooks.ts useGovernedWorkspaces (confidence 1.00)
  • 💀 .../api/hooks.ts useGovernedWorkspaceProjects (confidence 1.00)
  • 💀 .../api/hooks.ts useWorkspaceSources (confidence 1.00)
7 more
  • 💀 .../api/hooks.ts useCreateProjectSourceBinding (confidence 1.00)
  • 💀 .../api/hooks.ts useOperationsInbox (confidence 1.00)
  • 💀 .../api/hooks.ts useDecideOperationsApproval (confidence 1.00)
  • 💀 .../api/hooks.ts usePatchOperationsAgent (confidence 1.00)
  • 💀 .../api/hooks.ts useAssignOperationsAgentProfile (confidence 1.00)
  • 💀 .../api/hooks.ts usePresets (confidence 1.00)
  • 💀 .../api/hooks.ts useBrowserActPacks (confidence 1.00)

👀 Suggested reviewers @2233admin


📊 See the full report for this PR
Your repo map with this PR's blast radius lit up, every caller of the contracts it changes, and health before and after. No sign-in. · ⭐ Star Repowise · 📥 Install bot · Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot · Updated 2026-08-08 10:44 UTC

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added Control Center with kill-switch controls, ODP health monitoring, advisory reports, audit history, polling, and confirmation safeguards.
    • Added OpenClaw and Hermes as selectable automation executors.
    • Added runtime support for OpenClaw and Hermes, including error handling, timeouts, and availability checks.
  • Bug Fixes

    • Improved shared configuration validation and corrected control-route navigation and formatting issues.
  • Documentation

    • Added overnight run reports, baseline details, findings, and blocker documentation.

Walkthrough

Changes

Runtime adapters

Layer / File(s) Summary
Shared runtime validation and registration
backend/agent_runtimes/base.py, backend/agent_runtimes/pi_adapter.py, backend/agent_runtimes/registry.py
Common subprocess configuration validation is centralized. Pi uses the shared validator. Hermes and OpenClaw load through the runtime registry.
Hermes subprocess adapter
backend/agent_runtimes/hermes_adapter.py
Adds one-shot Hermes execution with prompt composition, usage-file support, timeout handling, process errors, and lifecycle events.
OpenClaw subprocess adapter
backend/agent_runtimes/openclaw_adapter.py
Adds OpenClaw execution with JSON and text parsing, nested reply extraction, configuration handling, timeout handling, and lifecycle events.
Runtime adapter validation
tests/unit/agent_runtimes/test_hermes_adapter.py, tests/unit/agent_runtimes/test_openclaw_adapter.py
Adds fake-binary tests for successful execution, failures, timeouts, parsing, validation, availability, and capabilities.

Control Center

Layer / File(s) Summary
Control Center interface and API wiring
frontend/lib/api/hooks.ts, frontend/app/(app)/control/page.tsx
Adds polling hooks and a Control Center page for kill-switch state, ODP monitoring, advisory reports, and paginated audit records.
Control Center exposure and regression coverage
docs/backend-capability-exposure-matrix.yaml, frontend/lib/navigation.ts, frontend/app/(app)/operations-agents/page.tsx, frontend/scripts/check-control-plane-regressions.mjs
Maps control operations to /control, exposes the route, adds Hermes and OpenClaw executors, and updates regression checks.

Overnight records

Layer / File(s) Summary
Overnight validation records
.night/*.md, backend/workflow/trigger_scope.py
Records baseline metrics, blockers, findings, and final results. Adjusts indentation in trigger_scope.py.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant ControlCenterPage
  participant ReactQueryHooks
  participant ControlAPI
  Operator->>ControlCenterPage: open /control
  ControlCenterPage->>ReactQueryHooks: request operational data
  ReactQueryHooks->>ControlAPI: fetch kill-switch, advisory, and ODP state
  ControlAPI-->>ReactQueryHooks: return state and reports
  ReactQueryHooks-->>ControlCenterPage: render panels and audit data
  Operator->>ControlCenterPage: confirm kill-switch action
  ControlCenterPage->>ReactQueryHooks: submit state change
  ReactQueryHooks->>ControlAPI: update kill-switch
  ControlAPI-->>ReactQueryHooks: return updated state
Loading

Possibly related PRs

Suggested reviewers: 2233admin

Poem

A rabbit hops through runtime streams,
With Hermes prompts and OpenClaw dreams.
The control page watches, polls, and glows,
While kill-switch safety clearly shows.
Tests guard each subprocess trail,
And night reports record the tale.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: adding OpenClaw and Hermes runtime adapters for operations agents.
Description check ✅ Passed The description directly explains the new runtime adapters, registration, frontend exposure, tests, verification, and known blocker.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai
coderabbitai Bot requested a review from 2233admin August 8, 2026 10:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.night/FINDINGS.md:
- Around line 27-28: Synchronize the overnight status records with the final
decisions: in .night/FINDINGS.md lines 27-28, reconcile the F3-2
confidence/rationale with its implemented status by recording the exception or
updating the status; in .night/REPORT.md line 12, reconcile the Phase 3 pending
status with the implemented findings described on lines 43-48 by marking it
complete or stating the remaining sign-off condition.

In @.night/REPORT.md:
- Around line 18-19: Insert a blank line between the “## 基线对照表” heading and the
following Markdown table, preserving the heading and table content unchanged.

In `@backend/agent_runtimes/base.py`:
- Around line 182-183: Extend validate_common_config’s env validation so that,
after confirming config["env"] is a dict, every key and value must be a string;
append the existing validation error through the normal ConfigError path for any
invalid entry, while preserving acceptance of valid string-to-string mappings.

In `@backend/agent_runtimes/hermes_adapter.py`:
- Around line 184-210: Update the subprocess collection in hermes_adapter.py at
lines 184-210 and openclaw_adapter.py at lines 236-263: within each adapter’s
asyncio.timeout block, replace the sequential stdout read and proc.wait flow
with concurrent stdout/stderr collection via proc.communicate(), preserving the
captured output and return-code handling used by each adapter. Ensure both pipes
are drained before completion and keep the existing timeout, termination, and
error behavior unchanged.

In `@backend/agent_runtimes/openclaw_adapter.py`:
- Around line 171-188: The _parse_stdout function only attempts JSON parsing per
line, so formatted multi-line JSON is missed. After the reversed candidate loop
fails to produce a payload, parse the full stdout with json.loads, pass the
result to _extract_reply_text, and preserve the existing recognized-text and
no-text error behavior while allowing JSONDecodeError to fall back to plain-text
handling.

In `@frontend/app/`(app)/control/page.tsx:
- Around line 420-430: Update handleKillToggle and confirmEngage to surface
setKill.error for both engage and disengage mutations, using the existing UI
error-message pattern. Keep the confirmation dialog open while the engage
mutation is pending or fails, and close it only after the mutation succeeds;
preserve the current mutation values and toggle behavior on success.

In `@frontend/app/`(app)/operations-agents/page.tsx:
- Around line 28-29: Update the unknown-executor fallback in the executor lookup
logic to select the entry whose id is custom rather than relying on
EXECUTORS[3]. Preserve the existing fallback behavior and display the custom
executor for unknown persisted values.

In `@frontend/lib/api/hooks.ts`:
- Around line 751-756: Protect the set_kill_switch API route or its v1_router
boundary with the existing operator/management authorization guard, while
preserving the useSetKillSwitch mutation behavior. Ensure direct POST requests
require the same authorization as the confirmation UI.

In `@frontend/scripts/check-control-plane-regressions.mjs`:
- Around line 165-166: Add an assertion in the regression test alongside the
existing polling interval checks to verify the page contains the advisory
report’s 60-second interval, refetchInterval: 60_000.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a6a26d8-a229-46f7-a00c-6f2f28201f63

📥 Commits

Reviewing files that changed from the base of the PR and between 94ab53d and 6d9bffc.

📒 Files selected for processing (18)
  • .night/BASELINE.md
  • .night/BLOCKERS.md
  • .night/FINDINGS.md
  • .night/REPORT.md
  • backend/agent_runtimes/base.py
  • backend/agent_runtimes/hermes_adapter.py
  • backend/agent_runtimes/openclaw_adapter.py
  • backend/agent_runtimes/pi_adapter.py
  • backend/agent_runtimes/registry.py
  • backend/workflow/trigger_scope.py
  • docs/backend-capability-exposure-matrix.yaml
  • frontend/app/(app)/control/page.tsx
  • frontend/app/(app)/operations-agents/page.tsx
  • frontend/lib/api/hooks.ts
  • frontend/lib/navigation.ts
  • frontend/scripts/check-control-plane-regressions.mjs
  • tests/unit/agent_runtimes/test_hermes_adapter.py
  • tests/unit/agent_runtimes/test_openclaw_adapter.py

Comment thread .night/FINDINGS.md
Comment on lines +27 to +28
Confidence: 低(不符合"上溯到 root type"门槛,不落地)
Status: implemented (3ee50ad, +1 newline, ruff clean)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the overnight status records with the final decisions.

The records contain two status mismatches:

  • .night/FINDINGS.md#L27-L28: Line 27 says F3-2 should not land, but Line 28 says it was implemented. Record the later exception or update the status and rationale.
  • .night/REPORT.md#L12-L12: Line 12 marks Phase 3 pending, but Lines 43-48 say its findings are implemented. Mark the phase complete or state the remaining sign-off condition.
📍 Affects 2 files
  • .night/FINDINGS.md#L27-L28 (this comment)
  • .night/REPORT.md#L12-L12
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.night/FINDINGS.md around lines 27 - 28, Synchronize the overnight status
records with the final decisions: in .night/FINDINGS.md lines 27-28, reconcile
the F3-2 confidence/rationale with its implemented status by recording the
exception or updating the status; in .night/REPORT.md line 12, reconcile the
Phase 3 pending status with the implemented findings described on lines 43-48 by
marking it complete or stating the remaining sign-off condition.

Comment thread .night/REPORT.md
Comment on lines +18 to +19
## 基线对照表
| 指标 | 基线 | 结束 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the blank line required before the table.

markdownlint-cli2 reports MD058 at Line 19 because ## 基线对照表 is followed immediately by the table. Insert one blank line before | 指标....

Proposed fix
 ## 基线对照表
+
 | 指标 | 基线 | 结束 |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 基线对照表
| 指标 | 基线 | 结束 |
## 基线对照表
| 指标 | 基线 | 结束 |
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 19-19: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.night/REPORT.md around lines 18 - 19, Insert a blank line between the “##
基线对照表” heading and the following Markdown table, preserving the heading and
table content unchanged.

Source: Linters/SAST tools

Comment on lines +182 to +183
if "env" in config and config["env"] is not None and not isinstance(config["env"], dict):
errors.append("'env' must be a dict when provided")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '170,192p' backend/agent_runtimes/base.py
rg -n -C 3 'validate_config\(\{"env"' tests/unit/agent_runtimes

Repository: 2233admin/opencli-Razormind

Length of output: 1617


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Candidate files:\n'
git ls-files | rg 'backend/agent_runtimes|tests/unit/agent_runtimes' | sed -n '1,200p'

printf '\nAll validate_common_config usages:\n'
rg -n -C 4 'validate_common_config|_compose_env|create_subprocess_exec|ConfigError' backend tests/unit/agent_runtimes || true

printf '\nStatic Python env-validation behavior probe:\n'
python3 - <<'PY'
config = {"env": {"TOKEN": 1}}
errors = []
if "env" in config and config["env"] is not None and not isinstance(config["env"], dict):
    errors.append("'env' must be a dict when provided")
elif "env" in config and config["env"] is not None:
    env = config["env"]
    if not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
        errors.append("'env' must be a dict of strings when provided")
print("errors_without_new_check:", errors)
print("non_string_value_passes_current_check:", len(errors) == 0)
PY

Repository: 2233admin/opencli-Razormind

Length of output: 42554


Validate env keys and values as strings.

validate_common_config currently only rejects a non-dict env, so config["env"] = {"TOKEN": 1} passes validation. The adapter then passes that value to asyncio.create_subprocess_exec, which raises TypeError at invoke time instead of returning a ConfigError. Reject mappings with non-string keys or values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/agent_runtimes/base.py` around lines 182 - 183, Extend
validate_common_config’s env validation so that, after confirming config["env"]
is a dict, every key and value must be a string; append the existing validation
error through the normal ConfigError path for any invalid entry, while
preserving acceptance of valid string-to-string mappings.

Comment on lines +184 to +210
try:
async with asyncio.timeout(timeout_seconds):
stdout_bytes = await proc.stdout.read() if proc.stdout is not None else b""
returncode = await proc.wait()
except (TimeoutError, asyncio.CancelledError) as exc:
proc.terminate()
try:
await asyncio.wait_for(proc.wait(), timeout=_KILL_GRACE_SECONDS)
except TimeoutError:
proc.kill()
await proc.wait()
if isinstance(exc, asyncio.CancelledError):
raise
yield event_error(
task.task_id,
f"hermes run timed out after {timeout_seconds}s",
error_type="TimeoutError",
)
return

text = stdout_bytes.decode(errors="replace").strip()

if returncode != 0:
stderr_tail = b""
if proc.stderr is not None:
stderr_tail = await proc.stderr.read()
tail = stderr_tail[-_STDERR_TAIL_BYTES:].decode(errors="replace")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '184,216p' backend/agent_runtimes/hermes_adapter.py
sed -n '236,270p' backend/agent_runtimes/openclaw_adapter.py
rg -n -C 3 'stderr|timeout|communicate' tests/unit/agent_runtimes/test_hermes_adapter.py tests/unit/agent_runtimes/test_openclaw_adapter.py

Repository: 2233admin/opencli-Razormind

Length of output: 8398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect subprocess setup and helper/protocol signatures around both adapters.
for f in backend/agent_runtimes/hermes_adapter.py backend/agent_runtimes/openclaw_adapter.py; do
  echo "=== $f outline ==="
  ast-grep outline "$f" || true
  echo "=== $f relevant subprocess helpers ==="
  rg -n -C 5 'subprocess|stdin|stdout|stderr|proc|run|communicate|timeout_seconds' "$f"
done

Repository: 2233admin/opencli-Razormind

Length of output: 15941


Drain both subprocess pipes before waiting on the process.

Both adapters create subprocess stdout and stderr pipes and then read stdout before the later stderr read. If the child fills stderr, proc.wait() can block after stdout.read() completes, so both adapters can exhaust asyncio.timeout() without reading stderr. Use concurrent collection for stdout and stderr, such as await proc.communicate(), inside the timeout block.

📍 Affects 2 files
  • backend/agent_runtimes/hermes_adapter.py#L184-L210 (this comment)
  • backend/agent_runtimes/openclaw_adapter.py#L236-L263
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/agent_runtimes/hermes_adapter.py` around lines 184 - 210, Update the
subprocess collection in hermes_adapter.py at lines 184-210 and
openclaw_adapter.py at lines 236-263: within each adapter’s asyncio.timeout
block, replace the sequential stdout read and proc.wait flow with concurrent
stdout/stderr collection via proc.communicate(), preserving the captured output
and return-code handling used by each adapter. Ensure both pipes are drained
before completion and keep the existing timeout, termination, and error behavior
unchanged.

Comment on lines +171 to +188
# OpenClaw prints notices before the payload; try the last JSON-looking
# line first, then a full-document parse.
candidates: list[str] = []
for line in stdout.splitlines():
stripped = line.strip()
if stripped.startswith("{") or stripped.startswith("["):
candidates.append(stripped)
if candidates:
for candidate in reversed(candidates):
try:
payload = json.loads(candidate)
except json.JSONDecodeError:
continue
text = _extract_reply_text(payload)
if text:
return text, None
return None, "OpenClaw JSON reply contained no recognized text field"
return None, None # non-JSON stdout handled as plain text by caller

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '169,188p' backend/agent_runtimes/openclaw_adapter.py
rg -n -C 3 'NESTED|NON_JSON|json.loads|formatted' tests/unit/agent_runtimes/test_openclaw_adapter.py

Repository: 2233admin/opencli-Razormind

Length of output: 2093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from backend.agent_runtimes.openclaw_adapter import _extract_reply_text
import json

example = "OpenClaw notice\n" + json.dumps({"response": {"content": "nested reply"}})
lines = [line.strip() for line in example.splitlines()]
print("example:")
print(example)
print("candidate lines:")
for i, line in enumerate(lines, 1):
    print(f"{i}: {line!r}")
print("would parse last JSON-like candidate:")
try:
    candidate = [l for l in lines if l.startswith("{") or l.startswith("[")][-1]
    payload = json.loads(candidate)
    print("candidate payload:", payload)
    print("text:", _extract_reply_text(payload))
except Exception as exc:
    print("candidate parse error:", type(exc).__name__, str(exc))
print("current code returns:", None, None)
print("full-document parse text:", _extract_reply_text(json.loads(example)))
PY

Repository: 2233admin/opencli-Razormind

Length of output: 680


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- openclaw_adapter relevant sections ---'
sed -n '1,70p;140,200p;220,270p' backend/agent_runtimes/openclaw_adapter.py

echo '--- test relevant sections ---'
sed -n '1,80p;90,125p' tests/unit/agent_runtimes/test_openclaw_adapter.py

echo '--- Python JSON probe (inline payload parser) ---'
python3 - <<'PY'
import json

def _extract_reply_text(payload):
    # Minimal copy of the real helper body read from source.
    if isinstance(payload, dict):
        for field in ("message", "text", "content"):
            value = payload.get(field)
            if isinstance(value, str):
                return value
        if not payload:
            return ""
    # Recursive search for first string value.
    if isinstance(payload, dict):
        for value in payload.values():
            if isinstance(value, dict):
                v = _extract_reply_text(value)
                if v is not None:
                    return v
            if isinstance(value, str):
                return value
            if isinstance(value, list):
                text = next((_extract_reply_text(item) for item in value if isinstance(item, dict)), None)
                if text is not None:
                    return text or first_str(value)
                text = next((item for item in value if isinstance(item, str)), "")
                if text:
                    return text
    if isinstance(payload, list):
        return first_str(payload)
    return None

def first_str(value):
    return next((item for item in value if isinstance(item, str)), None) if isinstance(value, list) else None

example = "OpenClaw notice\n" + json.dumps({"response": {"content": "nested reply"}})
candidates = [line.strip() for line in example.splitlines() if line.strip().startswith("{") or line.strip().startswith("[")]
print("input:", repr(example))
print("candidates:", candidates)
try:
    payload = json.loads(candidates[-1])
    print("single line parse payload:", payload)
    print("single line text:", _extract_reply_text(payload))
except Exception as exc:
    print("single line parse error:", type(exc).__name__, str(exc))
print("full document text:", _extract_reply_text(json.loads(example)))
PY

Repository: 2233admin/opencli-Razormind

Length of output: 11455


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json

_EXTRACT_ignored = None

def _extract_reply_text(payload):
    """Minimal extraction matching backend/agent_runtimes/openclaw_adapter.py comment/logic."""
    if isinstance(payload, str):
        return payload
    if isinstance(payload, dict):
        for field in ("text", "reply", "content", "message", "result", "response"):
            value = payload.get(field)
            if isinstance(value, str):
                return value
            if isinstance(value, dict) or isinstance(value, str):
                v = _extract_reply_text(value)
                if v is not None:
                    return v
        # Fallback probe for recursive object, list, or scalar.
        def first_str(seq):
            return next((item for item in seq if isinstance(item, str)), None)
        if isinstance(payload, dict):
            for value in payload.values():
                v = _extract_reply_text(value)
                if v is not None:
                    return v
        if isinstance(payload, list):
            text = first_str(payload)
            if text:
                return text
    return None

examples = [
    ("unindented single line", '{"text": "single line json"}'),
    ("notice + indented JSON", "OpenClaw notice\n" + json.dumps({"text": "indented json"}, indent=2)),
    ("plain text", "plain diagnostic output"),
]

for name, stdout in examples:
    candidates = [line.strip() for line in stdout.splitlines() if line.strip().startswith(("{", "["))]
    print(name, "=>" , stdout.splitlines()[-1][:50])
    print(" candidates:", candidates)
    for candidate in reversed(candidates):
        try:
            payload = json.loads(candidate)
        except json.JSONDecodeError as exc:
            print(" line parse failed:", type(exc).__name__, str(exc)[:80])
            continue
        print(" line parse payload:", payload)
        text = _extract_reply_text(payload)
        if text:
            print(" line parse text:", text)
            break
    else:
        try:
            payload = json.loads(stdout)
            text = _extract_reply_text(payload)
            if text:
                print(" full parse text:", text)
            else:
                print(" full parse text: None")
        except json.JSONDecodeError as exc:
            print(" full parse failed:", type(exc).__name__, str(exc)[:80])
PY

Repository: 2233admin/opencli-Razormind

Length of output: 720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json

def current_extract(payload):
    """Reproduce the current top-level dict/probe extraction for relevant OpenClaw shapes."""
    if isinstance(payload, str):
        return payload
    if isinstance(payload, dict):
        for field in ("text", "reply", "content", "message", "result", "response"):
            value = payload.get(field)
            if isinstance(value, str):
                return value
            if isinstance(value, dict):
                v = current_extract(value)
                if v is not None:
                    return v
    return None

examples = [
    ("unindented single line", '{"text": "single line json"}'),
    ("indented single object", json.dumps({"text": "indented json"}, indent=2)),
    ("notice + indented JSON", "OpenClaw notice\n" + json.dumps({"text": "indented json"}, indent=2)),
    ("plain text", "plain diagnostic output"),
]

def current_parse(stdout):
    candidates = [line.strip() for line in stdout.splitlines() if line.strip().startswith(("{", "["))]
    for candidate in reversed(candidates):
        try:
            payload = json.loads(candidate)
        except json.JSONDecodeError as exc:
            continue
        text = current_extract(payload)
        if text:
            return text, "FOUND"
        return None, "FOUND_but_no_text"
    return None, "NO_CANDIDATE"

for name, stdout in examples:
    print(name)
    print(" current:", current_parse(stdout))
PY

Repository: 2233admin/opencli-Razormind

Length of output: 384


Parse formatted JSON when line-based parsing finds no complete payload.

_parse_stdout uses stdout.splitlines() to build candidates, so json.dumps(payload, indent=2) creates multiple candidate lines like { and } that cannot be parsed, and the adapter falls back to plain-text handling. After the last per-line parse attempt fails, parse json.loads(stdout) and pass the payload to _extract_reply_text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/agent_runtimes/openclaw_adapter.py` around lines 171 - 188, The
_parse_stdout function only attempts JSON parsing per line, so formatted
multi-line JSON is missed. After the reversed candidate loop fails to produce a
payload, parse the full stdout with json.loads, pass the result to
_extract_reply_text, and preserve the existing recognized-text and no-text error
behavior while allowing JSONDecodeError to fall back to plain-text handling.

Comment on lines +420 to +430
const handleKillToggle = (engaged: boolean) => {
if (engaged) {
setConfirmOpen(true)
} else {
setKill.mutate(false)
}
}

const confirmEngage = () => {
setKill.mutate(true)
setConfirmOpen(false)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Show kill-switch mutation failures.

Both mutation paths ignore setKill.error. Line 430 also closes the confirmation dialog before the engage request succeeds. If the POST fails, the operator receives no failure feedback and can assume that automatic execution is blocked.

Keep the dialog open until success. Show an error message for both engage and disengage failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/`(app)/control/page.tsx around lines 420 - 430, Update
handleKillToggle and confirmEngage to surface setKill.error for both engage and
disengage mutations, using the existing UI error-message pattern. Keep the
confirmation dialog open while the engage mutation is pending or fails, and
close it only after the mutation succeeds; preserve the current mutation values
and toggle behavior on success.

Comment on lines +28 to +29
{ id: 'openclaw', name: 'OpenClaw', icon: Bot, color: 'text-rose-400' },
{ id: 'hermes', name: 'Hermes', icon: Sparkles, color: 'text-amber-400' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the unknown-executor fallback stable.

Line 40 uses EXECUTORS[3] as the fallback. These insertions change that entry from custom to openclaw. An unknown persisted executor will now display as OpenClaw.

Select the fallback by id === 'custom' instead of by array position.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/`(app)/operations-agents/page.tsx around lines 28 - 29, Update
the unknown-executor fallback in the executor lookup logic to select the entry
whose id is custom rather than relying on EXECUTORS[3]. Preserve the existing
fallback behavior and display the custom executor for unknown persisted values.

Comment thread frontend/lib/api/hooks.ts
Comment on lines +751 to +756
export function useSetKillSwitch() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (engaged: boolean) => api.setKillSwitch(engaged),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['kill-switch'] }),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline backend/api/v1/control.py --items all
rg -n -C 6 'kill-switch|set_kill_switch|Depends\(|include_router|require.*role|permission|authorize' backend

Repository: 2233admin/opencli-Razormind

Length of output: 50384


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the control router route and router wiring without scanning the whole backend.
sed -n '1,80p' backend/api/v1/control.py
sed -n '180,250p' backend/api/v1/control.py

# Find where backend/api/v1/control.py is included and what dependencies/middleware wrap /control.
rg -n -C 4 'backend\.api\.v1\.control|control_router|include_router\([^)]*\S*control|Depends\(|middleware|middleware_factory|require.*permission|permissions|Operation' backend/app | head -n 200

Repository: 2233admin/opencli-Razormind

Length of output: 5491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate FastAPI app/routers and control router inclusion without assuming backend/app.
rg -n -C 5 'include_router|APIRouter|create_app|FastAPI|dependency_overrides|Depends\(|middleware|security|permission|ControlRouter|control_router|backend/api/v1/control' backend | head -n 240

# Read focused relevant includes with wc to bound selection.
for f in $(rg -l 'include_router|control_router|backend/api/v1/control' backend | head -n 20); do
  echo "===== $f ($(wc -l < "$f") lines) ====="
  rg -n -C 8 'control_router|include_router|middleware|Depends|security|permission|APIRouter' "$f" || true
done

# Inspect kill_switch mutation directly.
sed -n '1,80p' backend/control/kill_switch.py
sed -n '1,60p' backend/schemas/control.py

Repository: 2233admin/opencli-Razormind

Length of output: 24725


Authorization Bypass (CWE-862): Missing Authorization

Reachability: Internal

Reachability path
● Entry
  frontend/app/(app)/control/page.tsx:38
  formatMs
│
▼
● Sink
  frontend/lib/api/hooks.ts

Gate kill-switch state changes with operator authorization.

POST /api/v1/control/kill-switch is mounted under /api/v1, receives only body.engaged, and does not declare a role/permission dependency. Add an operator/management guard at the set_kill_switch route or v1_router boundary so confirmation UI cannot be bypassed by direct API calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/lib/api/hooks.ts` around lines 751 - 756, Protect the
set_kill_switch API route or its v1_router boundary with the existing
operator/management authorization guard, while preserving the useSetKillSwitch
mutation behavior. Ensure direct POST requests require the same authorization as
the confirmation UI.

Comment on lines +165 to +166
assert.match(page, /refetchInterval: 30_000/)
assert.match(page, /refetchInterval: 15_000/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the advisory polling interval.

The test checks the 30-second kill-switch interval and the 15-second ODP interval. It does not check useAdvisoryReport({ refetchInterval: 60_000 }). Removing advisory polling would pass this regression check.

Add an assertion for refetchInterval: 60_000.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/scripts/check-control-plane-regressions.mjs` around lines 165 - 166,
Add an assertion in the regression test alongside the existing polling interval
checks to verify the page contains the advisory report’s 60-second interval,
refetchInterval: 60_000.

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