Skip to content

Commit db02a96

Browse files
authored
refactor(process): route bounded commands through Execa (#106495)
* refactor(process): centralize bounded command execution * refactor(process): migrate core one-shot commands * refactor(plugins): migrate one-shot commands * fix(process): await Windows tree termination * chore(plugin-sdk): refresh process runtime surface * refactor(process): migrate remaining bounded commands * refactor(process): normalize command result handling * refactor(process): split execution responsibilities * chore(plugin-sdk): refresh API baseline * chore(process): remove release-owned changelog entry * fix(process): narrow binary command input checks * fix(process): cap sandbox command output * fix(qa-lab): preserve exact node probe env * chore(ci): refresh dead export baseline * fix(process): preserve force-kill command deadlines * fix(process): avoid post-exit timeout reclassification * test(process): update scp staging wrapper mock * test(process): update remaining wrapper mocks * refactor(qa-lab): preserve Execa tar execution
1 parent 171fabe commit db02a96

98 files changed

Lines changed: 4067 additions & 5805 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
35e24e7461ffc83dba13cdc56f1b1b1efcf5a11d7067392ddb275b879208d633 plugin-sdk-api-baseline.json
2-
6eb82047169b07702451bbf59c9430be429efd3a60baa04076f70598c32dfb12 plugin-sdk-api-baseline.jsonl
1+
0bbd37802d7330a5639480fd15a4f97131b996423136598fc8bd20719d6ad009 plugin-sdk-api-baseline.json
2+
7b889420c787ac6c16fff3f9ef7ff0136f7387c1c695b7991f458b5c72f57818 plugin-sdk-api-baseline.jsonl

extensions/acpx/src/process-reaper.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22
* ACPX process ownership checks and cleanup. The reaper only terminates
33
* OpenClaw-owned wrapper trees after validating paths, packages, and lease ids.
44
*/
5-
import { execFile } from "node:child_process";
65
import { createRequire } from "node:module";
76
import path from "node:path";
8-
import { promisify } from "node:util";
7+
import { runExec } from "openclaw/plugin-sdk/process-runtime";
98
import { splitCommandParts } from "./command-line.js";
109
import { resolveAcpxPluginRoot } from "./config.js";
1110
import { OPENCLAW_ACPX_LEASE_ID_ARG, OPENCLAW_GATEWAY_INSTANCE_ID_ARG } from "./process-lease.js";
1211

13-
const execFileAsync = promisify(execFile);
1412
const requireFromHere = createRequire(import.meta.url);
1513
const GENERATED_WRAPPER_BASENAMES = new Set([
1614
"codex-acp-wrapper.mjs",
@@ -218,7 +216,8 @@ async function listPlatformProcesses(): Promise<AcpxProcessInfo[]> {
218216
if (process.platform === "win32") {
219217
return [];
220218
}
221-
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], {
219+
const { stdout } = await runExec("ps", ["-axo", "pid=,ppid=,command="], {
220+
logOutput: false,
222221
maxBuffer: 8 * 1024 * 1024,
223222
});
224223
return parseProcessList(stdout);

extensions/browser/src/browser/chrome-mcp.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
* Manages chrome-devtools-mcp processes and sessions, maps Browser actions to
55
* MCP tools, and exposes tab/snapshot/action helpers for logged-in browsers.
66
*/
7-
import { execFile } from "node:child_process";
87
import { randomUUID } from "node:crypto";
98
import fs from "node:fs/promises";
109
import os from "node:os";
1110
import path from "node:path";
1211
import { setTimeout as sleepTimeout } from "node:timers/promises";
13-
import { promisify } from "node:util";
1412
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1513
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1614
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
@@ -19,6 +17,7 @@ import {
1917
addTimerTimeoutGraceMs,
2018
resolveNonNegativeIntegerOption,
2119
} from "openclaw/plugin-sdk/number-runtime";
20+
import { runExec } from "openclaw/plugin-sdk/process-runtime";
2221
import {
2322
normalizeOptionalString,
2423
readStringValue,
@@ -217,7 +216,6 @@ const CHROME_MCP_SNAPSHOT_REF_PREFIX = "mcp-ref:";
217216
class ChromeMcpReconnectRequiredError extends Error {}
218217
class ChromeMcpProcessSnapshotError extends Error {}
219218

220-
const execFileAsync = promisify(execFile);
221219
const sessions = new Map<string, ChromeMcpSession>();
222220
const pendingSessions = new Map<string, PendingChromeMcpSession>();
223221
const retainedCleanupSessions = new Map<string, Set<ChromeMcpSession>>();
@@ -800,7 +798,7 @@ async function listChromeMcpPlatformProcesses(
800798
return await listChromeMcpLinuxProcesses();
801799
}
802800
const windows = platform === "win32";
803-
const { stdout } = await execFileAsync(
801+
const { stdout } = await runExec(
804802
windows ? "powershell.exe" : "ps",
805803
windows
806804
? [
@@ -812,9 +810,9 @@ async function listChromeMcpPlatformProcesses(
812810
: ["-axww", "-o", "pid=,ppid=,lstart=,command="],
813811
{
814812
env: windows ? undefined : { ...process.env, LC_ALL: "C", TZ: "UTC" },
813+
logOutput: false,
815814
maxBuffer: 4 * 1024 * 1024,
816-
timeout: 2_000,
817-
windowsHide: windows,
815+
timeoutMs: 2_000,
818816
},
819817
);
820818
if (windows) {
@@ -934,10 +932,10 @@ async function taskkillChromeMcpProcessTree(
934932
await deps.taskkillProcessTree(rootPid);
935933
return;
936934
}
937-
await execFileAsync("taskkill", ["/pid", String(rootPid), "/t", "/f"], {
935+
await runExec("taskkill", ["/pid", String(rootPid), "/t", "/f"], {
936+
logOutput: false,
938937
maxBuffer: 64 * 1024,
939-
timeout: 2_000,
940-
windowsHide: true,
938+
timeoutMs: 2_000,
941939
});
942940
}
943941

extensions/browser/src/browser/system-chrome-cookies.ts

Lines changed: 38 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** macOS Chrome-family cookie database decryption and Playwright mapping. */
2-
import { execFile } from "node:child_process";
32
import crypto from "node:crypto";
43
import { DatabaseSync } from "node:sqlite";
4+
import { runCommandBuffered } from "openclaw/plugin-sdk/process-runtime";
55

66
export type SystemBrowser = "chrome" | "brave" | "edge" | "chromium";
77

@@ -69,47 +69,45 @@ function isAsciiWhitespace(value: number): boolean {
6969
/** Read the browser Safe Storage secret. The OS consent prompt is intentional. */
7070
async function readKeychainSecret(entry: KeychainEntry, signal?: AbortSignal): Promise<Buffer> {
7171
signal?.throwIfAborted();
72-
return await new Promise((resolve, reject) => {
73-
execFile(
74-
"security",
75-
["find-generic-password", "-w", "-s", entry.service, "-a", entry.account],
76-
{ encoding: "buffer", signal },
77-
(error, stdout) => {
78-
if (error) {
79-
if (signal?.aborted) {
80-
reject(
81-
signal.reason instanceof Error
82-
? signal.reason
83-
: new Error("Browser cookie import aborted.", { cause: signal.reason ?? error }),
84-
);
85-
return;
86-
}
87-
reject(
88-
new Error(
89-
`could not read ${entry.service} from macOS Keychain; approve the prompt and retry`,
90-
),
91-
);
92-
return;
93-
}
94-
const raw = Buffer.from(stdout);
95-
let start = 0;
96-
let end = raw.length;
97-
while (start < end && isAsciiWhitespace(raw.readUInt8(start))) {
98-
start += 1;
99-
}
100-
while (end > start && isAsciiWhitespace(raw.readUInt8(end - 1))) {
101-
end -= 1;
102-
}
103-
const secret = Buffer.from(raw.subarray(start, end));
104-
raw.fill(0);
105-
if (secret.length === 0) {
106-
reject(new Error(`macOS Keychain returned an empty ${entry.service} secret`));
107-
return;
108-
}
109-
resolve(secret);
72+
let stdout: Buffer;
73+
try {
74+
const result = await runCommandBuffered(
75+
["security", "find-generic-password", "-w", "-s", entry.service, "-a", entry.account],
76+
{
77+
signal,
78+
maxOutputBytes: 1024 * 1024,
11079
},
11180
);
112-
});
81+
if (result.termination !== "exit" || result.code !== 0) {
82+
throw result.error ?? new Error(`security exited with code ${result.code ?? "unknown"}`);
83+
}
84+
stdout = result.stdout;
85+
} catch (error) {
86+
if (signal?.aborted) {
87+
throw signal.reason instanceof Error
88+
? signal.reason
89+
: new Error("Browser cookie import aborted.", { cause: signal.reason ?? error });
90+
}
91+
throw new Error(
92+
`could not read ${entry.service} from macOS Keychain; approve the prompt and retry`,
93+
{ cause: error },
94+
);
95+
}
96+
const raw = stdout;
97+
let start = 0;
98+
let end = raw.length;
99+
while (start < end && isAsciiWhitespace(raw.readUInt8(start))) {
100+
start += 1;
101+
}
102+
while (end > start && isAsciiWhitespace(raw.readUInt8(end - 1))) {
103+
end -= 1;
104+
}
105+
const secret = Buffer.from(raw.subarray(start, end));
106+
raw.fill(0);
107+
if (secret.length === 0) {
108+
throw new Error(`macOS Keychain returned an empty ${entry.service} secret`);
109+
}
110+
return secret;
113111
}
114112

115113
/** Convert Chromium's Windows-epoch microseconds to Unix seconds. */

extensions/codex/src/app-server/computer-use.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
* Computer Use plugin/MCP readiness checks and optional install flow for Codex
33
* app-server sessions.
44
*/
5-
import { execFile } from "node:child_process";
65
import { existsSync } from "node:fs";
7-
import { promisify } from "node:util";
6+
import { runExec } from "openclaw/plugin-sdk/process-runtime";
87
import { describeControlFailure } from "./capabilities.js";
98
import {
109
isCodexAppServerConnectionClosedError,
@@ -191,7 +190,6 @@ const CURATED_MARKETPLACE_POLL_INTERVAL_MS = 2_000;
191190
const COMPUTER_USE_MARKETPLACE_NAME_PRIORITY = ["openai-bundled", "openai-curated", "local"];
192191
const COMPUTER_USE_LIVE_TEST_RETRY_COUNT = 1;
193192
const COMPUTER_USE_LIVE_TEST_THREAD_NAME = "OpenClaw Computer Use readiness probe";
194-
const execFileAsync = promisify(execFile);
195193

196194
/** Reads Computer Use readiness without installing or mutating app-server state. */
197195
export async function readCodexComputerUseStatus(
@@ -1119,7 +1117,8 @@ export async function killStaleComputerUseMcpChildren(
11191117
}
11201118
let stdout: string;
11211119
try {
1122-
const result = await execFileAsync("/bin/ps", ["-axo", "pid=,ppid=,command="], {
1120+
const result = await runExec("/bin/ps", ["-axo", "pid=,ppid=,command="], {
1121+
logOutput: false,
11231122
maxBuffer: 5 * 1024 * 1024,
11241123
});
11251124
stdout = result.stdout;

0 commit comments

Comments
 (0)