feat(agent-runtimes): OpenClaw + Hermes runtime adapters for operations agents - #74
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (6)
📝 SummarySummary by CodeRabbit
WalkthroughThe 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. ChangesRuntime adapters
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
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (18)
.night/BASELINE.md.night/BLOCKERS.md.night/FINDINGS.md.night/REPORT.mdbackend/agent_runtimes/base.pybackend/agent_runtimes/hermes_adapter.pybackend/agent_runtimes/openclaw_adapter.pybackend/agent_runtimes/pi_adapter.pybackend/agent_runtimes/registry.pybackend/workflow/trigger_scope.pydocs/backend-capability-exposure-matrix.yamlfrontend/app/(app)/control/page.tsxfrontend/app/(app)/operations-agents/page.tsxfrontend/lib/api/hooks.tsfrontend/lib/navigation.tsfrontend/scripts/check-control-plane-regressions.mjstests/unit/agent_runtimes/test_hermes_adapter.pytests/unit/agent_runtimes/test_openclaw_adapter.py
| Confidence: 低(不符合"上溯到 root type"门槛,不落地) | ||
| Status: implemented (3ee50ad, +1 newline, ruff clean) |
There was a problem hiding this comment.
📐 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.
| ## 基线对照表 | ||
| | 指标 | 基线 | 结束 | |
There was a problem hiding this comment.
📐 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.
| ## 基线对照表 | |
| | 指标 | 基线 | 结束 | | |
| ## 基线对照表 | |
| | 指标 | 基线 | 结束 | |
🧰 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
| # 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 |
There was a problem hiding this comment.
🎯 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.pyRepository: 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)))
PYRepository: 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)))
PYRepository: 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])
PYRepository: 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))
PYRepository: 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.
| const handleKillToggle = (engaged: boolean) => { | ||
| if (engaged) { | ||
| setConfirmOpen(true) | ||
| } else { | ||
| setKill.mutate(false) | ||
| } | ||
| } | ||
|
|
||
| const confirmEngage = () => { | ||
| setKill.mutate(true) | ||
| setConfirmOpen(false) |
There was a problem hiding this comment.
🎯 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.
| { id: 'openclaw', name: 'OpenClaw', icon: Bot, color: 'text-rose-400' }, | ||
| { id: 'hermes', name: 'Hermes', icon: Sparkles, color: 'text-amber-400' }, |
There was a problem hiding this comment.
🎯 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.
| export function useSetKillSwitch() { | ||
| const queryClient = useQueryClient() | ||
| return useMutation({ | ||
| mutationFn: (engaged: boolean) => api.setKillSwitch(engaged), | ||
| onSuccess: () => queryClient.invalidateQueries({ queryKey: ['kill-switch'] }), | ||
| }) |
There was a problem hiding this comment.
🔒 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' backendRepository: 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 200Repository: 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.pyRepository: 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.
| assert.match(page, /refetchInterval: 30_000/) | ||
| assert.match(page, /refetchInterval: 15_000/) |
There was a problem hiding this comment.
🎯 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.
|
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. |
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.
New adapters
backend/agent_runtimes/hermes_adapter.pyhermeshermes -z <prompt>(one-shot stdio; final text on stdout)backend/agent_runtimes/openclaw_adapter.pyopenclawopenclaw agent --agent <id> -m <msg> --json(single turn via Gateway;--localopt-in)Both implement
RuntimeAdapter(base.py ABC):runtime_type,capabilities,validate_config,health,is_available,invoke— emitting only the closed EVENT_TYPES set viaevent_*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).EXECUTORSin operations-agents gainsopenclaw+hermesentries (executor is a free string on the backend — UI affordance only).Verified
hermes -zthrough the adapter →started → text → doneevent stream (verified 2026-08-08 against Hermes v0.20.0).except Exceptionnarrowed to pi's(BrokenPipeError, ConnectionResetError).Notes / known blocker
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.yamlupdated: 4 control-plane wrappers (getKillSwitch/setKillSwitch/getOdpState/getAdvisoryReport) moved from unreferenced → referenced with/controlfrontend_route.validate_common_configextracted into base.py (removes duplicated binary/cwd/env/args/timeout_seconds guards across pi/hermes/openclaw; ΔLOC −22).Commits
(plus
.night/docs commits: baseline seal, report, blockers, findings)