Refactor agent harness into plugin extension#63452
Conversation
PR SummaryMedium Risk Overview Extends gateway/native client contracts by adding Tightens automation and native app behavior by adding a CI Reviewed by Cursor Bugbot for commit cdfe60c. Bugbot is set up for automated code reviews on this repo. Configure here. |
🔒 Aisle Security AnalysisWe found 10 potential security issue(s) in this PR:
1. 🟠 Environment-controlled external process spawn enables unintended code execution (Codex app-server client)
DescriptionThe Codex app-server client spawns an external executable whose path and arguments are controlled by environment variables:
If an attacker can influence environment variables or
Vulnerable code: const bin = process.env.OPENCLAW_CODEX_APP_SERVER_BIN?.trim() || "codex";
const extraArgs = splitShellWords(process.env.OPENCLAW_CODEX_APP_SERVER_ARGS ?? "");
const args = extraArgs.length > 0 ? extraArgs : ["app-server", "--listen", "stdio://"];
const child = spawn(bin, args, {
env: process.env,
stdio: ["pipe", "pipe", "pipe"],
});RecommendationTreat the spawned binary as a trusted dependency and prevent untrusted env/PATH from selecting it. Recommended mitigations (pick what fits your threat model):
import path from "node:path";
const configured = process.env.OPENCLAW_CODEX_APP_SERVER_BIN?.trim();
const bin = configured ? path.resolve(configured) : "/usr/local/bin/codex";
if (!bin.startsWith("/usr/local/bin/")) {
throw new Error("Invalid OPENCLAW_CODEX_APP_SERVER_BIN");
}
const env = {
PATH: process.env.PATH, // if needed
HOME: process.env.HOME,
// explicitly required OpenAI/Codex vars only
};
spawn(bin, args, { env, stdio: ["pipe","pipe","pipe"] });
2. 🟠 Approval request not scoped to thread/turn when request params are missing
DescriptionThe Codex app-server approval bridge only checks that an incoming approval request matches the current thread/turn when identifiers are present. If This creates an authorization/scoping gap:
Vulnerable code: function matchesCurrentTurn(requestParams: JsonObject | undefined, threadId: string, turnId: string): boolean {
if (!requestParams) {
return true;
}
...
}If the app-server (or a compromised/malicious binary launched via RecommendationRequire approval requests to be explicitly scoped to the current thread/turn, and reject requests that omit identifiers. Example fix: function matchesCurrentTurn(
requestParams: JsonObject | undefined,
threadId: string,
turnId: string,
): boolean {
if (!requestParams) return false;
const requestThreadId =
readString(requestParams, "threadId") ?? readString(requestParams, "conversationId");
const requestTurnId = readString(requestParams, "turnId");
// Require both identifiers to be present.
if (!requestThreadId || !requestTurnId) return false;
return requestThreadId === threadId && requestTurnId === turnId;
}Additionally, consider logging/telemetry when rejecting unscoped approval requests so unexpected app-server behavior is visible. 3. 🟠 Codex app-server runs with approvals disabled by default and sandbox mode decoupled from OpenClaw sandbox context
DescriptionIn Impacts:
Vulnerable code (defaults): function resolveAppServerApprovalPolicy(): "never" | "on-request" | "on-failure" | "untrusted" {
const raw = process.env.OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY?.trim();
if (raw === "on-request" || raw === "on-failure" || raw === "untrusted") {
return raw;
}
return "never";
}
function resolveAppServerSandbox(): "read-only" | "workspace-write" | "danger-full-access" {
const raw = process.env.OPENCLAW_CODEX_APP_SERVER_SANDBOX?.trim();
if (raw === "read-only" || raw === "danger-full-access") {
return raw;
}
return "workspace-write";
}And the values are used when starting the app-server thread/turn:
RecommendationTie Codex app-server policy to the already-resolved OpenClaw sandbox/approval configuration and make secure behavior the default.
Example: function resolveAppServerApprovalPolicy(params: EmbeddedRunAttemptParams) {
// Prefer explicit OpenClaw config, and default to requiring approvals
return (process.env.OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY as any) ?? "on-request";
}
function resolveAppServerSandboxFromContext(
sandbox: Awaited<ReturnType<typeof resolveSandboxContext>>,
): "read-only" | "workspace-write" {
if (!sandbox?.enabled) return "read-only";
return sandbox.workspaceAccess === "rw" ? "workspace-write" : "read-only";
}
// When starting:
// approvalPolicy: resolveAppServerApprovalPolicy(params),
// sandbox: resolveAppServerSandboxFromContext(sandbox),Additionally, consider rejecting startup if env requests 4. 🟠 Untrusted plugins can register high-privilege AgentHarness implementations (agent runtime hijack)
DescriptionThe plugin API now exposes This is a high-privilege surface:
As a result, if third-party/untrusted plugins are allowed, a malicious plugin can register a harness that intercepts all agent runs and exfiltrates data or executes unintended actions. Vulnerable code: // src/plugins/registry.ts
registerAgentHarness: (harness) => registerAgentHarness(record, harness),and the registration implementation performs only ID uniqueness checks, then registers globally. RecommendationGate agent harness registration to trusted/bundled plugins (or an explicit admin allowlist) and enforce it in code. Options:
Example (conceptual) enforcement in const registerAgentHarness = (record: PluginRecord, harness: AgentHarness) => {
if (!record.manifest?.capabilities?.agentHarness) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: "agent harness registration is not permitted for this plugin",
});
return;
}
// proceed with registration...
};Also consider:
5. 🟡 Auto-mode harness fallback can replay side effects by rerunning attempt on PI after plugin harness error
Description
Vulnerable code: try {
return await harness.runAttempt(params);
} catch (error) {
if (runtime !== "auto") {
throw error;
}
log.warn(`${harness.label} failed; falling back to embedded PI backend`, { error });
return createPiAgentHarness().runAttempt(params);
}RecommendationGate fallback on an explicit replay safety signal, and default to no fallback when uncertain. Options:
Example (conceptual) gating: try {
return await harness.runAttempt(params);
} catch (err) {
if (runtime !== "auto") throw err;
const replaySafe = isReplaySafeError(err); // must be false/unknown by default
if (!replaySafe) {
// avoid duplicate side effects
throw err;
}
return createPiAgentHarness().runAttempt(params);
}6. 🟡 Potential sensitive data exposure via logging of spawned process stderr
DescriptionThe Codex app-server client logs the spawned process's stderr verbatim at debug level:
Vulnerable code: child.stderr.on("data", (chunk) => {
const text = chunk.toString("utf8").trim();
if (text) {
embeddedAgentLog.debug(`codex app-server stderr: ${text}`);
}
});Whether this is exploitable depends on your logging configuration and who can access logs, but it is a common source of credential leakage in CI/hosted environments. RecommendationAvoid logging untrusted stderr verbatim, or ensure it is redacted and gated. Options:
Example redaction: function redact(s: string): string {
return s
.replace(/(Bearer\s+)[A-Za-z0-9._-]+/g, "$1<redacted>")
.replace(/(OPENAI_API_KEY=)[^\s]+/g, "$1<redacted>");
}
child.stderr.on("data", (chunk) => {
const text = chunk.toString("utf8").trim();
if (text && process.env.OPENCLAW_DEBUG_CODEX_STDERR === "1") {
embeddedAgentLog.debug(`codex app-server stderr: ${redact(text)}`);
}
});7. 🟡 Potential symlink/hardlink overwrite via non-atomic write of Codex app-server binding file
Description
Even though
If an attacker (or untrusted local process) can create/replace Vulnerable code: await fs.writeFile(
resolveCodexAppServerBindingPath(sessionFile),
`${JSON.stringify(payload, null, 2)}\n`,
);RecommendationHarden binding file writes/deletes against symlink attacks and corruption:
Example (atomic replace with basic symlink check): import path from "node:path";
import fs from "node:fs/promises";
const bindingPath = resolveCodexAppServerBindingPath(sessionFile);
const dir = path.dirname(bindingPath);
const tmpPath = path.join(dir, `.tmp-${path.basename(bindingPath)}-${process.pid}`);
// Refuse to overwrite if an existing path is a symlink
try {
const st = await fs.lstat(bindingPath);
if (st.isSymbolicLink()) throw new Error("binding path is a symlink");
} catch (e: any) {
if (e?.code !== "ENOENT") throw e;
}
await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2) + "\n", { mode: 0o600 });
await fs.rename(tmpPath, bindingPath);For deletion, similarly 8. 🟡 Synthetic auth for Codex provider uses hard-coded token enabling unintended authenticated state and outbound requests
DescriptionThe new Codex provider plugin declares Impact:
Vulnerable code: resolveSyntheticAuth: () => ({
apiKey: "codex-app-server",
source: "codex-app-server",
mode: "token",
}),Related data-flow:
RecommendationDo not return a usable-looking token unless it is derived from verified local Codex app-server/session state. Options:
Example (gated): resolveSyntheticAuth: async (ctx) => {
const session = await codexAppServerGetSession();
if (!session?.token) return undefined;
return { apiKey: session.token, source: "codex-app-server", mode: "token" };
}Also consider setting provider 9. 🟡 Unbounded transcript read/split under session write lock can cause memory/CPU DoS
Description
This can lead to denial of service if a transcript grows large (intentionally or naturally):
Vulnerable code: raw = await fs.readFile(sessionFile, "utf8");
for (const line of raw.split(/\r?\n/)) {
... JSON.parse(line) ...
}RecommendationAvoid reading/parsing the full transcript under the write lock. Mitigations:
Example using streaming with a size cap (conceptual): import { createReadStream } from "node:fs";
import readline from "node:readline";
async function readKeysCapped(file: string, maxLines = 50_000): Promise<Set<string>> {
const keys = new Set<string>();
const rl = readline.createInterface({ input: createReadStream(file, { encoding: "utf8" }) });
let lines = 0;
for await (const line of rl) {
if (++lines > maxLines) break;
// parse line and collect idempotencyKey
}
return keys;
}Also consider bounding 10. 🟡 Codex dynamic tool bridge reports success even when AgentToolResult encodes failure
Description
This is problematic because OpenClaw tool execution can return an error result without throwing. For example, Impact:
Vulnerable code: const result = await tool.execute(call.callId, preparedArgs, params.signal);
...
return {
contentItems: result.content.flatMap(convertToolContent),
success: true,
};RecommendationDerive the response Suggested approach:
Example: function isFailureResult(result: AgentToolResult<unknown>): boolean {
const details = (result as { details?: unknown }).details;
if (details && typeof details === "object" && !Array.isArray(details)) {
const status = (details as Record<string, unknown>).status;
return status === "error" || status === "failed";
}
return false;
}
const result = await tool.execute(call.callId, preparedArgs, params.signal);
const failed = isFailureResult(result);
return {
contentItems: result.content.flatMap(convertToolContent),
success: !failed,
};Also consider updating telemetry ( Analyzed PR: #63452 at commit Last updated on: 2026-04-09T02:52:52Z |
Greptile SummaryThis PR extracts the Codex app-server runtime into a proper bundled plugin extension (
Confidence Score: 4/5Safe to merge after addressing the silent fallback in the forced-runtime path; all other changes are well-structured. One P1 finding: when OPENCLAW_AGENT_RUNTIME is set to a harness ID that is not registered (e.g. plugin load failure), selectAgentHarness silently returns PI with no diagnostic, causing the operator to run on an unexpected backend. All other logic — registry, event projector, tool bridge, session binding, SDK surface — is sound and the targeted tests cover the key selection scenarios. src/agents/harness/selection.ts — the forced-runtime fallback branch needs a warning log before falling back to PI.
|
| if (runtime !== "auto") { | ||
| const forced = harnesses.find((entry) => entry.id === runtime); | ||
| if (forced) { | ||
| return forced; | ||
| } | ||
| return createPiAgentHarness(); | ||
| } |
There was a problem hiding this comment.
Silent PI fallback when forced runtime ID is not registered
When OPENCLAW_AGENT_RUNTIME is set to a specific harness ID (e.g. "codex") but that harness is not yet registered — because the plugin failed to load or was disabled — selectAgentHarness silently returns the PI harness with no log or diagnostic. The operator asked for a specific backend and instead gets PI behavior with no indication anything went wrong. Compare the auto-with-fallback path at line 79 which does emit a log.warn; the forced path should do the same (or throw).
| if (runtime !== "auto") { | |
| const forced = harnesses.find((entry) => entry.id === runtime); | |
| if (forced) { | |
| return forced; | |
| } | |
| return createPiAgentHarness(); | |
| } | |
| if (runtime !== "auto") { | |
| const forced = harnesses.find((entry) => entry.id === runtime); | |
| if (forced) { | |
| return forced; | |
| } | |
| log.warn( | |
| `forced agent runtime "${runtime}" is not registered; falling back to embedded PI backend`, | |
| ); | |
| return createPiAgentHarness(); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/selection.ts
Line: 31-37
Comment:
**Silent PI fallback when forced runtime ID is not registered**
When `OPENCLAW_AGENT_RUNTIME` is set to a specific harness ID (e.g. `"codex"`) but that harness is not yet registered — because the plugin failed to load or was disabled — `selectAgentHarness` silently returns the PI harness with no log or diagnostic. The operator asked for a specific backend and instead gets PI behavior with no indication anything went wrong. Compare the `auto`-with-fallback path at line 79 which does emit a `log.warn`; the forced path should do the same (or throw).
```suggestion
if (runtime !== "auto") {
const forced = harnesses.find((entry) => entry.id === runtime);
if (forced) {
return forced;
}
log.warn(
`forced agent runtime "${runtime}" is not registered; falling back to embedded PI backend`,
);
return createPiAgentHarness();
}
```
How can I resolve this? If you propose a fix, please make it concise.| @@ -0,0 +1,81 @@ | |||
| import { log } from "../pi-embedded-runner/logger.js"; | |||
There was a problem hiding this comment.
Registry uses PI-specific logger
registry.ts is generic harness infrastructure, but it imports its logger from "../pi-embedded-runner/logger.js" — a PI implementation detail. If the logger namespace or module ever moves, the generic registry will need updating too. Consider importing from a shared logger factory (e.g. createSubsystemLogger("agents")) to decouple the registry from the PI runner.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/harness/registry.ts
Line: 1
Comment:
**Registry uses PI-specific logger**
`registry.ts` is generic harness infrastructure, but it imports its logger from `"../pi-embedded-runner/logger.js"` — a PI implementation detail. If the logger namespace or module ever moves, the generic registry will need updating too. Consider importing from a shared logger factory (e.g. `createSubsystemLogger("agents")`) to decouple the registry from the PI runner.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b36855f1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| completed = true; | ||
| resolveCompletion?.(); | ||
| } |
There was a problem hiding this comment.
Initialize completion resolver before handling turn notifications
turn/completed notifications can be processed before resolveCompletion is assigned, because the notification handler is registered earlier and calls resolveCompletion?.() opportunistically. When that race happens (for very fast turns), the resolver call is dropped and the attempt waits until timeout even though the turn already finished, producing spurious timeouts/interrupts.
Useful? React with 👍 / 👎.
| params.abortSignal?.addEventListener( | ||
| "abort", | ||
| () => { | ||
| runAbortController.abort(params.abortSignal?.reason ?? "upstream_abort"); | ||
| }, |
There was a problem hiding this comment.
Propagate already-aborted upstream signals immediately
This only subscribes to future abort events and never checks whether params.abortSignal is already aborted when the attempt starts. In that case the local controller stays active, so the Codex turn can still start and run until another timeout/abort path fires instead of stopping immediately.
Useful? React with 👍 / 👎.
| this.closed = true; | ||
| for (const pending of this.pending.values()) { | ||
| pending.reject(error); | ||
| } | ||
| this.pending.clear(); |
There was a problem hiding this comment.
Reset shared Codex client after transport shutdown
When the app-server process exits/errors, closeWithError marks the client closed but leaves the module-level shared client promise intact. Subsequent getSharedCodexAppServerClient() calls can keep returning that closed client, causing every later request to fail until process restart instead of recreating the transport.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca14b2fdd8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const response = await params.client.request<CodexThreadResumeResponse>("thread/resume", { | ||
| threadId: binding.threadId, | ||
| }); |
There was a problem hiding this comment.
Recreate thread when dynamic tool config changes
buildDynamicTools derives the tool list from per-attempt flags (disableTools, toolsAllow, model/tool support), but the resume path ignores that fresh list and immediately calls thread/resume with only the old threadId. Since dynamicTools are only provided on thread/start, resumed sessions keep the first turn’s tool configuration, so later turns can lose newly allowed tools (or keep stale schemas) after config/model changes. This can make subsequent attempts behave incorrectly until the binding is cleared.
Useful? React with 👍 / 👎.
| id, | ||
| pluginId: harness.pluginId ?? record.id, | ||
| }; | ||
| registerGlobalAgentHarness(normalizedHarness, { ownerPluginId: record.id }); |
There was a problem hiding this comment.
Respect non-activating mode for harness registration
registerAgentHarness always mutates the process-global harness registry, even when the registry is created with activateGlobalSideEffects: false. Non-activating loads (such as metadata/CLI registry reads) are expected to stay local, so this leaks harnesses into global state and can change later harness selection or trigger duplicate-registration errors on subsequent activating loads in the same process.
Useful? React with 👍 / 👎.
| replayMetadata: { | ||
| hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool, | ||
| replaySafe: !toolTelemetry.didSendViaMessagingTool, | ||
| }, |
There was a problem hiding this comment.
Replay metadata ignores mutating tools and cron side effects
High Severity
The Codex event projector's replayMetadata only considers didSendViaMessagingTool, but the PI harness equivalent (buildAttemptReplayMetadata) also checks toolMetas for mutating tool names (like bash, apply_patch) and successfulCronAdds. Native Codex command executions and file changes are recorded in toolMetas by recordToolMeta, and cron adds are tracked in toolTelemetry.successfulCronAdds, but neither is consulted when computing hadPotentialSideEffects / replaySafe. This can cause the system to consider a turn replay-safe even after it executed shell commands, applied patches, or added cron jobs.
Reviewed by Cursor Bugbot for commit 5111de9. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5111de9e83
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| replayMetadata: { | ||
| hadPotentialSideEffects: toolTelemetry.didSendViaMessagingTool, | ||
| replaySafe: !toolTelemetry.didSendViaMessagingTool, |
There was a problem hiding this comment.
Mark mutating Codex turns as having side effects
replayMetadata is currently derived only from didSendViaMessagingTool, so turns that run mutating tools (for example commandExecution, fileChange, or successful cron add) but do not send a message are misclassified as replay-safe. In those cases, the incomplete-turn/retry safeguards in src/agents/pi-embedded-runner/run/incomplete-turn.ts can surface the safer "try again" path without the side-effect warning even though actions may already have executed. This should include non-messaging side effects (at least tool-mutation and cron signals, consistent with buildAttemptReplayMetadata).
Useful? React with 👍 / 👎.
| if (!this.child.killed) { | ||
| this.child.kill?.(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Client close() leaks pending requests and stale singleton
High Severity
The close() method sets this.closed = true before terminating the child process. This prevents the exit handler from fully cleaning up via closeWithError(), leaving pending requests unresolved and the shared client singleton uncleared.
Reviewed by Cursor Bugbot for commit fdcd766. Configure here.
| embeddedAgentLog.warn("codex app-server notification handler failed", { error }); | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Fire-and-forget async notification handlers cause data races
Medium Severity
handleNotification in the client fires async notification handlers without awaiting them. The run-attempt registers an async handler that updates the CodexAppServerEventProjector state and resolves the completion promise. When multiple notifications arrive in rapid succession, they process concurrently on the projector, risking interleaved state mutations—e.g., turn/completed can resolve the completion promise before a preceding item/agentMessage/delta handler finishes, causing the final result to miss assistant text.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit fdcd766. Configure here.
| (runtime === "auto" && (provider === "codex" || provider === "openai-codex")); | ||
| if (!shouldUseCodex) { | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
Compact function duplicates harness selection runtime logic
Low Severity
maybeCompactCodexAppServerSession independently calls resolveEmbeddedAgentRuntime() and re-checks the provider, duplicating logic that selectAgentHarness in selection.ts already performed before invoking this function through the harness's compact callback. These two decision paths could diverge silently if the provider-matching logic is updated in one place but not the other.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit fdcd766. Configure here.
| if (!value || typeof value !== "object" || Array.isArray(value)) { | ||
| return undefined; | ||
| } | ||
| return value as JsonObject; |
There was a problem hiding this comment.
Exported coerceJsonObject function is never used anywhere
Low Severity
coerceJsonObject is exported from protocol.ts but is never imported or called anywhere in the codebase. It's dead code that adds unnecessary surface area to the module.
Reviewed by Cursor Bugbot for commit fdcd766. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fdcd7660c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "providers": ["codex"], | ||
| "modelSupport": { | ||
| "providerIds": ["codex"], | ||
| "modelPrefixes": ["gpt-", "o1", "o3", "o4", "arcanine"] |
There was a problem hiding this comment.
Remove ambiguous Codex model ownership prefixes
This manifest claims broad shorthand prefixes (gpt-, o1, o3, o4) that already belong to the bundled OpenAI plugin, so shorthand model refs now match multiple bundled owners. In src/plugins/providers.ts, resolvePreferredManifestPluginIds returns undefined for ambiguous bundled matches, and then resolveConfiguredPluginAutoEnableCandidates in src/config/plugin-auto-enable.shared.ts skips auto-enable unless there is exactly one owner. The regression is that configs relying on model-based plugin activation (especially constrained allowlist/disabled setups) can stop auto-enabling the right provider plugin for refs like gpt-5.4.
Useful? React with 👍 / 👎.
… (#46299) Merged via squash. Prepared head SHA: 5ce7634 Co-authored-by: manuel-claw <[email protected]> Co-authored-by: mcaxtr <[email protected]> Reviewed-by: @mcaxtr
|
Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Duplicate "talk config" prefix in log message
- Removed the duplicate 'talk config' prefix from line 925 so the log message now reads as a single coherent line without repetition.
Or push these changes by commenting:
@cursor push 68eb36a449
Preview (68eb36a449)
diff --git a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
--- a/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
+++ b/apps/macos/Sources/OpenClaw/TalkModeRuntime.swift
@@ -922,7 +922,7 @@
self.logger
.info(
"talk config provider=\(cfg.activeProvider, privacy: .public) " +
- "talk config voiceId=\(voiceLabel, privacy: .public) " +
+ "voiceId=\(voiceLabel, privacy: .public) " +
"modelId=\(modelLabel, privacy: .public) " +
"apiKey=\(hasApiKey, privacy: .public) " +
"interrupt=\(cfg.interruptOnSpeech, privacy: .public) " +This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cdfe60c. Configure here.
| .info( | ||
| "talk config voiceId=\(voiceLabel, privacy: .public) " + | ||
| "talk config provider=\(cfg.activeProvider, privacy: .public) " + | ||
| "talk config voiceId=\(voiceLabel, privacy: .public) " + |
There was a problem hiding this comment.
Duplicate "talk config" prefix in log message
Low Severity
The log message in reloadConfig() contains a duplicate "talk config" prefix. The concatenated string reads "talk config provider=... talk config voiceId=..." because when "provider=..." was prepended to the existing log line, the original "talk config " before "voiceId" was not removed. The second occurrence is a copy-paste artifact.
Reviewed by Cursor Bugbot for commit cdfe60c. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cdfe60cd42
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const id = harness.id.trim(); | ||
| if (!id) { | ||
| pushDiagnostic({ | ||
| level: "error", |
There was a problem hiding this comment.
Reserve
pi harness id for built-in runtime
registerAgentHarness currently accepts any non-empty id, including pi. In selectAgentHarness, registered harnesses are listed before createPiAgentHarness(), and forced mode returns the first harness whose id matches OPENCLAW_AGENT_RUNTIME; so a plugin that registers id: "pi" can shadow the built-in PI backend. That breaks the explicit PI runtime override/kill-switch behavior and makes backend selection depend on plugin registration order.
Useful? React with 👍 / 👎.
| turnId = turn.turn.id; | ||
| projector = new CodexAppServerEventProjector(params, thread.threadId, turnId); | ||
| for (const notification of pendingNotifications.splice(0)) { | ||
| await handleNotification(notification); | ||
| } |
There was a problem hiding this comment.
Complete attempt when
turn/start is already terminal
After turn/start returns, the flow always waits for a later turn/completed notification to resolve completion, but it never checks whether turn.turn.status is already completed/failed/interrupted in the start response. If the app-server returns a terminal turn without emitting a follow-up completion notification (fast/synchronous path), this attempt hangs until timeout despite the turn already being finished.
Useful? React with 👍 / 👎.



Summary
Testing
Note: full pnpm test intentionally not run; this branch is verified with targeted tests per the app-server refactor plan.