Skip to content

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

Merged
2233admin merged 1 commit into
2233admin:mainfrom
1012839419a-alt:night
Sep 2, 2026
Merged

feat(agent-runtimes): OpenClaw + Hermes runtime adapters for operations agents#74
2233admin merged 1 commit 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)

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 96bd71b5-92bf-4c35-8b14-62b50cbe9139

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb12da and 833beca.

📒 Files selected for processing (6)
  • backend/agent_runtimes/base.py
  • backend/agent_runtimes/hermes_adapter.py
  • backend/agent_runtimes/openclaw_adapter.py
  • backend/agent_runtimes/registry.py
  • tests/unit/agent_runtimes/test_hermes_adapter.py
  • tests/unit/agent_runtimes/test_openclaw_adapter.py

📝 Summary

Summary by CodeRabbit

  • New Features
    • Added support for running tasks through Hermes and OpenClaw agent runtimes.
    • Added configuration options for binaries, models, providers, arguments, environment variables, working directories, usage files, and timeouts.
    • Added runtime readiness checks and clear reporting for unavailable binaries, failures, timeouts, cancellations, and empty responses.
    • Added support for parsing structured and plain-text OpenClaw responses.
    • Added runtime capability reporting for streaming, resumability, and persistent sessions.

Walkthrough

The PR adds shared subprocess configuration validation and runtime capability constants. It registers Hermes and OpenClaw adapters that execute one-shot tasks, handle process failures and timeouts, parse output, and emit lifecycle events. Unit tests cover both adapters.

Changes

Runtime adapters

Layer / File(s) Summary
Shared runtime validation and registration
backend/agent_runtimes/base.py, backend/agent_runtimes/registry.py
The runtime base adds capability constants and accumulated validation for subprocess settings. Registry initialization loads Hermes and OpenClaw adapters.
Hermes subprocess execution
backend/agent_runtimes/hermes_adapter.py, tests/unit/agent_runtimes/test_hermes_adapter.py
Hermes supports one-shot prompts, provider and model selection, environment and usage-file options, readiness checks, lifecycle events, stderr handling, timeouts, cancellation, and typed errors. Tests cover these paths.
OpenClaw subprocess execution
backend/agent_runtimes/openclaw_adapter.py, tests/unit/agent_runtimes/test_openclaw_adapter.py
OpenClaw supports command composition, local mode, model selection, JSON and text reply extraction, readiness checks, lifecycle events, stderr handling, timeouts, cancellation, and typed errors. Tests cover these paths.

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

Sequence Diagram(s)

sequenceDiagram
  participant AgentTask
  participant HermesRuntimeAdapter
  participant HermesProcess
  AgentTask->>HermesRuntimeAdapter: invoke(task)
  HermesRuntimeAdapter->>HermesProcess: start one-shot process
  HermesRuntimeAdapter->>HermesProcess: send composed prompt
  HermesProcess-->>HermesRuntimeAdapter: return output and exit status
  HermesRuntimeAdapter-->>AgentTask: emit lifecycle, text, completion, or error events
Loading
sequenceDiagram
  participant AgentTask
  participant OpenClawRuntimeAdapter
  participant OpenClawProcess
  AgentTask->>OpenClawRuntimeAdapter: invoke(task)
  OpenClawRuntimeAdapter->>OpenClawProcess: start agent command
  OpenClawRuntimeAdapter->>OpenClawProcess: send task message
  OpenClawProcess-->>OpenClawRuntimeAdapter: return JSON or text output
  OpenClawRuntimeAdapter-->>AgentTask: emit extracted reply, completion, or error events
Loading

Poem

A rabbit reviews the runtime trail
Hermes sends prompts without fail
OpenClaw parses text and JSON
Timeouts stop the subprocess march
Tests guard each event in the garden

🚥 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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Warning

Your free Security trial is over. An organization admin can upgrade to Advanced for continuous pull request security review or dismiss this notice.


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 Outdated
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 Outdated
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 thread backend/agent_runtimes/base.py Outdated
Comment thread backend/agent_runtimes/hermes_adapter.py Outdated
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 thread frontend/app/(app)/control/page.tsx Outdated
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 Outdated
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.

@2233admin
2233admin merged commit fc01007 into 2233admin:main Sep 2, 2026
4 of 9 checks passed
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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.

2 participants