@openclaw/acpx 2026.9.5 → 2026.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/.setup/command-line-CPBLOiZM.mjs +138 -0
- package/dist/.setup/{config-DvCm0_Dp.mjs → config-CdPsIued.mjs} +19 -4
- package/dist/.setup/harness-attempt-CDXMeP5Z.mjs +369 -0
- package/dist/.setup/{pi-session-catalog-runtime-CbLyvshh.mjs → pi-session-catalog-runtime-UhiQSaDw.mjs} +6 -6
- package/dist/.setup/{process-lease-B83BGiLj.mjs → process-lease-C3dNPtu8.mjs} +2 -62
- package/dist/.setup/{register.runtime-CTfxhkFm.mjs → register.runtime-Bm4HrMCp.mjs} +51 -15
- package/dist/.setup/service-DMae5pPH.mjs +2740 -0
- package/dist/.setup/{session-owner-migration-BuKHYP6F.mjs → session-owner-migration-CdJSYRk1.mjs} +15 -12
- package/dist/.setup/{session-resource-Dzl0U7kK.mjs → session-resource-BaJ6bs5d.mjs} +13 -1
- package/dist/doctor-contract-api.js +3 -3
- package/dist/index.js +266 -6
- package/dist/register.runtime.js +1 -1
- package/openclaw.plugin.json +18 -2
- package/package.json +5 -5
- package/skills/acp-router/SKILL.md +2 -2
- package/dist/.setup/rolldown-runtime-8H4AJuhK.mjs +0 -14
- package/dist/.setup/runtime-Du-YvDnr.mjs +0 -1171
- package/dist/.setup/service-DZYyo6-7.mjs +0 -1571
- package/dist/.setup/{pi-session-paths-EMbd4Hkz.mjs → pi-session-paths-CIvyk6KB.mjs} +2 -2
|
@@ -0,0 +1,2740 @@
|
|
|
1
|
+
import { t as resolveAcpxSessionResource } from "./session-resource-BaJ6bs5d.mjs";
|
|
2
|
+
import { i as toAcpMcpServers, n as resolveAcpxPluginRoot, r as resolveOpenClawRoot, t as resolveAcpxPluginConfig } from "./config-CdPsIued.mjs";
|
|
3
|
+
import { a as renderAgentCommand, c as CODEX_ACP_BIN, d as OPENCLAW_CODEX_CONFIG_ARG, i as normalizeAgentName, l as CODEX_ACP_PACKAGE, n as isCodexAcpCommand, o as resolveAgentCommand, r as isOpenClawBridgeCommand, s as splitCommandParts, t as isClaudeAcpCommand, u as LEGACY_CODEX_ACP_PACKAGE } from "./command-line-CPBLOiZM.mjs";
|
|
4
|
+
import { a as hashAcpxProcessCommand, c as openAcpxProcessLeaseStateStore, d as ACPX_GATEWAY_INSTANCE_KEY, f as ACPX_GATEWAY_INSTANCE_NAMESPACE, h as normalizeAcpxGatewayInstanceRecord, i as createAcpxProcessLeaseStore, l as readAcpxProcessLeaseIdentity, n as OPENCLAW_ACPX_LEASE_ID_ARG, r as OPENCLAW_GATEWAY_INSTANCE_ID_ARG, t as ACPX_PROBE_LEASE_SESSION_KEY, u as withAcpxLeaseArgs } from "./process-lease-C3dNPtu8.mjs";
|
|
5
|
+
import { AcpRuntimeError } from "../runtime-api.js";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { AcpxRuntime, createAgentRegistry, createFileSessionStore, decodeAcpxRuntimeHandleState, isRequestedModelUnsupportedError } from "acpx/runtime";
|
|
8
|
+
import { finiteSecondsToTimerSafeMilliseconds, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path, { resolve } from "node:path";
|
|
11
|
+
import { isRecord, normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
12
|
+
import os, { availableParallelism } from "node:os";
|
|
13
|
+
import fs$1 from "node:fs/promises";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
16
|
+
import { escapeRegExp, sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
17
|
+
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
|
|
18
|
+
import { parse, stringify } from "smol-toml";
|
|
19
|
+
import { isPidAlive, runExec } from "openclaw/plugin-sdk/process-runtime";
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
|
+
import { isDeepStrictEqual } from "node:util";
|
|
22
|
+
import { redactSensitiveText } from "openclaw/plugin-sdk/security-runtime";
|
|
23
|
+
import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
|
|
24
|
+
//#region extensions/acpx/src/codex-trust-config.ts
|
|
25
|
+
/**
|
|
26
|
+
* Builds isolated Codex config for ACPX sessions. It preserves safe inherited
|
|
27
|
+
* runtime options while rendering only trusted project entries for the session.
|
|
28
|
+
*/
|
|
29
|
+
function stripTomlComment(line) {
|
|
30
|
+
let quote = null;
|
|
31
|
+
let escaping = false;
|
|
32
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
33
|
+
const ch = line[index];
|
|
34
|
+
if (escaping) {
|
|
35
|
+
escaping = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (quote === "\"" && ch === "\\") {
|
|
39
|
+
escaping = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (quote) {
|
|
43
|
+
if (ch === quote) quote = null;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === "'" || ch === "\"") {
|
|
47
|
+
quote = ch;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === "#") return line.slice(0, index);
|
|
51
|
+
}
|
|
52
|
+
return line;
|
|
53
|
+
}
|
|
54
|
+
function parseTomlString(value) {
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) try {
|
|
57
|
+
return JSON.parse(trimmed);
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
function parseTomlDottedKey(value) {
|
|
64
|
+
const parts = [];
|
|
65
|
+
let current = "";
|
|
66
|
+
let quote = null;
|
|
67
|
+
let escaping = false;
|
|
68
|
+
for (const ch of value.trim()) {
|
|
69
|
+
if (escaping) {
|
|
70
|
+
current += ch;
|
|
71
|
+
escaping = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (quote === "\"" && ch === "\\") {
|
|
75
|
+
current += ch;
|
|
76
|
+
escaping = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (quote) {
|
|
80
|
+
current += ch;
|
|
81
|
+
if (ch === quote) quote = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (ch === "'" || ch === "\"") {
|
|
85
|
+
quote = ch;
|
|
86
|
+
current += ch;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ch === ".") {
|
|
90
|
+
parts.push(current.trim());
|
|
91
|
+
current = "";
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
current += ch;
|
|
95
|
+
}
|
|
96
|
+
if (current.trim()) parts.push(current.trim());
|
|
97
|
+
return parts.map((part) => parseTomlString(part) ?? part);
|
|
98
|
+
}
|
|
99
|
+
function parseProjectHeader(line) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) return;
|
|
102
|
+
const parts = parseTomlDottedKey(trimmed.slice(1, -1));
|
|
103
|
+
return parts.length === 2 && parts[0] === "projects" ? parts[1] : void 0;
|
|
104
|
+
}
|
|
105
|
+
function parseTrustedInlineProjectEntries(value) {
|
|
106
|
+
const trusted = [];
|
|
107
|
+
for (const match of value.matchAll(/(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*\{(?<body>[^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g)) {
|
|
108
|
+
const key = match.groups?.key;
|
|
109
|
+
const body = match.groups?.body;
|
|
110
|
+
if (!key || !body || !/\btrust_level\s*=\s*["']trusted["']/.test(body)) continue;
|
|
111
|
+
const projectPath = parseTomlString(key) ?? key.trim();
|
|
112
|
+
if (projectPath) trusted.push(projectPath);
|
|
113
|
+
}
|
|
114
|
+
return trusted;
|
|
115
|
+
}
|
|
116
|
+
/** Extract trusted project paths from Codex TOML config. */
|
|
117
|
+
function extractTrustedCodexProjectPaths(configToml) {
|
|
118
|
+
const trusted = /* @__PURE__ */ new Set();
|
|
119
|
+
let currentProjectPath;
|
|
120
|
+
let inProjectsTable = false;
|
|
121
|
+
for (const rawLine of configToml.split(/\r?\n/)) {
|
|
122
|
+
const line = stripTomlComment(rawLine).trim();
|
|
123
|
+
if (!line) continue;
|
|
124
|
+
if (line.startsWith("[")) {
|
|
125
|
+
currentProjectPath = parseProjectHeader(line);
|
|
126
|
+
inProjectsTable = line === "[projects]";
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (currentProjectPath && /^trust_level\s*=\s*["']trusted["']\s*$/.test(line)) {
|
|
130
|
+
trusted.add(currentProjectPath);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const assignment = /^(?<key>"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_\-/.~:]+)\s*=\s*(?<value>.+)$/.exec(line);
|
|
134
|
+
const rawKey = assignment?.groups?.key;
|
|
135
|
+
const rawValue = assignment?.groups?.value;
|
|
136
|
+
if (!rawKey || rawValue === void 0) continue;
|
|
137
|
+
const key = parseTomlString(rawKey) ?? rawKey;
|
|
138
|
+
const value = rawValue.trim();
|
|
139
|
+
if (inProjectsTable && /^\{.*\}$/.test(value)) {
|
|
140
|
+
if (/\btrust_level\s*=\s*["']trusted["']/.test(value) && key) trusted.add(key);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (key === "projects" || inProjectsTable) for (const projectPath of parseTrustedInlineProjectEntries(value)) trusted.add(projectPath);
|
|
144
|
+
}
|
|
145
|
+
return Array.from(trusted);
|
|
146
|
+
}
|
|
147
|
+
const INHERITED_TOP_LEVEL_CODEX_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
148
|
+
"model",
|
|
149
|
+
"model_provider",
|
|
150
|
+
"model_reasoning_effort",
|
|
151
|
+
"sandbox_mode"
|
|
152
|
+
]);
|
|
153
|
+
const INHERITED_MODEL_PROVIDER_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
154
|
+
"name",
|
|
155
|
+
"base_url",
|
|
156
|
+
"wire_api",
|
|
157
|
+
"env_key",
|
|
158
|
+
"env_key_instructions",
|
|
159
|
+
"requires_openai_auth",
|
|
160
|
+
"request_max_retries",
|
|
161
|
+
"stream_max_retries",
|
|
162
|
+
"stream_idle_timeout_ms"
|
|
163
|
+
]);
|
|
164
|
+
function parseTableHeader(line) {
|
|
165
|
+
const trimmed = line.trim();
|
|
166
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]") || trimmed.startsWith("[[")) return;
|
|
167
|
+
return parseTomlDottedKey(trimmed.slice(1, -1));
|
|
168
|
+
}
|
|
169
|
+
function isInheritedModelProviderTable(parts) {
|
|
170
|
+
return parts?.[0] === "model_providers" && parts.length === 2;
|
|
171
|
+
}
|
|
172
|
+
function parseTopLevelAssignmentKey(line) {
|
|
173
|
+
return /^(?<key>[A-Za-z0-9_-]+)\s*=\s*(?<value>.+)$/.exec(line)?.groups?.key;
|
|
174
|
+
}
|
|
175
|
+
function extractInheritedCodexRuntimeConfig(configToml) {
|
|
176
|
+
const inheritedLines = [];
|
|
177
|
+
let inAnyTable = false;
|
|
178
|
+
let inInheritedTable = false;
|
|
179
|
+
let pendingInheritedTableHeader = "";
|
|
180
|
+
function flushInheritedTableHeader() {
|
|
181
|
+
if (!pendingInheritedTableHeader) return;
|
|
182
|
+
if (inheritedLines.length > 0 && inheritedLines[inheritedLines.length - 1] !== "") inheritedLines.push("");
|
|
183
|
+
inheritedLines.push(pendingInheritedTableHeader);
|
|
184
|
+
pendingInheritedTableHeader = "";
|
|
185
|
+
}
|
|
186
|
+
for (const rawLine of configToml.split(/\r?\n/)) {
|
|
187
|
+
const trimmedLine = rawLine.trim();
|
|
188
|
+
const semanticLine = stripTomlComment(rawLine).trim();
|
|
189
|
+
if (trimmedLine.startsWith("[")) {
|
|
190
|
+
const tableParts = parseTableHeader(trimmedLine);
|
|
191
|
+
inAnyTable = true;
|
|
192
|
+
inInheritedTable = isInheritedModelProviderTable(tableParts);
|
|
193
|
+
if (inInheritedTable) pendingInheritedTableHeader = rawLine.trimEnd();
|
|
194
|
+
else pendingInheritedTableHeader = "";
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (inInheritedTable) {
|
|
198
|
+
if (!semanticLine) continue;
|
|
199
|
+
const key = parseTopLevelAssignmentKey(semanticLine);
|
|
200
|
+
if (!key || !INHERITED_MODEL_PROVIDER_CONFIG_KEYS.has(key)) continue;
|
|
201
|
+
flushInheritedTableHeader();
|
|
202
|
+
inheritedLines.push(rawLine.trimEnd());
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (inAnyTable) continue;
|
|
206
|
+
const key = parseTopLevelAssignmentKey(semanticLine);
|
|
207
|
+
if (!key) continue;
|
|
208
|
+
if (!INHERITED_TOP_LEVEL_CODEX_CONFIG_KEYS.has(key)) continue;
|
|
209
|
+
inheritedLines.push(rawLine.trimEnd());
|
|
210
|
+
}
|
|
211
|
+
while (inheritedLines.length > 0 && inheritedLines[inheritedLines.length - 1] === "") inheritedLines.pop();
|
|
212
|
+
return inheritedLines.join("\n");
|
|
213
|
+
}
|
|
214
|
+
/** Render a session-local Codex config with inherited runtime settings and trust entries. */
|
|
215
|
+
function renderIsolatedCodexConfig(params) {
|
|
216
|
+
const normalized = Array.from(new Set(params.projectPaths.map((projectPath) => projectPath.trim()).filter(Boolean).map((projectPath) => path.resolve(projectPath)))).toSorted((left, right) => left.localeCompare(right));
|
|
217
|
+
return [
|
|
218
|
+
"# Generated by OpenClaw for Codex ACP sessions.",
|
|
219
|
+
params.sourceConfigToml ? extractInheritedCodexRuntimeConfig(params.sourceConfigToml) : "",
|
|
220
|
+
...normalized.flatMap((projectPath) => [
|
|
221
|
+
"",
|
|
222
|
+
`[projects.${JSON.stringify(projectPath)}]`,
|
|
223
|
+
"trust_level = \"trusted\""
|
|
224
|
+
]),
|
|
225
|
+
""
|
|
226
|
+
].filter((line, index, lines) => !(line === "" && lines[index - 1] === "")).join("\n");
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region extensions/acpx/src/codex-auth-bridge.ts
|
|
230
|
+
/**
|
|
231
|
+
* Prepares isolated Codex and Claude ACP wrapper commands for ACPX. The bridge
|
|
232
|
+
* copies safe auth/config state into plugin-owned homes and redacts diagnostics.
|
|
233
|
+
*/
|
|
234
|
+
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
|
|
235
|
+
const CLAUDE_ACP_BIN = "claude-agent-acp";
|
|
236
|
+
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
|
|
237
|
+
const requireFromHere$1 = createRequire(import.meta.url);
|
|
238
|
+
function readSelfManifest() {
|
|
239
|
+
const manifestPath = path.join(resolveAcpxPluginRoot(import.meta.url), "package.json");
|
|
240
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
241
|
+
}
|
|
242
|
+
function readManifestDependencyVersion(packageName) {
|
|
243
|
+
const version = readSelfManifest().dependencies?.[packageName];
|
|
244
|
+
if (typeof version !== "string" || version.trim() === "") throw new Error(`Missing ${packageName} dependency version in @openclaw/acpx manifest`);
|
|
245
|
+
return version;
|
|
246
|
+
}
|
|
247
|
+
const CODEX_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CODEX_ACP_PACKAGE);
|
|
248
|
+
const CLAUDE_ACP_PACKAGE_VERSION = readManifestDependencyVersion(CLAUDE_ACP_PACKAGE);
|
|
249
|
+
function basename(value) {
|
|
250
|
+
return value.split(/[\\/]/).pop() ?? value;
|
|
251
|
+
}
|
|
252
|
+
function resolvePackageBinPath(packageJsonPath, manifest, binName) {
|
|
253
|
+
const { bin } = manifest;
|
|
254
|
+
const relativeBinPath = typeof bin === "string" ? bin : bin && typeof bin === "object" ? bin[binName] : void 0;
|
|
255
|
+
if (typeof relativeBinPath !== "string" || relativeBinPath.trim() === "") return;
|
|
256
|
+
return path.resolve(path.dirname(packageJsonPath), relativeBinPath);
|
|
257
|
+
}
|
|
258
|
+
async function resolveInstalledAcpPackageBinPath(packageName, binName) {
|
|
259
|
+
try {
|
|
260
|
+
const packageJsonPath = requireFromHere$1.resolve(`${packageName}/package.json`);
|
|
261
|
+
const { value: manifest } = await readJsonFileWithFallback(packageJsonPath, {});
|
|
262
|
+
if (manifest.name !== packageName) return;
|
|
263
|
+
const binPath = resolvePackageBinPath(packageJsonPath, manifest, binName);
|
|
264
|
+
if (!binPath) return;
|
|
265
|
+
await fs$1.access(binPath);
|
|
266
|
+
return binPath;
|
|
267
|
+
} catch {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function resolveInstalledCodexAcpBinPath() {
|
|
272
|
+
return await resolveInstalledAcpPackageBinPath(CODEX_ACP_PACKAGE, CODEX_ACP_BIN);
|
|
273
|
+
}
|
|
274
|
+
async function resolveInstalledClaudeAcpBinPath() {
|
|
275
|
+
return await resolveInstalledAcpPackageBinPath(CLAUDE_ACP_PACKAGE, CLAUDE_ACP_BIN);
|
|
276
|
+
}
|
|
277
|
+
const DIAGNOSTIC_REDACTION_RULES = [
|
|
278
|
+
{
|
|
279
|
+
source: String.raw`(authorization\s*[:=]\s*bearer\s+)[^\s'"<>]+`,
|
|
280
|
+
flags: "gi",
|
|
281
|
+
replacement: "$1[REDACTED]"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
source: String.raw`((?:api[_-]?key|apiKey|access[_-]?token|refresh[_-]?token|client[_-]?secret|token|secret|password|passwd|credential)\s*[:=]\s*)[^\s'"<>]+`,
|
|
285
|
+
flags: "gi",
|
|
286
|
+
replacement: "$1[REDACTED]"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
source: String.raw`("(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*")[^"]+`,
|
|
290
|
+
flags: "g",
|
|
291
|
+
replacement: "$1[REDACTED]"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
source: String.raw`(["']?(?:api[-_]?key|apiKey|access[-_]?token|accessToken|refresh[-_]?token|refreshToken|id[-_]?token|idToken|auth[-_]?token|authToken|client[-_]?secret|clientSecret|app[-_]?secret|appSecret|token|secret|password|passwd|credential)["']?\s*[:=]\s*["']?)[^"',}\s<>]+`,
|
|
295
|
+
flags: "gi",
|
|
296
|
+
replacement: "$1[REDACTED]"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
source: String.raw`([?&](?:access[-_]?token|auth[-_]?token|refresh[-_]?token|api[-_]?key|client[-_]?secret|token|key|secret|password|pass|passwd|auth|signature)=)[^&\s'"<>]+`,
|
|
300
|
+
flags: "gi",
|
|
301
|
+
replacement: "$1[REDACTED]"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
source: String.raw`(--(?:api[-_]?key|token|secret|password|passwd)\s+)[^\s'"]+`,
|
|
305
|
+
flags: "gi",
|
|
306
|
+
replacement: "$1[REDACTED]"
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
source: String.raw`-----BEGIN [A-Z ]*PRI` + String.raw`VATE KEY-----[\s\S]+?-----END [A-Z ]*PRI` + String.raw`VATE KEY-----`,
|
|
310
|
+
flags: "g",
|
|
311
|
+
replacement: "[REDACTED_PRIVATE_KEY]"
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
source: String.raw`\b(sk-[A-Za-z0-9_-]{8,})\b`,
|
|
315
|
+
flags: "g",
|
|
316
|
+
replacement: "[REDACTED_OPENAI_KEY]"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
source: String.raw`\b(gh[pousr]_[A-Za-z0-9_]{20,})\b`,
|
|
320
|
+
flags: "g",
|
|
321
|
+
replacement: "[REDACTED_GITHUB_TOKEN]"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
source: String.raw`\b(github_pat_[A-Za-z0-9_]{20,})\b`,
|
|
325
|
+
flags: "g",
|
|
326
|
+
replacement: "[REDACTED_GITHUB_TOKEN]"
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
source: String.raw`\b(xox[baprs]-[A-Za-z0-9-]{10,})\b`,
|
|
330
|
+
flags: "g",
|
|
331
|
+
replacement: "[REDACTED_SLACK_TOKEN]"
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
source: String.raw`\b(gsk_[A-Za-z0-9_-]{10,})\b`,
|
|
335
|
+
flags: "g",
|
|
336
|
+
replacement: "[REDACTED_API_KEY]"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
source: String.raw`\b(AIza[0-9A-Za-z\-_]{20,})\b`,
|
|
340
|
+
flags: "g",
|
|
341
|
+
replacement: "[REDACTED_GOOGLE_KEY]"
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
source: String.raw`\b(ya29\.[0-9A-Za-z_\-./+=]{10,})\b`,
|
|
345
|
+
flags: "g",
|
|
346
|
+
replacement: "[REDACTED_GOOGLE_TOKEN]"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
source: String.raw`\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b`,
|
|
350
|
+
flags: "g",
|
|
351
|
+
replacement: "[REDACTED_JWT]"
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
source: String.raw`\b(pplx-[A-Za-z0-9_-]{10,})\b`,
|
|
355
|
+
flags: "g",
|
|
356
|
+
replacement: "[REDACTED_API_KEY]"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
source: String.raw`\b(npm_[A-Za-z0-9]{10,})\b`,
|
|
360
|
+
flags: "g",
|
|
361
|
+
replacement: "[REDACTED_NPM_TOKEN]"
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
source: String.raw`\b(LTAI[A-Za-z0-9]{10,})\b`,
|
|
365
|
+
flags: "g",
|
|
366
|
+
replacement: "[REDACTED_ACCESS_KEY]"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
source: String.raw`\b(hf_[A-Za-z0-9]{10,})\b`,
|
|
370
|
+
flags: "g",
|
|
371
|
+
replacement: "[REDACTED_API_KEY]"
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
source: String.raw`\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
|
375
|
+
flags: "g",
|
|
376
|
+
replacement: "bot[REDACTED_TELEGRAM_TOKEN]"
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
source: String.raw`\b(\d{6,}:[A-Za-z0-9_-]{20,})\b`,
|
|
380
|
+
flags: "g",
|
|
381
|
+
replacement: "[REDACTED_TELEGRAM_TOKEN]"
|
|
382
|
+
}
|
|
383
|
+
];
|
|
384
|
+
function buildAdapterWrapperScript(params) {
|
|
385
|
+
return `#!/usr/bin/env node
|
|
386
|
+
import { appendFileSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
387
|
+
import path from "node:path";
|
|
388
|
+
import { spawn } from "node:child_process";
|
|
389
|
+
import { StringDecoder } from "node:string_decoder";
|
|
390
|
+
import { fileURLToPath } from "node:url";
|
|
391
|
+
|
|
392
|
+
${params.envSetup}
|
|
393
|
+
const stderrLogFileNamePrefix = ${params.stderrLogFileNamePrefix ? JSON.stringify(params.stderrLogFileNamePrefix) : "undefined"};
|
|
394
|
+
const stderrLogMaxChars = 256 * 1024;
|
|
395
|
+
|
|
396
|
+
const openClawWrapperArgs = new Set([
|
|
397
|
+
${JSON.stringify(OPENCLAW_ACPX_LEASE_ID_ARG)},
|
|
398
|
+
${JSON.stringify(OPENCLAW_GATEWAY_INSTANCE_ID_ARG)},
|
|
399
|
+
${(params.openClawWrapperArgs ?? []).map((arg) => JSON.stringify(arg)).join(",\n ")}
|
|
400
|
+
]);
|
|
401
|
+
|
|
402
|
+
function readOpenClawWrapperArg(args, name) {
|
|
403
|
+
const index = args.indexOf(name);
|
|
404
|
+
if (index < 0) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
const value = args[index + 1];
|
|
408
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function readOpenClawWrapperArgs(args, name) {
|
|
412
|
+
const values = [];
|
|
413
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
414
|
+
if (args[index] !== name) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const value = args[index + 1];
|
|
418
|
+
if (typeof value === "string" && value.trim()) {
|
|
419
|
+
values.push(value.trim());
|
|
420
|
+
}
|
|
421
|
+
index += 1;
|
|
422
|
+
}
|
|
423
|
+
return values;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function safeDiagnosticFilePart(value) {
|
|
427
|
+
const sanitized = String(value || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120);
|
|
428
|
+
return sanitized || "pid-" + process.pid;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function resolveStderrLogPath(args) {
|
|
432
|
+
if (!stderrLogFileNamePrefix) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
const leaseId =
|
|
436
|
+
readOpenClawWrapperArg(args, ${JSON.stringify(OPENCLAW_ACPX_LEASE_ID_ARG)}) ||
|
|
437
|
+
"pid-" + process.pid;
|
|
438
|
+
const fileName = stderrLogFileNamePrefix + "." + safeDiagnosticFilePart(leaseId) + ".log";
|
|
439
|
+
return fileURLToPath(new URL("./" + fileName, import.meta.url));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const diagnosticRedactionRules = ${JSON.stringify(DIAGNOSTIC_REDACTION_RULES)}.map((rule) => [
|
|
443
|
+
new RegExp(rule.source, rule.flags),
|
|
444
|
+
rule.replacement,
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
function redactDiagnosticText(text) {
|
|
448
|
+
let redacted = text;
|
|
449
|
+
for (const [pattern, replacement] of diagnosticRedactionRules) {
|
|
450
|
+
redacted = redacted.replace(pattern, replacement);
|
|
451
|
+
}
|
|
452
|
+
return redacted;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function tailUtf16Safe(text, maxChars) {
|
|
456
|
+
let start = Math.max(0, text.length - maxChars);
|
|
457
|
+
const startsInsideSurrogatePair =
|
|
458
|
+
start > 0 &&
|
|
459
|
+
start < text.length &&
|
|
460
|
+
text.charCodeAt(start) >= 0xdc00 &&
|
|
461
|
+
text.charCodeAt(start) <= 0xdfff &&
|
|
462
|
+
text.charCodeAt(start - 1) >= 0xd800 &&
|
|
463
|
+
text.charCodeAt(start - 1) <= 0xdbff;
|
|
464
|
+
if (startsInsideSurrogatePair) {
|
|
465
|
+
start += 1;
|
|
466
|
+
}
|
|
467
|
+
return text.slice(start);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let pendingStderrLogText = "";
|
|
471
|
+
// Pipe chunks can split a UTF-8 sequence. Preserve decoder state so diagnostic
|
|
472
|
+
// capture does not manufacture replacement characters between chunks.
|
|
473
|
+
const stderrDecoder = new StringDecoder("utf8");
|
|
474
|
+
const stderrPrivateKeyEndPattern = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
475
|
+
|
|
476
|
+
function hasUnclosedPrivateKeyBlock(text) {
|
|
477
|
+
let lastBeginIndex = -1;
|
|
478
|
+
for (const match of text.matchAll(/-----BEGIN [A-Z ]*PRIVATE KEY-----/g)) {
|
|
479
|
+
lastBeginIndex = match.index ?? lastBeginIndex;
|
|
480
|
+
}
|
|
481
|
+
if (lastBeginIndex === -1) {
|
|
482
|
+
return -1;
|
|
483
|
+
}
|
|
484
|
+
return stderrPrivateKeyEndPattern.test(text.slice(lastBeginIndex)) ? -1 : lastBeginIndex;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function writeRedactedStderrLog(text) {
|
|
488
|
+
if (!stderrLogPath) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (!text) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
appendFileSync(stderrLogPath, redactDiagnosticText(text), "utf8");
|
|
496
|
+
const current = readFileSync(stderrLogPath, "utf8");
|
|
497
|
+
if (current.length > stderrLogMaxChars) {
|
|
498
|
+
writeFileSync(stderrLogPath, tailUtf16Safe(current, stderrLogMaxChars), "utf8");
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
// Stderr capture is diagnostic-only; never break the ACP adapter.
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function redactIncompletePrivateKeyTail(text) {
|
|
506
|
+
const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(text);
|
|
507
|
+
if (unclosedPrivateKeyStart === -1) {
|
|
508
|
+
return text;
|
|
509
|
+
}
|
|
510
|
+
return text.slice(0, unclosedPrivateKeyStart) + "[REDACTED_PRIVATE_KEY]";
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function flushFinalizedStderrLogText() {
|
|
514
|
+
const lastLineBreak = pendingStderrLogText.lastIndexOf("\\n");
|
|
515
|
+
if (lastLineBreak === -1) {
|
|
516
|
+
if (pendingStderrLogText.length > stderrLogMaxChars) {
|
|
517
|
+
pendingStderrLogText = tailUtf16Safe(pendingStderrLogText, stderrLogMaxChars);
|
|
518
|
+
}
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
let flushEnd = lastLineBreak + 1;
|
|
522
|
+
const unclosedPrivateKeyStart = hasUnclosedPrivateKeyBlock(
|
|
523
|
+
pendingStderrLogText.slice(0, flushEnd),
|
|
524
|
+
);
|
|
525
|
+
if (unclosedPrivateKeyStart !== -1) {
|
|
526
|
+
flushEnd = unclosedPrivateKeyStart;
|
|
527
|
+
}
|
|
528
|
+
if (flushEnd <= 0) {
|
|
529
|
+
if (pendingStderrLogText.length > stderrLogMaxChars) {
|
|
530
|
+
pendingStderrLogText = tailUtf16Safe(pendingStderrLogText, stderrLogMaxChars);
|
|
531
|
+
}
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const finalizedText = pendingStderrLogText.slice(0, flushEnd);
|
|
535
|
+
pendingStderrLogText = pendingStderrLogText.slice(flushEnd);
|
|
536
|
+
writeRedactedStderrLog(finalizedText);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function appendStderrLog(chunk) {
|
|
540
|
+
const text = stderrDecoder.write(chunk);
|
|
541
|
+
if (!text) {
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
pendingStderrLogText += text;
|
|
545
|
+
flushFinalizedStderrLogText();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function finishStderrLog() {
|
|
549
|
+
pendingStderrLogText += stderrDecoder.end();
|
|
550
|
+
const text = redactIncompletePrivateKeyTail(pendingStderrLogText);
|
|
551
|
+
pendingStderrLogText = "";
|
|
552
|
+
writeRedactedStderrLog(text);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function stripOpenClawWrapperArgs(args) {
|
|
556
|
+
const stripped = [];
|
|
557
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
558
|
+
const value = args[index];
|
|
559
|
+
if (openClawWrapperArgs.has(value)) {
|
|
560
|
+
index += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
stripped.push(value);
|
|
564
|
+
}
|
|
565
|
+
return stripped;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const rawConfiguredArgs = process.argv.slice(2);
|
|
569
|
+
${params.envConfigSetup ?? ""}
|
|
570
|
+
const stderrLogPath = resolveStderrLogPath(rawConfiguredArgs);
|
|
571
|
+
if (stderrLogPath) {
|
|
572
|
+
try {
|
|
573
|
+
rmSync(stderrLogPath, { force: true });
|
|
574
|
+
} catch {
|
|
575
|
+
// Diagnostic cleanup must never prevent the adapter from starting.
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const configuredArgs = stripOpenClawWrapperArgs(rawConfiguredArgs);
|
|
580
|
+
|
|
581
|
+
function resolveNpmCliPath() {
|
|
582
|
+
const candidate = path.resolve(
|
|
583
|
+
path.dirname(process.execPath),
|
|
584
|
+
"..",
|
|
585
|
+
"lib",
|
|
586
|
+
"node_modules",
|
|
587
|
+
"npm",
|
|
588
|
+
"bin",
|
|
589
|
+
"npm-cli.js",
|
|
590
|
+
);
|
|
591
|
+
return existsSync(candidate) ? candidate : undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const npmCliPath = resolveNpmCliPath();
|
|
595
|
+
const installedBinPath = ${params.installedBinPath ? JSON.stringify(params.installedBinPath) : "undefined"};
|
|
596
|
+
let defaultCommand;
|
|
597
|
+
let defaultArgs;
|
|
598
|
+
if (installedBinPath) {
|
|
599
|
+
defaultCommand = process.execPath;
|
|
600
|
+
defaultArgs = [installedBinPath];
|
|
601
|
+
} else if (npmCliPath) {
|
|
602
|
+
defaultCommand = process.execPath;
|
|
603
|
+
defaultArgs = [npmCliPath, "exec", "--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
|
|
604
|
+
} else {
|
|
605
|
+
defaultCommand = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
606
|
+
defaultArgs = ["--yes", "--package", "${params.packageSpec}", "--", "${params.binName}"];
|
|
607
|
+
}
|
|
608
|
+
const command =
|
|
609
|
+
configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}" ? configuredArgs[1] : defaultCommand;
|
|
610
|
+
const args =
|
|
611
|
+
configuredArgs[0] === "${RUN_CONFIGURED_COMMAND_SENTINEL}"
|
|
612
|
+
? configuredArgs.slice(2)
|
|
613
|
+
: [...defaultArgs, ...configuredArgs];
|
|
614
|
+
|
|
615
|
+
if (!command) {
|
|
616
|
+
console.error("[openclaw] missing configured ${params.displayName} ACP command");
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const child = spawn(command, args, {
|
|
621
|
+
detached: process.platform !== "win32",
|
|
622
|
+
env,
|
|
623
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
624
|
+
windowsHide: true,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
child.stderr?.on("data", (chunk) => {
|
|
628
|
+
appendStderrLog(chunk);
|
|
629
|
+
process.stderr.write(chunk);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
let forceKillTimer;
|
|
633
|
+
let orphanCleanupStarted = false;
|
|
634
|
+
let childExitCode = 1;
|
|
635
|
+
|
|
636
|
+
function killChildTree(signal, options = {}) {
|
|
637
|
+
if (!child.pid || (!options.force && child.killed)) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (process.platform !== "win32") {
|
|
641
|
+
try {
|
|
642
|
+
// The adapter can spawn grandchildren; signaling the process group keeps
|
|
643
|
+
// the generated wrapper from leaving an ACP tree behind.
|
|
644
|
+
process.kill(-child.pid, signal);
|
|
645
|
+
return;
|
|
646
|
+
} catch {
|
|
647
|
+
// Fall back to direct child signaling below.
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
child.kill(signal);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
654
|
+
process.once(signal, () => {
|
|
655
|
+
killChildTree(signal);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const originalParentPid = process.ppid;
|
|
660
|
+
const parentWatcher =
|
|
661
|
+
process.platform === "win32"
|
|
662
|
+
? undefined
|
|
663
|
+
: setInterval(() => {
|
|
664
|
+
// Orphan detection: parent PID changed means our original parent died.
|
|
665
|
+
// The new parent could be PID 1 (init) on bare-metal hosts, OR a
|
|
666
|
+
// systemd user-session manager, OR a container init, OR a session
|
|
667
|
+
// leader — depending on environment. Previously this only triggered
|
|
668
|
+
// on PPID == 1, which missed all systemd-managed deployments and
|
|
669
|
+
// leaked codex-acp adapter trees on every gateway restart.
|
|
670
|
+
if (process.ppid === originalParentPid) {
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (orphanCleanupStarted) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
orphanCleanupStarted = true;
|
|
677
|
+
if (parentWatcher) {
|
|
678
|
+
clearInterval(parentWatcher);
|
|
679
|
+
}
|
|
680
|
+
killChildTree("SIGTERM");
|
|
681
|
+
// Keep the wrapper alive long enough for stubborn adapters to receive
|
|
682
|
+
// a forced fallback signal after SIGTERM.
|
|
683
|
+
forceKillTimer = setTimeout(() => {
|
|
684
|
+
killChildTree("SIGKILL", { force: true });
|
|
685
|
+
childExitCode = 1;
|
|
686
|
+
}, 1_500);
|
|
687
|
+
}, 1_000);
|
|
688
|
+
parentWatcher?.unref?.();
|
|
689
|
+
|
|
690
|
+
child.on("error", (error) => {
|
|
691
|
+
console.error(\`[openclaw] failed to launch ${params.displayName} ACP wrapper: \${error.message}\`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
child.on("exit", (code, signal) => {
|
|
696
|
+
if (parentWatcher) {
|
|
697
|
+
clearInterval(parentWatcher);
|
|
698
|
+
}
|
|
699
|
+
if (orphanCleanupStarted) {
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
if (forceKillTimer) {
|
|
703
|
+
clearTimeout(forceKillTimer);
|
|
704
|
+
}
|
|
705
|
+
if (code !== null) {
|
|
706
|
+
childExitCode = code;
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
childExitCode = signal ? 1 : 0;
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
child.on("close", () => {
|
|
713
|
+
finishStderrLog();
|
|
714
|
+
process.exit(childExitCode);
|
|
715
|
+
});
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
function buildCodexAcpWrapperScript(installedBinPath) {
|
|
719
|
+
return buildAdapterWrapperScript({
|
|
720
|
+
displayName: "Codex",
|
|
721
|
+
packageSpec: `${CODEX_ACP_PACKAGE}@${CODEX_ACP_PACKAGE_VERSION}`,
|
|
722
|
+
binName: CODEX_ACP_BIN,
|
|
723
|
+
installedBinPath,
|
|
724
|
+
stderrLogFileNamePrefix: "codex-acp-wrapper.stderr",
|
|
725
|
+
openClawWrapperArgs: [OPENCLAW_CODEX_CONFIG_ARG],
|
|
726
|
+
envSetup: `const codexHome = fileURLToPath(new URL("./codex-home/", import.meta.url));
|
|
727
|
+
const codexAuthPath = fileURLToPath(new URL("./codex-home/auth.json", import.meta.url));
|
|
728
|
+
const codexApiKey = (process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || "").trim();
|
|
729
|
+
let shouldWriteCodexApiKeyAuth = false;
|
|
730
|
+
if (codexApiKey) {
|
|
731
|
+
if (!existsSync(codexAuthPath)) {
|
|
732
|
+
shouldWriteCodexApiKeyAuth = true;
|
|
733
|
+
} else {
|
|
734
|
+
try {
|
|
735
|
+
const existingCodexAuth = JSON.parse(readFileSync(codexAuthPath, "utf8"));
|
|
736
|
+
shouldWriteCodexApiKeyAuth =
|
|
737
|
+
!existingCodexAuth ||
|
|
738
|
+
typeof existingCodexAuth !== "object" ||
|
|
739
|
+
typeof existingCodexAuth.OPENAI_API_KEY === "string";
|
|
740
|
+
} catch {
|
|
741
|
+
shouldWriteCodexApiKeyAuth = true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (shouldWriteCodexApiKeyAuth) {
|
|
746
|
+
writeFileSync(
|
|
747
|
+
codexAuthPath,
|
|
748
|
+
JSON.stringify({
|
|
749
|
+
OPENAI_API_KEY: codexApiKey,
|
|
750
|
+
tokens: null,
|
|
751
|
+
last_refresh: null,
|
|
752
|
+
}) + "\\n",
|
|
753
|
+
{ mode: 0o600 },
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
const env = {
|
|
757
|
+
...process.env,
|
|
758
|
+
CODEX_HOME: codexHome,
|
|
759
|
+
};`,
|
|
760
|
+
envConfigSetup: `function isCodexConfigObject(value) {
|
|
761
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function mergeCodexConfig(base, override) {
|
|
765
|
+
const merged = Object.assign(Object.create(null), base);
|
|
766
|
+
for (const [key, value] of Object.entries(override)) {
|
|
767
|
+
const existing = merged[key];
|
|
768
|
+
merged[key] =
|
|
769
|
+
isCodexConfigObject(existing) && isCodexConfigObject(value)
|
|
770
|
+
? mergeCodexConfig(existing, value)
|
|
771
|
+
: value;
|
|
772
|
+
}
|
|
773
|
+
return merged;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const openClawCodexConfigs = readOpenClawWrapperArgs(
|
|
777
|
+
rawConfiguredArgs,
|
|
778
|
+
${JSON.stringify(OPENCLAW_CODEX_CONFIG_ARG)},
|
|
779
|
+
);
|
|
780
|
+
if (openClawCodexConfigs.length > 0) {
|
|
781
|
+
let existingCodexConfig = {};
|
|
782
|
+
if (typeof env.CODEX_CONFIG === "string" && env.CODEX_CONFIG.trim()) {
|
|
783
|
+
try {
|
|
784
|
+
const parsedCodexConfig = JSON.parse(env.CODEX_CONFIG);
|
|
785
|
+
if (!parsedCodexConfig || typeof parsedCodexConfig !== "object" || Array.isArray(parsedCodexConfig)) {
|
|
786
|
+
throw new Error("CODEX_CONFIG must be a JSON object");
|
|
787
|
+
}
|
|
788
|
+
existingCodexConfig = parsedCodexConfig;
|
|
789
|
+
} catch {
|
|
790
|
+
console.error("[openclaw] CODEX_CONFIG must be a valid JSON object");
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
for (const openClawCodexConfig of openClawCodexConfigs) {
|
|
795
|
+
try {
|
|
796
|
+
const parsedOpenClawCodexConfig = JSON.parse(openClawCodexConfig);
|
|
797
|
+
if (
|
|
798
|
+
!parsedOpenClawCodexConfig ||
|
|
799
|
+
typeof parsedOpenClawCodexConfig !== "object" ||
|
|
800
|
+
Array.isArray(parsedOpenClawCodexConfig)
|
|
801
|
+
) {
|
|
802
|
+
throw new Error("invalid OpenClaw Codex config");
|
|
803
|
+
}
|
|
804
|
+
existingCodexConfig = mergeCodexConfig(existingCodexConfig, parsedOpenClawCodexConfig);
|
|
805
|
+
} catch {
|
|
806
|
+
console.error("[openclaw] invalid generated Codex ACP startup config");
|
|
807
|
+
process.exit(1);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
env.CODEX_CONFIG = JSON.stringify(existingCodexConfig);
|
|
811
|
+
}`
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function buildClaudeAcpWrapperScript(installedBinPath) {
|
|
815
|
+
return buildAdapterWrapperScript({
|
|
816
|
+
displayName: "Claude",
|
|
817
|
+
packageSpec: `${CLAUDE_ACP_PACKAGE}@${CLAUDE_ACP_PACKAGE_VERSION}`,
|
|
818
|
+
binName: CLAUDE_ACP_BIN,
|
|
819
|
+
installedBinPath,
|
|
820
|
+
envSetup: `const env = {
|
|
821
|
+
...process.env,
|
|
822
|
+
};`
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
async function readSourceCodexConfig(codexHome) {
|
|
826
|
+
try {
|
|
827
|
+
return await fs$1.readFile(path.join(codexHome, "config.toml"), "utf8");
|
|
828
|
+
} catch (error) {
|
|
829
|
+
if (error.code === "ENOENT") return;
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function prepareIsolatedCodexHome(params) {
|
|
834
|
+
const sourceConfig = await readSourceCodexConfig(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
835
|
+
const trustedProjectPaths = [...sourceConfig ? extractTrustedCodexProjectPaths(sourceConfig) : [], params.workspaceDir];
|
|
836
|
+
const codexHome = path.join(params.baseDir, "codex-home");
|
|
837
|
+
await fs$1.mkdir(codexHome, { recursive: true });
|
|
838
|
+
await fs$1.writeFile(path.join(codexHome, "config.toml"), renderIsolatedCodexConfig({
|
|
839
|
+
sourceConfigToml: sourceConfig,
|
|
840
|
+
projectPaths: trustedProjectPaths
|
|
841
|
+
}), "utf8");
|
|
842
|
+
return codexHome;
|
|
843
|
+
}
|
|
844
|
+
async function writeAdapterWrapper(baseDir, fileName, script) {
|
|
845
|
+
await fs$1.mkdir(baseDir, { recursive: true });
|
|
846
|
+
const wrapperPath = path.join(baseDir, fileName);
|
|
847
|
+
await fs$1.writeFile(wrapperPath, script, { encoding: "utf8" });
|
|
848
|
+
try {
|
|
849
|
+
await fs$1.chmod(wrapperPath, 493);
|
|
850
|
+
} catch {}
|
|
851
|
+
return wrapperPath;
|
|
852
|
+
}
|
|
853
|
+
function buildWrapperCommand(wrapperPath, args = []) {
|
|
854
|
+
return [
|
|
855
|
+
process.execPath,
|
|
856
|
+
wrapperPath,
|
|
857
|
+
...args
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
function isAcpPackageSpec(value, packageName) {
|
|
861
|
+
const escapedPackageName = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
862
|
+
return new RegExp(`^${escapedPackageName}(?:@.+)?$`, "i").test(value.trim());
|
|
863
|
+
}
|
|
864
|
+
function isAcpBinName(value, binName) {
|
|
865
|
+
const commandName = basename(value);
|
|
866
|
+
const escapedBinName = binName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
867
|
+
return new RegExp(`^${escapedBinName}(?:\\.exe|\\.[cm]?js)?$`, "i").test(commandName);
|
|
868
|
+
}
|
|
869
|
+
function isPackageRunnerCommand(value) {
|
|
870
|
+
return /^(?:npx|npm|pnpm|bunx)(?:\.cmd|\.exe)?$/i.test(basename(value));
|
|
871
|
+
}
|
|
872
|
+
function extractConfiguredAdapterArgs(params) {
|
|
873
|
+
const parts = splitCommandParts(params.configuredCommand ?? []);
|
|
874
|
+
if (!parts.length) return [];
|
|
875
|
+
const packageIndex = parts.findIndex((part) => isAcpPackageSpec(part, params.packageName));
|
|
876
|
+
if (packageIndex >= 0) {
|
|
877
|
+
if (!isPackageRunnerCommand(parts[0] ?? "")) return;
|
|
878
|
+
const afterPackage = parts.slice(packageIndex + 1);
|
|
879
|
+
if (afterPackage[0] === "--" && isAcpBinName(afterPackage[1] ?? "", params.binName)) return afterPackage.slice(2);
|
|
880
|
+
if (isAcpBinName(afterPackage[0] ?? "", params.binName)) return afterPackage.slice(1);
|
|
881
|
+
return afterPackage[0] === "--" ? afterPackage.slice(1) : afterPackage;
|
|
882
|
+
}
|
|
883
|
+
if (isAcpBinName(parts[0] ?? "", params.binName)) return parts.slice(1);
|
|
884
|
+
if (basename(parts[0] ?? "") === "node" && isAcpBinName(parts[1] ?? "", params.binName)) return parts.slice(2);
|
|
885
|
+
}
|
|
886
|
+
function mergeConfigRecords(base, override) {
|
|
887
|
+
const merged = { ...base };
|
|
888
|
+
for (const [key, value] of Object.entries(override)) {
|
|
889
|
+
const existing = merged[key];
|
|
890
|
+
const nextValue = isRecord(existing) && isRecord(value) ? mergeConfigRecords(existing, value) : value;
|
|
891
|
+
Object.defineProperty(merged, key, {
|
|
892
|
+
value: nextValue,
|
|
893
|
+
configurable: true,
|
|
894
|
+
enumerable: true,
|
|
895
|
+
writable: true
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
return merged;
|
|
899
|
+
}
|
|
900
|
+
function parseLegacyCodexConfigAssignment(assignment) {
|
|
901
|
+
const separator = assignment.indexOf("=");
|
|
902
|
+
if (separator <= 0) throw new Error(`Invalid legacy Codex ACP config override: ${assignment}`);
|
|
903
|
+
const rawKey = assignment.slice(0, separator).trim();
|
|
904
|
+
const key = rawKey === "use_legacy_landlock" ? "features.use_legacy_landlock" : rawKey;
|
|
905
|
+
const rawValue = assignment.slice(separator + 1).trim();
|
|
906
|
+
try {
|
|
907
|
+
return parse(`${key} = ${rawValue}`);
|
|
908
|
+
} catch {
|
|
909
|
+
const literal = rawValue.replace(/^["']+|["']+$/g, "");
|
|
910
|
+
return parse(`${key} = ${JSON.stringify(literal)}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
function migrateLegacyCodexArgs(args) {
|
|
914
|
+
let config = {};
|
|
915
|
+
const forwardedArgs = [];
|
|
916
|
+
let hadOverrides = false;
|
|
917
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
918
|
+
const arg = args[index] ?? "";
|
|
919
|
+
let assignment;
|
|
920
|
+
if (arg === "-c" || arg === "--config") assignment = args[index += 1];
|
|
921
|
+
else if (arg.startsWith("--config=")) assignment = arg.slice(9);
|
|
922
|
+
else if (arg.startsWith("-c=")) assignment = arg.slice(3);
|
|
923
|
+
else if (arg.startsWith("-c") && arg.length > 2) assignment = arg.slice(2);
|
|
924
|
+
else {
|
|
925
|
+
forwardedArgs.push(arg);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
if (!assignment) throw new Error(`Missing value for legacy Codex ACP option ${arg}`);
|
|
929
|
+
hadOverrides = true;
|
|
930
|
+
config = mergeConfigRecords(config, parseLegacyCodexConfigAssignment(assignment));
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
config,
|
|
934
|
+
forwardedArgs,
|
|
935
|
+
hadOverrides
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function resolveCodexAdapterLaunch(configuredCommand) {
|
|
939
|
+
const legacyAdapterArgs = extractConfiguredAdapterArgs({
|
|
940
|
+
configuredCommand,
|
|
941
|
+
packageName: LEGACY_CODEX_ACP_PACKAGE,
|
|
942
|
+
binName: CODEX_ACP_BIN
|
|
943
|
+
});
|
|
944
|
+
if (legacyAdapterArgs) {
|
|
945
|
+
const migration = migrateLegacyCodexArgs(legacyAdapterArgs);
|
|
946
|
+
return {
|
|
947
|
+
args: [...migration.hadOverrides ? [OPENCLAW_CODEX_CONFIG_ARG, JSON.stringify(migration.config)] : [], ...migration.forwardedArgs],
|
|
948
|
+
...migration.hadOverrides ? { migratedConfig: migration.config } : {}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const maintainedAdapterArgs = extractConfiguredAdapterArgs({
|
|
952
|
+
configuredCommand,
|
|
953
|
+
packageName: CODEX_ACP_PACKAGE,
|
|
954
|
+
binName: CODEX_ACP_BIN
|
|
955
|
+
});
|
|
956
|
+
if (!maintainedAdapterArgs) return;
|
|
957
|
+
return { args: maintainedAdapterArgs };
|
|
958
|
+
}
|
|
959
|
+
async function persistMigratedCodexMcpConfig(params) {
|
|
960
|
+
const mcpServers = params.migratedConfig?.mcp_servers;
|
|
961
|
+
if (!isRecord(mcpServers)) return;
|
|
962
|
+
const configPath = path.join(params.codexHome, "config.toml");
|
|
963
|
+
const merged = mergeConfigRecords(parse(await fs$1.readFile(configPath, "utf8")), { mcp_servers: mcpServers });
|
|
964
|
+
await fs$1.writeFile(configPath, stringify(merged), "utf8");
|
|
965
|
+
}
|
|
966
|
+
function buildClaudeAcpWrapperCommand(wrapperPath, configuredCommand) {
|
|
967
|
+
const configuredAdapterArgs = extractConfiguredAdapterArgs({
|
|
968
|
+
configuredCommand,
|
|
969
|
+
packageName: CLAUDE_ACP_PACKAGE,
|
|
970
|
+
binName: CLAUDE_ACP_BIN
|
|
971
|
+
});
|
|
972
|
+
if (configuredAdapterArgs) return buildWrapperCommand(wrapperPath, configuredAdapterArgs);
|
|
973
|
+
return configuredCommand ?? buildWrapperCommand(wrapperPath);
|
|
974
|
+
}
|
|
975
|
+
/** Prepare ACPX agent commands and isolated auth homes for Codex/Claude adapters. */
|
|
976
|
+
async function prepareAcpxCodexAuthConfig(params) {
|
|
977
|
+
params.logger;
|
|
978
|
+
const codexBaseDir = path.join(params.stateDir, "acpx");
|
|
979
|
+
const configuredCodexCommand = params.pluginConfig.agents.codex;
|
|
980
|
+
const configuredClaudeCommand = params.pluginConfig.agents.claude;
|
|
981
|
+
const codexLaunch = resolveCodexAdapterLaunch(configuredCodexCommand);
|
|
982
|
+
await persistMigratedCodexMcpConfig({
|
|
983
|
+
codexHome: await prepareIsolatedCodexHome({
|
|
984
|
+
baseDir: codexBaseDir,
|
|
985
|
+
workspaceDir: params.pluginConfig.cwd
|
|
986
|
+
}),
|
|
987
|
+
migratedConfig: codexLaunch?.migratedConfig
|
|
988
|
+
});
|
|
989
|
+
const installedCodexBinPath = await (params.resolveInstalledCodexAcpBinPath ?? resolveInstalledCodexAcpBinPath)();
|
|
990
|
+
const installedClaudeBinPath = await (params.resolveInstalledClaudeAcpBinPath ?? resolveInstalledClaudeAcpBinPath)();
|
|
991
|
+
const wrapperPath = await writeAdapterWrapper(codexBaseDir, "codex-acp-wrapper.mjs", buildCodexAcpWrapperScript(installedCodexBinPath));
|
|
992
|
+
const claudeWrapperPath = await writeAdapterWrapper(codexBaseDir, "claude-agent-acp-wrapper.mjs", buildClaudeAcpWrapperScript(installedClaudeBinPath));
|
|
993
|
+
return {
|
|
994
|
+
...params.pluginConfig,
|
|
995
|
+
agents: {
|
|
996
|
+
...params.pluginConfig.agents,
|
|
997
|
+
codex: buildWrapperCommand(wrapperPath, codexLaunch?.args ?? [RUN_CONFIGURED_COMMAND_SENTINEL, ...splitCommandParts(configuredCodexCommand ?? [])]),
|
|
998
|
+
claude: buildClaudeAcpWrapperCommand(claudeWrapperPath, configuredClaudeCommand)
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
//#endregion
|
|
1003
|
+
//#region extensions/acpx/src/process-reaper.ts
|
|
1004
|
+
/**
|
|
1005
|
+
* ACPX process ownership checks and cleanup. The reaper only terminates
|
|
1006
|
+
* OpenClaw-owned wrapper trees after validating paths, packages, and lease ids.
|
|
1007
|
+
*/
|
|
1008
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
1009
|
+
const GENERATED_WRAPPER_BASENAMES = /* @__PURE__ */ new Set(["codex-acp-wrapper.mjs", "claude-agent-acp-wrapper.mjs"]);
|
|
1010
|
+
const OPENCLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
|
|
1011
|
+
const ACPX_PROCESS_LIST_TIMEOUT_MS = 2e3;
|
|
1012
|
+
const OWNED_ACP_PACKAGE_NAMES = [
|
|
1013
|
+
CODEX_ACP_PACKAGE,
|
|
1014
|
+
LEGACY_CODEX_ACP_PACKAGE,
|
|
1015
|
+
"@zed-industries/codex-acp-darwin-arm64",
|
|
1016
|
+
"@zed-industries/codex-acp-darwin-x64",
|
|
1017
|
+
"@zed-industries/codex-acp-linux-arm64",
|
|
1018
|
+
"@zed-industries/codex-acp-linux-x64",
|
|
1019
|
+
"@zed-industries/codex-acp-win32-arm64",
|
|
1020
|
+
"@zed-industries/codex-acp-win32-x64",
|
|
1021
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
1022
|
+
"acpx"
|
|
1023
|
+
];
|
|
1024
|
+
const PLUGIN_DEPS_CODEX_PACKAGE_NAMES = [
|
|
1025
|
+
"@openai/codex",
|
|
1026
|
+
"@openai/codex-darwin-arm64",
|
|
1027
|
+
"@openai/codex-darwin-x64",
|
|
1028
|
+
"@openai/codex-linux-arm64",
|
|
1029
|
+
"@openai/codex-linux-x64",
|
|
1030
|
+
"@openai/codex-win32-arm64",
|
|
1031
|
+
"@openai/codex-win32-x64"
|
|
1032
|
+
];
|
|
1033
|
+
const ACP_PACKAGE_MARKERS = [
|
|
1034
|
+
...OWNED_ACP_PACKAGE_NAMES.map((packageName) => `/node_modules/${packageName}/`),
|
|
1035
|
+
...PLUGIN_DEPS_CODEX_PACKAGE_NAMES.map((packageName) => `/node_modules/${packageName}/`),
|
|
1036
|
+
"/acpx/dist/"
|
|
1037
|
+
];
|
|
1038
|
+
function normalizePathLike(value) {
|
|
1039
|
+
return value.replaceAll("\\", "/");
|
|
1040
|
+
}
|
|
1041
|
+
function resolvePackageRoot(packageName) {
|
|
1042
|
+
try {
|
|
1043
|
+
return normalizePathLike(path.dirname(requireFromHere.resolve(`${packageName}/package.json`)));
|
|
1044
|
+
} catch {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function resolveOwnedAcpPackageRootCandidates(packageName) {
|
|
1049
|
+
const pluginRoot = resolveAcpxPluginRoot(import.meta.url);
|
|
1050
|
+
const openClawRoot = resolveOpenClawRoot(pluginRoot);
|
|
1051
|
+
return [
|
|
1052
|
+
resolvePackageRoot(packageName),
|
|
1053
|
+
path.join(pluginRoot, "node_modules", packageName),
|
|
1054
|
+
path.join(openClawRoot, "node_modules", packageName)
|
|
1055
|
+
].flatMap((root) => root ? [normalizePathLike(root)] : []);
|
|
1056
|
+
}
|
|
1057
|
+
const OWNED_ACP_PACKAGE_ROOTS = Array.from(new Set(OWNED_ACP_PACKAGE_NAMES.flatMap(resolveOwnedAcpPackageRootCandidates)));
|
|
1058
|
+
function commandBelongsToResolvedAcpPackage(command) {
|
|
1059
|
+
return OWNED_ACP_PACKAGE_ROOTS.some((root) => command.includes(`${root}/`));
|
|
1060
|
+
}
|
|
1061
|
+
function commandMentionsGeneratedWrapper(command) {
|
|
1062
|
+
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
|
|
1063
|
+
}
|
|
1064
|
+
function commandContainsExactWrapperPath(command, wrapperPath) {
|
|
1065
|
+
const expectedPath = normalizePathLike(wrapperPath);
|
|
1066
|
+
return new RegExp(`(?:^|[\\s"'])${escapeRegExp(expectedPath)}(?=$|[\\s"'])`).test(normalizePathLike(command));
|
|
1067
|
+
}
|
|
1068
|
+
function wrapperPathBelongsToRoot(wrapperPath, wrapperRoot) {
|
|
1069
|
+
const normalizedPath = normalizePathLike(wrapperPath);
|
|
1070
|
+
const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
|
|
1071
|
+
return GENERATED_WRAPPER_BASENAMES.has(path.posix.basename(normalizedPath)) && normalizedPath.startsWith(`${normalizedRoot}/`);
|
|
1072
|
+
}
|
|
1073
|
+
/** Check whether a command references an OpenClaw-generated ACPX wrapper path. */
|
|
1074
|
+
function isOpenClawLeaseAwareAcpxProcessCommand(params) {
|
|
1075
|
+
const command = normalizePathLike(Array.isArray(params.command) ? params.command.join(" ") : params.command ?? "");
|
|
1076
|
+
const root = params.wrapperRoot ? `${normalizePathLike(params.wrapperRoot).replace(/\/+$/, "")}/` : "";
|
|
1077
|
+
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(`${root}${basename}`));
|
|
1078
|
+
}
|
|
1079
|
+
function commandsReferToSameRootCommand(liveCommand, storedCommand) {
|
|
1080
|
+
if (!storedCommand?.trim()) return true;
|
|
1081
|
+
return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
|
|
1082
|
+
}
|
|
1083
|
+
function liveCommandMatchesLeaseIdentity(params) {
|
|
1084
|
+
if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) return true;
|
|
1085
|
+
const identity = readAcpxProcessLeaseIdentity(params.command);
|
|
1086
|
+
return (!params.expectedLeaseId || identity?.leaseId === params.expectedLeaseId) && (!params.expectedGatewayInstanceId || identity?.gatewayInstanceId === params.expectedGatewayInstanceId);
|
|
1087
|
+
}
|
|
1088
|
+
/** Check whether a command is owned by OpenClaw ACPX runtime packages or wrappers. */
|
|
1089
|
+
function isOpenClawOwnedAcpxProcessCommand(params) {
|
|
1090
|
+
const command = params.command?.trim();
|
|
1091
|
+
if (!command) return false;
|
|
1092
|
+
const normalized = normalizePathLike(command);
|
|
1093
|
+
if (isOpenClawLeaseAwareAcpxProcessCommand({
|
|
1094
|
+
command: normalized,
|
|
1095
|
+
wrapperRoot: params.wrapperRoot
|
|
1096
|
+
})) return true;
|
|
1097
|
+
if (commandBelongsToResolvedAcpPackage(normalized)) return true;
|
|
1098
|
+
if (!normalized.includes(OPENCLAW_PLUGIN_DEPS_MARKER)) return false;
|
|
1099
|
+
return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
|
|
1100
|
+
}
|
|
1101
|
+
function parseProcessList(stdout) {
|
|
1102
|
+
const processes = [];
|
|
1103
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
1104
|
+
const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
|
|
1105
|
+
const pid = match?.groups?.pid;
|
|
1106
|
+
const ppid = match?.groups?.ppid;
|
|
1107
|
+
const command = match?.groups?.command;
|
|
1108
|
+
if (!pid || !ppid || !command) continue;
|
|
1109
|
+
processes.push({
|
|
1110
|
+
pid: Number.parseInt(pid, 10),
|
|
1111
|
+
ppid: Number.parseInt(ppid, 10),
|
|
1112
|
+
command
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return processes;
|
|
1116
|
+
}
|
|
1117
|
+
/** List host processes in the compact shape needed by ACPX cleanup. */
|
|
1118
|
+
async function listPlatformProcesses() {
|
|
1119
|
+
if (process.platform === "win32") return [];
|
|
1120
|
+
const { stdout } = await runExec("ps", ["-axo", "pid=,ppid=,command="], {
|
|
1121
|
+
logOutput: false,
|
|
1122
|
+
maxBuffer: 8388608,
|
|
1123
|
+
timeoutMs: ACPX_PROCESS_LIST_TIMEOUT_MS
|
|
1124
|
+
});
|
|
1125
|
+
return parseProcessList(stdout);
|
|
1126
|
+
}
|
|
1127
|
+
function collectProcessTree(processes, rootPid) {
|
|
1128
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1129
|
+
for (const processInfo of processes) {
|
|
1130
|
+
const children = childrenByParent.get(processInfo.ppid) ?? [];
|
|
1131
|
+
children.push(processInfo);
|
|
1132
|
+
childrenByParent.set(processInfo.ppid, children);
|
|
1133
|
+
}
|
|
1134
|
+
const root = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])).get(rootPid);
|
|
1135
|
+
const collected = [];
|
|
1136
|
+
if (root) collected.push(root);
|
|
1137
|
+
const queue = [...childrenByParent.get(rootPid) ?? []];
|
|
1138
|
+
while (queue.length > 0) {
|
|
1139
|
+
const next = queue.shift();
|
|
1140
|
+
if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) continue;
|
|
1141
|
+
collected.push(next);
|
|
1142
|
+
queue.push(...childrenByParent.get(next.pid) ?? []);
|
|
1143
|
+
}
|
|
1144
|
+
return collected;
|
|
1145
|
+
}
|
|
1146
|
+
function uniquePids(processes) {
|
|
1147
|
+
return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
|
|
1148
|
+
}
|
|
1149
|
+
async function terminatePids(pids, deps) {
|
|
1150
|
+
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
|
|
1151
|
+
const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => {
|
|
1152
|
+
setTimeout(resolve, ms);
|
|
1153
|
+
}));
|
|
1154
|
+
const terminated = [];
|
|
1155
|
+
for (const pid of pids) {
|
|
1156
|
+
deps?.assertCurrent?.();
|
|
1157
|
+
try {
|
|
1158
|
+
killProcess(pid, "SIGTERM");
|
|
1159
|
+
terminated.push(pid);
|
|
1160
|
+
} catch {}
|
|
1161
|
+
}
|
|
1162
|
+
if (terminated.length === 0) return terminated;
|
|
1163
|
+
await sleep(750);
|
|
1164
|
+
for (const pid of terminated) {
|
|
1165
|
+
deps?.assertCurrent?.();
|
|
1166
|
+
if (deps?.killProcess || isPidAlive(pid)) try {
|
|
1167
|
+
killProcess(pid, "SIGKILL");
|
|
1168
|
+
} catch {}
|
|
1169
|
+
}
|
|
1170
|
+
return terminated;
|
|
1171
|
+
}
|
|
1172
|
+
/** Terminate one validated OpenClaw-owned ACPX wrapper process tree. */
|
|
1173
|
+
async function cleanupOpenClawOwnedAcpxProcessTree(params) {
|
|
1174
|
+
const rootPid = params.rootPid;
|
|
1175
|
+
if (!rootPid || rootPid <= 0 || rootPid === process.pid) return {
|
|
1176
|
+
inspectedPids: [],
|
|
1177
|
+
terminatedPids: [],
|
|
1178
|
+
skippedReason: "missing-root"
|
|
1179
|
+
};
|
|
1180
|
+
if ((params.deps?.platform ?? process.platform) === "win32") return {
|
|
1181
|
+
inspectedPids: [],
|
|
1182
|
+
terminatedPids: [],
|
|
1183
|
+
skippedReason: "unsupported-platform"
|
|
1184
|
+
};
|
|
1185
|
+
let processes;
|
|
1186
|
+
try {
|
|
1187
|
+
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
|
1188
|
+
} catch {
|
|
1189
|
+
return {
|
|
1190
|
+
inspectedPids: [],
|
|
1191
|
+
terminatedPids: [],
|
|
1192
|
+
skippedReason: "process-list-unavailable"
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
const listedTree = collectProcessTree(processes, rootPid);
|
|
1196
|
+
if (listedTree.length === 0) return {
|
|
1197
|
+
inspectedPids: [],
|
|
1198
|
+
terminatedPids: [],
|
|
1199
|
+
skippedReason: "unverified-root"
|
|
1200
|
+
};
|
|
1201
|
+
const rootCommand = listedTree[0]?.command ?? params.rootCommand;
|
|
1202
|
+
const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(rootCommand ?? ""));
|
|
1203
|
+
const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(params.rootCommand ?? ""));
|
|
1204
|
+
if (!liveCommandWasGeneratedWrapper && (storedCommandWasGeneratedWrapper || !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)) || !isOpenClawOwnedAcpxProcessCommand({
|
|
1205
|
+
command: rootCommand,
|
|
1206
|
+
wrapperRoot: params.wrapperRoot
|
|
1207
|
+
}) || !liveCommandMatchesLeaseIdentity({
|
|
1208
|
+
command: rootCommand,
|
|
1209
|
+
expectedLeaseId: params.expectedLeaseId,
|
|
1210
|
+
expectedGatewayInstanceId: params.expectedGatewayInstanceId
|
|
1211
|
+
})) return {
|
|
1212
|
+
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
|
1213
|
+
terminatedPids: [],
|
|
1214
|
+
skippedReason: "not-openclaw-owned"
|
|
1215
|
+
};
|
|
1216
|
+
const pids = uniquePids(listedTree.toReversed());
|
|
1217
|
+
return {
|
|
1218
|
+
inspectedPids: uniquePids(listedTree),
|
|
1219
|
+
terminatedPids: await terminatePids(pids, params.deps)
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/** Recover a pending lease by matching its exact live wrapper identity. */
|
|
1223
|
+
async function cleanupOpenClawOwnedAcpxPendingLease(params) {
|
|
1224
|
+
if ((params.deps?.platform ?? process.platform) === "win32") return {
|
|
1225
|
+
inspectedPids: [],
|
|
1226
|
+
terminatedPids: [],
|
|
1227
|
+
skippedReason: "unsupported-platform"
|
|
1228
|
+
};
|
|
1229
|
+
if (!params.wrapperPath || !wrapperPathBelongsToRoot(params.wrapperPath, params.wrapperRoot)) return {
|
|
1230
|
+
inspectedPids: [],
|
|
1231
|
+
terminatedPids: [],
|
|
1232
|
+
skippedReason: "unverified-root"
|
|
1233
|
+
};
|
|
1234
|
+
let processes;
|
|
1235
|
+
try {
|
|
1236
|
+
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
|
1237
|
+
} catch {
|
|
1238
|
+
return {
|
|
1239
|
+
inspectedPids: [],
|
|
1240
|
+
terminatedPids: [],
|
|
1241
|
+
skippedReason: "process-list-unavailable"
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
const matchingRoots = processes.filter((processInfo) => commandContainsExactWrapperPath(processInfo.command, params.wrapperPath) && liveCommandMatchesLeaseIdentity({
|
|
1245
|
+
command: processInfo.command,
|
|
1246
|
+
expectedLeaseId: params.leaseId,
|
|
1247
|
+
expectedGatewayInstanceId: params.gatewayInstanceId
|
|
1248
|
+
}));
|
|
1249
|
+
if (matchingRoots.length === 0) return {
|
|
1250
|
+
inspectedPids: [],
|
|
1251
|
+
terminatedPids: [],
|
|
1252
|
+
skippedReason: "missing-root"
|
|
1253
|
+
};
|
|
1254
|
+
if (matchingRoots.length > 1) return {
|
|
1255
|
+
inspectedPids: uniquePids(matchingRoots),
|
|
1256
|
+
terminatedPids: [],
|
|
1257
|
+
skippedReason: "ambiguous-root"
|
|
1258
|
+
};
|
|
1259
|
+
const listedTree = collectProcessTree(processes, matchingRoots[0].pid);
|
|
1260
|
+
const pids = uniquePids(listedTree.toReversed());
|
|
1261
|
+
return {
|
|
1262
|
+
inspectedPids: uniquePids(listedTree),
|
|
1263
|
+
terminatedPids: await terminatePids(pids, params.deps)
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
/** Reap orphaned OpenClaw-owned ACPX wrapper trees during runtime startup. */
|
|
1267
|
+
async function reapStaleOpenClawOwnedAcpxOrphans(params) {
|
|
1268
|
+
if ((params.deps?.platform ?? process.platform) === "win32") return {
|
|
1269
|
+
inspectedPids: [],
|
|
1270
|
+
terminatedPids: [],
|
|
1271
|
+
skippedReason: "unsupported-platform"
|
|
1272
|
+
};
|
|
1273
|
+
let processes;
|
|
1274
|
+
try {
|
|
1275
|
+
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
|
1276
|
+
} catch {
|
|
1277
|
+
return {
|
|
1278
|
+
inspectedPids: [],
|
|
1279
|
+
terminatedPids: [],
|
|
1280
|
+
skippedReason: "process-list-unavailable"
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
const orphanTrees = processes.filter((processInfo) => processInfo.ppid === 1 && !readAcpxProcessLeaseIdentity(processInfo.command) && isOpenClawOwnedAcpxProcessCommand({
|
|
1284
|
+
command: processInfo.command,
|
|
1285
|
+
wrapperRoot: params.wrapperRoot
|
|
1286
|
+
})).map((orphan) => collectProcessTree(processes, orphan.pid));
|
|
1287
|
+
return {
|
|
1288
|
+
inspectedPids: uniquePids(orphanTrees.flat()),
|
|
1289
|
+
terminatedPids: await terminatePids(uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())), params.deps)
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
//#endregion
|
|
1293
|
+
//#region extensions/acpx/src/model-ref.ts
|
|
1294
|
+
function withAcpxSessionOptions(input) {
|
|
1295
|
+
const model = input.model?.trim() || input.sessionOptions?.model;
|
|
1296
|
+
const sessionOptions = model ? {
|
|
1297
|
+
...input.sessionOptions,
|
|
1298
|
+
model
|
|
1299
|
+
} : input.sessionOptions;
|
|
1300
|
+
const { modelExplicit: _modelExplicit, thinkingExplicit: _thinkingExplicit, ...rest } = input;
|
|
1301
|
+
return {
|
|
1302
|
+
...rest,
|
|
1303
|
+
...sessionOptions ? { sessionOptions } : {}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
async function withOpenClawModelRef(requested, apply) {
|
|
1307
|
+
try {
|
|
1308
|
+
return await apply(requested);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
const model = requested.trim();
|
|
1311
|
+
const slash = model.indexOf("/");
|
|
1312
|
+
if (!isRequestedModelUnsupportedError(error) || error.reason !== "unadvertised-model" || error.ambiguous === true || slash <= 0 || slash === model.length - 1) throw error;
|
|
1313
|
+
return await apply(model.slice(slash + 1));
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
async function ensureSessionWithModelRef(ensureSession, input) {
|
|
1317
|
+
const ensure = (model) => ensureSession(withAcpxSessionOptions({
|
|
1318
|
+
...input,
|
|
1319
|
+
model
|
|
1320
|
+
}));
|
|
1321
|
+
const requested = input.model?.trim();
|
|
1322
|
+
try {
|
|
1323
|
+
return requested ? await withOpenClawModelRef(requested, ensure) : await ensureSession(withAcpxSessionOptions(input));
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
if (!requested || input.modelExplicit || !isRequestedModelUnsupportedError(error) || error.reason !== "missing-capability") throw error;
|
|
1326
|
+
return {
|
|
1327
|
+
...await ensure(void 0),
|
|
1328
|
+
appliedModel: { kind: "dropped" }
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
//#endregion
|
|
1333
|
+
//#region extensions/acpx/src/runtime-generations.ts
|
|
1334
|
+
var AcpxGenerationRegistry = class {
|
|
1335
|
+
constructor(sessionStore, delegate, createDelegate) {
|
|
1336
|
+
this.sessionStore = sessionStore;
|
|
1337
|
+
this.delegate = delegate;
|
|
1338
|
+
this.createDelegate = createDelegate;
|
|
1339
|
+
this.generations = /* @__PURE__ */ new Map();
|
|
1340
|
+
this.isolatedSessionResources = /* @__PURE__ */ new Set();
|
|
1341
|
+
this.privateDelegates = /* @__PURE__ */ new Set();
|
|
1342
|
+
this.retiringDelegates = /* @__PURE__ */ new WeakSet();
|
|
1343
|
+
this.nextGenerationId = 0;
|
|
1344
|
+
this.generationOwner = Symbol("acpx-runtime-owner");
|
|
1345
|
+
this.stopping = false;
|
|
1346
|
+
}
|
|
1347
|
+
get isStopping() {
|
|
1348
|
+
return this.stopping;
|
|
1349
|
+
}
|
|
1350
|
+
assertRunning() {
|
|
1351
|
+
if (this.stopping) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
|
|
1352
|
+
}
|
|
1353
|
+
fromCaptured(resource, captured) {
|
|
1354
|
+
return captured?.owner === this.generationOwner ? captured : this.currentGeneration(resource);
|
|
1355
|
+
}
|
|
1356
|
+
prepareFresh(resource) {
|
|
1357
|
+
const generation = this.generations.get(resource);
|
|
1358
|
+
if (generation) this.retireGeneration(generation);
|
|
1359
|
+
else this.sessionStore.markFresh(resource);
|
|
1360
|
+
}
|
|
1361
|
+
resolveDelegate(generation, nativeTools) {
|
|
1362
|
+
this.assertRunning();
|
|
1363
|
+
if (generation.delegate && generation.nativeTools !== nativeTools) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP session tool ownership changed.");
|
|
1364
|
+
if (!generation.delegate) {
|
|
1365
|
+
generation.delegate = generation.afterReset ? this.createDelegate() : this.delegate;
|
|
1366
|
+
generation.nativeTools = nativeTools;
|
|
1367
|
+
if (generation.delegate !== this.delegate) this.privateDelegates.add(generation.delegate);
|
|
1368
|
+
}
|
|
1369
|
+
return generation.delegate;
|
|
1370
|
+
}
|
|
1371
|
+
currentGeneration(resource) {
|
|
1372
|
+
if (this.stopping) throw new AcpRuntimeError("ACP_BACKEND_UNAVAILABLE", "ACP runtime is shut down.");
|
|
1373
|
+
let generation = this.generations.get(resource);
|
|
1374
|
+
if (!generation) {
|
|
1375
|
+
const fresh = this.sessionStore.isFresh(resource);
|
|
1376
|
+
const afterReset = fresh || this.isolatedSessionResources.has(resource);
|
|
1377
|
+
if (afterReset) this.isolatedSessionResources.add(resource);
|
|
1378
|
+
generation = {
|
|
1379
|
+
id: ++this.nextGenerationId,
|
|
1380
|
+
owner: this.generationOwner,
|
|
1381
|
+
resource,
|
|
1382
|
+
ensureQueue: new KeyedAsyncQueue(),
|
|
1383
|
+
retired: false,
|
|
1384
|
+
activeOperations: 0,
|
|
1385
|
+
pendingAdmissions: 0,
|
|
1386
|
+
admissionState: "unadmitted",
|
|
1387
|
+
activeRecordOperations: /* @__PURE__ */ new Map(),
|
|
1388
|
+
closedRecordIds: /* @__PURE__ */ new Set(),
|
|
1389
|
+
records: /* @__PURE__ */ new Map(),
|
|
1390
|
+
closeCompleted: false,
|
|
1391
|
+
afterReset,
|
|
1392
|
+
awaitPriorWrites: fresh
|
|
1393
|
+
};
|
|
1394
|
+
this.generations.set(resource, generation);
|
|
1395
|
+
}
|
|
1396
|
+
return generation;
|
|
1397
|
+
}
|
|
1398
|
+
async runAdmission(resource, run) {
|
|
1399
|
+
const generation = this.currentGeneration(resource);
|
|
1400
|
+
generation.pendingAdmissions += 1;
|
|
1401
|
+
try {
|
|
1402
|
+
return await generation.ensureQueue.enqueue(resource + "\0" + generation.id, async () => {
|
|
1403
|
+
try {
|
|
1404
|
+
const result = await run(generation);
|
|
1405
|
+
generation.admissionState = "admitted";
|
|
1406
|
+
return result;
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
if (generation.admissionState !== "admitted") generation.admissionState = "failed";
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
} finally {
|
|
1413
|
+
generation.pendingAdmissions -= 1;
|
|
1414
|
+
this.releaseIdleGeneration(generation);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
retireGeneration(generation) {
|
|
1418
|
+
generation.retired = true;
|
|
1419
|
+
if (this.generations.get(generation.resource) === generation) {
|
|
1420
|
+
this.generations.delete(generation.resource);
|
|
1421
|
+
this.sessionStore.markFresh(generation.resource);
|
|
1422
|
+
}
|
|
1423
|
+
this.releaseRetiredDelegate(generation);
|
|
1424
|
+
}
|
|
1425
|
+
releaseRetiredDelegate(generation) {
|
|
1426
|
+
const delegate = generation.delegate;
|
|
1427
|
+
if (!generation.retired || generation.activeOperations !== 0 || generation.pendingAdmissions !== 0 || !delegate || delegate === this.delegate || this.retiringDelegates.has(delegate)) return;
|
|
1428
|
+
this.retiringDelegates.add(delegate);
|
|
1429
|
+
delegate.shutdown().then(() => this.privateDelegates.delete(delegate), () => {});
|
|
1430
|
+
}
|
|
1431
|
+
retainGenerationOperation(generation, recordId) {
|
|
1432
|
+
generation.activeOperations += 1;
|
|
1433
|
+
generation.activeRecordOperations.set(recordId, (generation.activeRecordOperations.get(recordId) ?? 0) + 1);
|
|
1434
|
+
return () => {
|
|
1435
|
+
const remaining = (generation.activeRecordOperations.get(recordId) ?? 1) - 1;
|
|
1436
|
+
if (remaining === 0) {
|
|
1437
|
+
generation.activeRecordOperations.delete(recordId);
|
|
1438
|
+
generation.closedRecordIds.delete(recordId);
|
|
1439
|
+
} else generation.activeRecordOperations.set(recordId, remaining);
|
|
1440
|
+
generation.activeOperations -= 1;
|
|
1441
|
+
this.releaseIdleGeneration(generation);
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
releaseIdleGeneration(generation) {
|
|
1445
|
+
if (!generation.retired && (generation.closeCompleted || generation.admissionState === "failed") && generation.pendingAdmissions === 0 && generation.activeOperations === 0 && generation.records.size === 0 && this.generations.get(generation.resource) === generation) {
|
|
1446
|
+
generation.retired = true;
|
|
1447
|
+
this.generations.delete(generation.resource);
|
|
1448
|
+
}
|
|
1449
|
+
this.releaseRetiredDelegate(generation);
|
|
1450
|
+
}
|
|
1451
|
+
assertCurrentGeneration(generation) {
|
|
1452
|
+
if (this.stopping || generation.retired) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP runtime operation was superseded by reset.");
|
|
1453
|
+
}
|
|
1454
|
+
async shutdown() {
|
|
1455
|
+
this.stopping = true;
|
|
1456
|
+
const errors = (await Promise.allSettled([this.delegate, ...this.privateDelegates].map((delegate) => delegate.shutdown()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
1457
|
+
if (errors.length) throw new AggregateError(errors, "ACP runtime shutdown failed.");
|
|
1458
|
+
this.privateDelegates.clear();
|
|
1459
|
+
this.generations.clear();
|
|
1460
|
+
this.isolatedSessionResources.clear();
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
//#endregion
|
|
1464
|
+
//#region extensions/acpx/src/runtime-probe.ts
|
|
1465
|
+
var AcpxRuntimeProbe = class {
|
|
1466
|
+
constructor(params) {
|
|
1467
|
+
this.params = params;
|
|
1468
|
+
this.tail = Promise.resolve();
|
|
1469
|
+
}
|
|
1470
|
+
isHealthy() {
|
|
1471
|
+
return this.health !== void 0 && (this.health.agent !== this.params.getAgent() || this.health.ok);
|
|
1472
|
+
}
|
|
1473
|
+
async doctor() {
|
|
1474
|
+
const probe = this.tail.then(async () => {
|
|
1475
|
+
this.params.assertRunning();
|
|
1476
|
+
const agent = this.params.getAgent();
|
|
1477
|
+
const runtime = this.params.createRuntime(agent);
|
|
1478
|
+
try {
|
|
1479
|
+
const report = await this.params.runWithLease(agent, () => runtime.doctor());
|
|
1480
|
+
this.params.assertRunning();
|
|
1481
|
+
this.health = {
|
|
1482
|
+
agent,
|
|
1483
|
+
ok: report.ok
|
|
1484
|
+
};
|
|
1485
|
+
return report;
|
|
1486
|
+
} finally {
|
|
1487
|
+
await runtime.shutdown();
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
this.tail = probe.catch(() => {});
|
|
1491
|
+
return await probe;
|
|
1492
|
+
}
|
|
1493
|
+
async shutdown() {
|
|
1494
|
+
this.health = void 0;
|
|
1495
|
+
await this.tail;
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region extensions/acpx/src/runtime-session-store.ts
|
|
1500
|
+
/** Generation-bound persistence and process-lease metadata for ACPX resets. */
|
|
1501
|
+
function withOpenClawLeaseSessionMetadata(record, lease) {
|
|
1502
|
+
return {
|
|
1503
|
+
...record,
|
|
1504
|
+
openclawLeaseId: lease.leaseId,
|
|
1505
|
+
openclawGatewayInstanceId: lease.gatewayInstanceId
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function captureGenerationRecord(generation, record) {
|
|
1509
|
+
if (record.closed || generation.closedRecordIds.has(record.acpxRecordId)) generation.records.delete(record.acpxRecordId);
|
|
1510
|
+
else generation.records.set(record.acpxRecordId, record);
|
|
1511
|
+
}
|
|
1512
|
+
const acpxGenerationKey = Symbol("openclaw.acpxGeneration");
|
|
1513
|
+
const acpxOperationScope = new AsyncLocalStorage();
|
|
1514
|
+
function readSessionRecordName(record) {
|
|
1515
|
+
if (typeof record !== "object" || record === null) return "";
|
|
1516
|
+
const { name } = record;
|
|
1517
|
+
return typeof name === "string" ? name.trim() : "";
|
|
1518
|
+
}
|
|
1519
|
+
function readRecordAgentCommand(record) {
|
|
1520
|
+
return record?.agentArgv ?? record?.agentCommand;
|
|
1521
|
+
}
|
|
1522
|
+
function readRecordCwd(record) {
|
|
1523
|
+
if (typeof record !== "object" || record === null) return;
|
|
1524
|
+
const { cwd } = record;
|
|
1525
|
+
return typeof cwd === "string" ? cwd.trim() || void 0 : void 0;
|
|
1526
|
+
}
|
|
1527
|
+
function readRecordResetOnNextEnsure(record) {
|
|
1528
|
+
if (typeof record !== "object" || record === null) return false;
|
|
1529
|
+
const { acpx } = record;
|
|
1530
|
+
if (typeof acpx !== "object" || acpx === null) return false;
|
|
1531
|
+
return acpx.reset_on_next_ensure === true;
|
|
1532
|
+
}
|
|
1533
|
+
function readRecordAgentPid(record) {
|
|
1534
|
+
if (typeof record !== "object" || record === null) return;
|
|
1535
|
+
const { pid, processId } = record;
|
|
1536
|
+
const rawPid = pid ?? processId;
|
|
1537
|
+
const numericPid = typeof rawPid === "number" ? rawPid : typeof rawPid === "string" ? parseStrictPositiveInteger(rawPid) : void 0;
|
|
1538
|
+
return numericPid && Number.isInteger(numericPid) && numericPid > 0 ? numericPid : void 0;
|
|
1539
|
+
}
|
|
1540
|
+
function readOpenClawLeaseIdFromRecord(record) {
|
|
1541
|
+
if (typeof record !== "object" || record === null) return;
|
|
1542
|
+
const { openclawLeaseId } = record;
|
|
1543
|
+
return typeof openclawLeaseId === "string" ? openclawLeaseId.trim() || void 0 : void 0;
|
|
1544
|
+
}
|
|
1545
|
+
function readOpenClawGatewayInstanceIdFromRecord(record) {
|
|
1546
|
+
if (typeof record !== "object" || record === null) return;
|
|
1547
|
+
const { openclawGatewayInstanceId } = record;
|
|
1548
|
+
return typeof openclawGatewayInstanceId === "string" ? openclawGatewayInstanceId.trim() || void 0 : void 0;
|
|
1549
|
+
}
|
|
1550
|
+
function extractGeneratedWrapperPath(command) {
|
|
1551
|
+
return splitCommandParts(command ?? "").find((part) => (part.split(/[\\/]/).pop() ?? "") === "codex-acp-wrapper.mjs" || (part.split(/[\\/]/).pop() ?? "") === "claude-agent-acp-wrapper.mjs") ?? "";
|
|
1552
|
+
}
|
|
1553
|
+
function selectCurrentSessionLease(params) {
|
|
1554
|
+
const sessionKeys = new Set(normalizeStringEntries(params.sessionKeys));
|
|
1555
|
+
const candidates = params.leases.filter((lease) => sessionKeys.has(lease.sessionKey));
|
|
1556
|
+
if (params.rootPid) return candidates.find((lease) => lease.rootPid === params.rootPid);
|
|
1557
|
+
let selected;
|
|
1558
|
+
for (const lease of candidates) if (!selected || lease.startedAt > selected.startedAt) selected = lease;
|
|
1559
|
+
return selected;
|
|
1560
|
+
}
|
|
1561
|
+
function createResetAwareSessionStore(baseStore, params) {
|
|
1562
|
+
const freshSessionKeys = /* @__PURE__ */ new Set();
|
|
1563
|
+
const stateQueue = new KeyedAsyncQueue();
|
|
1564
|
+
const pendingWrites = /* @__PURE__ */ new Map();
|
|
1565
|
+
return {
|
|
1566
|
+
async load(sessionId) {
|
|
1567
|
+
const scope = acpxOperationScope.getStore();
|
|
1568
|
+
if (scope?.closeRecord && (sessionId === scope.generation.resource || sessionId === scope.closeRecord.acpxRecordId)) return scope.closeRecord;
|
|
1569
|
+
const resource = scope?.generation.resource ?? sessionId.trim();
|
|
1570
|
+
const pending = pendingWrites.get(sessionId.trim());
|
|
1571
|
+
if (pending && (scope?.generation.awaitPriorWrites || freshSessionKeys.has(resource))) await Promise.allSettled(pending);
|
|
1572
|
+
const load = async () => {
|
|
1573
|
+
if (scope?.generation.retired) return;
|
|
1574
|
+
const normalized = sessionId.trim();
|
|
1575
|
+
if (normalized && freshSessionKeys.has(normalized)) return;
|
|
1576
|
+
const record = await baseStore.load(sessionId);
|
|
1577
|
+
if (scope?.generation.retired || freshSessionKeys.has(scope?.generation.resource ?? normalized)) return;
|
|
1578
|
+
if (scope && record) captureGenerationRecord(scope.generation, record);
|
|
1579
|
+
if (!record || !params?.leaseStore || !params.gatewayInstanceId) return record;
|
|
1580
|
+
const sessionName = readSessionRecordName(record) || normalized;
|
|
1581
|
+
const lease = selectCurrentSessionLease({
|
|
1582
|
+
leases: await params.leaseStore.listOpen(params.gatewayInstanceId),
|
|
1583
|
+
sessionKeys: [sessionName, normalized],
|
|
1584
|
+
rootPid: readRecordAgentPid(record)
|
|
1585
|
+
});
|
|
1586
|
+
if (!lease) return record;
|
|
1587
|
+
if (scope?.generation.retired) return;
|
|
1588
|
+
const leasedRecord = withOpenClawLeaseSessionMetadata(record, lease);
|
|
1589
|
+
if (scope) captureGenerationRecord(scope.generation, leasedRecord);
|
|
1590
|
+
return leasedRecord;
|
|
1591
|
+
};
|
|
1592
|
+
return await load();
|
|
1593
|
+
},
|
|
1594
|
+
async save(record) {
|
|
1595
|
+
const scope = acpxOperationScope.getStore();
|
|
1596
|
+
if (scope) captureGenerationRecord(scope.generation, record);
|
|
1597
|
+
const resource = record.acpxRecordId;
|
|
1598
|
+
const retiredCloseRecord = scope?.generation.retired && record.closed && record.acpx?.reset_on_next_ensure === true ? scope.closeRecord : void 0;
|
|
1599
|
+
const writeRecord = async () => {
|
|
1600
|
+
if (scope?.generation.retired) {
|
|
1601
|
+
if (!retiredCloseRecord) return;
|
|
1602
|
+
const persisted = await baseStore.load(record.acpxRecordId);
|
|
1603
|
+
if (!persisted || persisted.acpxRecordId !== retiredCloseRecord.acpxRecordId || persisted.acpSessionId !== retiredCloseRecord.acpSessionId || persisted.createdAt !== retiredCloseRecord.createdAt) return;
|
|
1604
|
+
await baseStore.save(record);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
let recordToSave = record;
|
|
1608
|
+
const launch = params?.launchScope?.getStore();
|
|
1609
|
+
const sessionName = readSessionRecordName(record);
|
|
1610
|
+
const agentCommand = readRecordAgentCommand(record);
|
|
1611
|
+
const leasedCommand = launch?.leasedCommand ?? agentCommand;
|
|
1612
|
+
const leaseIdentity = launch ?? readAcpxProcessLeaseIdentity(leasedCommand);
|
|
1613
|
+
if (params?.leaseStore && params.gatewayInstanceId && params.wrapperRoot && (!launch || sessionName === launch.sessionKey) && leasedCommand && leaseIdentity?.gatewayInstanceId === params.gatewayInstanceId && isOpenClawLeaseAwareAcpxProcessCommand({
|
|
1614
|
+
command: leasedCommand,
|
|
1615
|
+
wrapperRoot: params.wrapperRoot
|
|
1616
|
+
})) {
|
|
1617
|
+
const existing = await params.leaseStore.load(leaseIdentity.leaseId);
|
|
1618
|
+
if (scope?.generation.retired) return;
|
|
1619
|
+
if (!existing || existing.gatewayInstanceId === leaseIdentity.gatewayInstanceId && existing.sessionKey === sessionName && existing.wrapperRoot === params.wrapperRoot) {
|
|
1620
|
+
const adoptingLease = Boolean(launch && !isDeepStrictEqual(splitCommandParts(launch.resolvedCommand), splitCommandParts(launch.leasedCommand)));
|
|
1621
|
+
const persistedCommand = launch && !adoptingLease ? launch.resolvedCommand : leasedCommand;
|
|
1622
|
+
recordToSave = withOpenClawLeaseSessionMetadata({
|
|
1623
|
+
...adoptingLease ? {
|
|
1624
|
+
...record,
|
|
1625
|
+
pid: void 0,
|
|
1626
|
+
processId: void 0,
|
|
1627
|
+
agentStartedAt: void 0
|
|
1628
|
+
} : record,
|
|
1629
|
+
agentCommand: renderAgentCommand(persistedCommand),
|
|
1630
|
+
agentArgv: Array.isArray(persistedCommand) ? persistedCommand : void 0
|
|
1631
|
+
}, leaseIdentity);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (scope?.generation.retired) return;
|
|
1635
|
+
await baseStore.save(recordToSave);
|
|
1636
|
+
if (scope && !scope.generation.retired) scope.generation.awaitPriorWrites = false;
|
|
1637
|
+
if (sessionName && !scope?.generation.retired) freshSessionKeys.delete(sessionName);
|
|
1638
|
+
};
|
|
1639
|
+
const writes = pendingWrites.get(resource) ?? /* @__PURE__ */ new Set();
|
|
1640
|
+
const previous = [...writes];
|
|
1641
|
+
const write = scope?.generation.awaitPriorWrites || retiredCloseRecord ? Promise.allSettled(previous).then(() => stateQueue.enqueue(resource, writeRecord)) : writeRecord();
|
|
1642
|
+
writes.add(write);
|
|
1643
|
+
pendingWrites.set(resource, writes);
|
|
1644
|
+
try {
|
|
1645
|
+
await write;
|
|
1646
|
+
} finally {
|
|
1647
|
+
writes.delete(write);
|
|
1648
|
+
if (writes.size === 0 && pendingWrites.get(resource) === writes) pendingWrites.delete(resource);
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
loadForClose: (sessionKey) => baseStore.load(sessionKey),
|
|
1652
|
+
isFresh: (sessionKey) => freshSessionKeys.has(sessionKey),
|
|
1653
|
+
markFresh(sessionKey) {
|
|
1654
|
+
const normalized = sessionKey.trim();
|
|
1655
|
+
if (normalized) freshSessionKeys.add(normalized);
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
//#endregion
|
|
1660
|
+
//#region extensions/acpx/src/runtime-process-cleanup.ts
|
|
1661
|
+
/** Capture OpenClaw wrapper cleanup ownership before a backend close can yield. */
|
|
1662
|
+
async function prepareAcpxProcessCleanup(params) {
|
|
1663
|
+
const { leaseStore, gatewayInstanceId, wrapperRoot, deps } = params;
|
|
1664
|
+
const rootPid = readRecordAgentPid(params.record);
|
|
1665
|
+
const rootCommand = params.command ? renderAgentCommand(params.command) : void 0;
|
|
1666
|
+
const identity = readAcpxProcessLeaseIdentity(params.command);
|
|
1667
|
+
const leaseId = readOpenClawLeaseIdFromRecord(params.record) ?? identity?.leaseId;
|
|
1668
|
+
const expectedGatewayInstanceId = readOpenClawGatewayInstanceIdFromRecord(params.record) ?? identity?.gatewayInstanceId;
|
|
1669
|
+
const sessionKeys = [params.sessionKey, readSessionRecordName(params.record)];
|
|
1670
|
+
const openLeases = rootPid && gatewayInstanceId && leaseStore ? await leaseStore.listOpen(gatewayInstanceId) : [];
|
|
1671
|
+
const selectedLease = rootPid ? selectCurrentSessionLease({
|
|
1672
|
+
leases: openLeases,
|
|
1673
|
+
sessionKeys,
|
|
1674
|
+
rootPid
|
|
1675
|
+
}) : void 0;
|
|
1676
|
+
const loadedLease = leaseId ? await leaseStore?.load(leaseId) : void 0;
|
|
1677
|
+
const ownedLease = selectedLease ?? (loadedLease && loadedLease.gatewayInstanceId === gatewayInstanceId && (!rootPid || loadedLease.rootPid === rootPid) && sessionKeys.includes(loadedLease.sessionKey) ? loadedLease : void 0);
|
|
1678
|
+
const lease = ownedLease ? { ...ownedLease } : void 0;
|
|
1679
|
+
return async () => {
|
|
1680
|
+
if (lease && lease.gatewayInstanceId === gatewayInstanceId) {
|
|
1681
|
+
await leaseStore?.markState(lease.leaseId, "closing");
|
|
1682
|
+
const result = lease.rootPid > 0 ? await cleanupOpenClawOwnedAcpxProcessTree({
|
|
1683
|
+
rootPid: lease.rootPid,
|
|
1684
|
+
rootCommand,
|
|
1685
|
+
expectedLeaseId: lease.leaseId,
|
|
1686
|
+
expectedGatewayInstanceId: lease.gatewayInstanceId,
|
|
1687
|
+
wrapperRoot: lease.wrapperRoot,
|
|
1688
|
+
deps
|
|
1689
|
+
}) : await cleanupOpenClawOwnedAcpxPendingLease({
|
|
1690
|
+
leaseId: lease.leaseId,
|
|
1691
|
+
gatewayInstanceId: lease.gatewayInstanceId,
|
|
1692
|
+
wrapperRoot: lease.wrapperRoot,
|
|
1693
|
+
wrapperPath: lease.wrapperPath,
|
|
1694
|
+
deps
|
|
1695
|
+
});
|
|
1696
|
+
await leaseStore?.markState(lease.leaseId, result.skippedReason === "process-list-unavailable" || result.skippedReason === "unsupported-platform" || lease.rootPid <= 0 && (result.skippedReason === "ambiguous-root" || result.skippedReason === "unverified-root") ? "open" : result.terminatedPids.length > 0 || result.skippedReason === "missing-root" ? "closed" : "lost");
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
if (!rootPid || !rootCommand) return;
|
|
1700
|
+
await cleanupOpenClawOwnedAcpxProcessTree({
|
|
1701
|
+
rootPid,
|
|
1702
|
+
rootCommand,
|
|
1703
|
+
...leaseId ? { expectedLeaseId: leaseId } : {},
|
|
1704
|
+
...expectedGatewayInstanceId ? { expectedGatewayInstanceId } : {},
|
|
1705
|
+
wrapperRoot,
|
|
1706
|
+
deps
|
|
1707
|
+
});
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
//#endregion
|
|
1711
|
+
//#region extensions/acpx/src/session-owner.ts
|
|
1712
|
+
function requireAcpxOwnerMigration(sessionKey) {
|
|
1713
|
+
throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", `ACP session "${sessionKey}" has an unqualified or unverifiable backend locator. Stop the Gateway and run "openclaw doctor --fix" to migrate ownership without losing history, then restart.`, { detailCode: "SESSION_OWNER_MIGRATION_REQUIRED" });
|
|
1714
|
+
}
|
|
1715
|
+
function assertAcpxSessionOwnerLocator(target, legacyBareSessionKeys) {
|
|
1716
|
+
const resource = resolveAcpxSessionResource(target);
|
|
1717
|
+
const qualified = resource === target.sessionKey.trim().toLowerCase();
|
|
1718
|
+
const persisted = target.persistedHandle;
|
|
1719
|
+
if (!qualified && (legacyBareSessionKeys?.has(target.sessionKey.trim().toLowerCase()) || legacyBareSessionKeys?.has(resource) && !persisted)) requireAcpxOwnerMigration(target.sessionKey);
|
|
1720
|
+
if (persisted) {
|
|
1721
|
+
const decoded = decodeAcpxRuntimeHandleState(persisted.runtimeSessionName);
|
|
1722
|
+
if (!qualified && !decoded || decoded && (decoded.name !== resource || persisted.acpxRecordId && decoded.acpxRecordId !== persisted.acpxRecordId)) requireAcpxOwnerMigration(target.sessionKey);
|
|
1723
|
+
}
|
|
1724
|
+
return resource;
|
|
1725
|
+
}
|
|
1726
|
+
/** Preserve physical oneshot record IDs and the upstream-encoded runtime handle. */
|
|
1727
|
+
function toAcpxResourceInput(input) {
|
|
1728
|
+
const sessionKey = assertAcpxSessionOwnerLocator({
|
|
1729
|
+
...input.handle,
|
|
1730
|
+
persistedHandle: input.handle
|
|
1731
|
+
});
|
|
1732
|
+
return {
|
|
1733
|
+
...input,
|
|
1734
|
+
handle: {
|
|
1735
|
+
...input.handle,
|
|
1736
|
+
sessionKey
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
//#endregion
|
|
1741
|
+
//#region extensions/acpx/src/runtime.ts
|
|
1742
|
+
/**
|
|
1743
|
+
* OpenClaw ACPX runtime adapter. It wraps the upstream acpx runtime with
|
|
1744
|
+
* OpenClaw session metadata, lease tracking, model scoping, and cleanup policy.
|
|
1745
|
+
*/
|
|
1746
|
+
const ACPX_PLUGIN_TOOLS_MCP_SERVER_NAME = "openclaw-plugin-tools";
|
|
1747
|
+
const ACPX_OPENCLAW_TOOLS_MCP_SERVER_NAME = "openclaw-tools";
|
|
1748
|
+
const OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV = "OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY";
|
|
1749
|
+
const CODEX_WRAPPER_STDERR_LOG_PREFIX = "codex-acp-wrapper.stderr";
|
|
1750
|
+
function safeDiagnosticFilePart(value) {
|
|
1751
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown";
|
|
1752
|
+
}
|
|
1753
|
+
function codexWrapperStderrLogFileName(leaseId) {
|
|
1754
|
+
return `${CODEX_WRAPPER_STDERR_LOG_PREFIX}.${safeDiagnosticFilePart(leaseId)}.log`;
|
|
1755
|
+
}
|
|
1756
|
+
function compactDiagnosticText(value) {
|
|
1757
|
+
return value.replace(/\s+/g, " ").trim();
|
|
1758
|
+
}
|
|
1759
|
+
function isGenericInternalAcpErrorMessage(message) {
|
|
1760
|
+
return message.trim() === "Internal error";
|
|
1761
|
+
}
|
|
1762
|
+
function isGenericInternalAcpError(error) {
|
|
1763
|
+
return error instanceof Error && isGenericInternalAcpErrorMessage(error.message);
|
|
1764
|
+
}
|
|
1765
|
+
async function readCodexWrapperStderrTail(params) {
|
|
1766
|
+
if (!params.wrapperRoot || !params.leaseId) return "";
|
|
1767
|
+
try {
|
|
1768
|
+
const text = await fs$1.readFile(path.join(params.wrapperRoot, codexWrapperStderrLogFileName(params.leaseId)), "utf8");
|
|
1769
|
+
return compactDiagnosticText(redactSensitiveText(sliceUtf16Safe(text, -6e3)));
|
|
1770
|
+
} catch {
|
|
1771
|
+
return "";
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const CODEX_ACP_AGENT_ID = "codex";
|
|
1775
|
+
const CODEX_ACP_OPENCLAW_PREFIX = "openai/";
|
|
1776
|
+
const CLAUDE_ACP_OPENCLAW_PREFIX = /^(?:anthropic|amazon-bedrock)\//i;
|
|
1777
|
+
const CODEX_ACP_THINKING_ALIASES = /* @__PURE__ */ new Map([
|
|
1778
|
+
["off", void 0],
|
|
1779
|
+
["minimal", "low"],
|
|
1780
|
+
["low", "low"],
|
|
1781
|
+
["medium", "medium"],
|
|
1782
|
+
["high", "high"],
|
|
1783
|
+
["x-high", "xhigh"],
|
|
1784
|
+
["x_high", "xhigh"],
|
|
1785
|
+
["extra-high", "xhigh"],
|
|
1786
|
+
["extra_high", "xhigh"],
|
|
1787
|
+
["extra high", "xhigh"],
|
|
1788
|
+
["xhigh", "xhigh"]
|
|
1789
|
+
]);
|
|
1790
|
+
function readAgentFromSessionKey(sessionKey) {
|
|
1791
|
+
const normalized = sessionKey?.trim();
|
|
1792
|
+
if (!normalized) return;
|
|
1793
|
+
const match = /^agent:(?<agent>[^:]+):/i.exec(normalized);
|
|
1794
|
+
return normalizeAgentName(match?.groups?.agent);
|
|
1795
|
+
}
|
|
1796
|
+
function readAgentFromHandle(handle) {
|
|
1797
|
+
const decoded = decodeAcpxRuntimeHandleState(handle.runtimeSessionName);
|
|
1798
|
+
return normalizeAgentName(decoded?.agent) ?? readAgentFromSessionKey(handle.sessionKey);
|
|
1799
|
+
}
|
|
1800
|
+
function failUnsupportedCodexAcpModel(rawModel) {
|
|
1801
|
+
throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", `Codex ACP model "${rawModel}" is not supported. Use openai/<model> or <model>/<reasoning-effort>.`);
|
|
1802
|
+
}
|
|
1803
|
+
const WIRE_TIMEOUT_CONFIG_KEYS = /* @__PURE__ */ new Set(["timeout", "timeout_seconds"]);
|
|
1804
|
+
function assertSupportedRuntimeSessionMode(mode) {
|
|
1805
|
+
if (mode === "persistent" || mode === "oneshot") return;
|
|
1806
|
+
throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", `Unsupported ACP runtime session mode ${JSON.stringify(mode)}. Expected one of: persistent, oneshot.`);
|
|
1807
|
+
}
|
|
1808
|
+
function failUnsupportedCodexAcpThinking(rawThinking) {
|
|
1809
|
+
throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", `Codex ACP thinking level "${rawThinking}" is not supported. Use off, minimal, low, medium, high, or xhigh.`);
|
|
1810
|
+
}
|
|
1811
|
+
function normalizeCodexAcpReasoningEffort(rawThinking) {
|
|
1812
|
+
const normalized = rawThinking?.trim().toLowerCase();
|
|
1813
|
+
if (!normalized) return;
|
|
1814
|
+
if (!CODEX_ACP_THINKING_ALIASES.has(normalized)) failUnsupportedCodexAcpThinking(rawThinking ?? "");
|
|
1815
|
+
return CODEX_ACP_THINKING_ALIASES.get(normalized);
|
|
1816
|
+
}
|
|
1817
|
+
function isCodexAcpReasoningEffortAlias(value) {
|
|
1818
|
+
const normalized = value?.trim().toLowerCase();
|
|
1819
|
+
return Boolean(normalized && CODEX_ACP_THINKING_ALIASES.has(normalized));
|
|
1820
|
+
}
|
|
1821
|
+
function classifyCodexAcpModelRequest(rawModel, rawThinking) {
|
|
1822
|
+
const raw = rawModel?.trim();
|
|
1823
|
+
const thinkingReasoningEffort = normalizeCodexAcpReasoningEffort(rawThinking);
|
|
1824
|
+
const thinkingOnlyOverride = thinkingReasoningEffort ? { reasoningEffort: thinkingReasoningEffort } : void 0;
|
|
1825
|
+
if (!raw) return {
|
|
1826
|
+
kind: "override",
|
|
1827
|
+
override: thinkingOnlyOverride ?? {}
|
|
1828
|
+
};
|
|
1829
|
+
let value = raw;
|
|
1830
|
+
let hadOpenAiQualifier = false;
|
|
1831
|
+
if (value.toLowerCase().startsWith(CODEX_ACP_OPENCLAW_PREFIX)) {
|
|
1832
|
+
value = value.slice(7);
|
|
1833
|
+
hadOpenAiQualifier = true;
|
|
1834
|
+
}
|
|
1835
|
+
let model = value.trim();
|
|
1836
|
+
let modelReasoningEffort;
|
|
1837
|
+
const slashIndex = value.lastIndexOf("/");
|
|
1838
|
+
if (slashIndex >= 0 && isCodexAcpReasoningEffortAlias(value.slice(slashIndex + 1))) {
|
|
1839
|
+
modelReasoningEffort = normalizeCodexAcpReasoningEffort(value.slice(slashIndex + 1));
|
|
1840
|
+
model = value.slice(0, slashIndex).trim();
|
|
1841
|
+
}
|
|
1842
|
+
if (hadOpenAiQualifier && (!model || model.includes("/"))) failUnsupportedCodexAcpModel(raw);
|
|
1843
|
+
if (!model || model.includes("/")) return thinkingOnlyOverride ? {
|
|
1844
|
+
kind: "unsupported",
|
|
1845
|
+
thinkingOverride: thinkingOnlyOverride
|
|
1846
|
+
} : { kind: "unsupported" };
|
|
1847
|
+
const reasoningEffort = rawThinking?.trim() ? thinkingReasoningEffort : modelReasoningEffort;
|
|
1848
|
+
return {
|
|
1849
|
+
kind: "override",
|
|
1850
|
+
override: {
|
|
1851
|
+
model,
|
|
1852
|
+
...reasoningEffort ? { reasoningEffort } : {}
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
function withCodexSessionModel(input, override) {
|
|
1857
|
+
const next = { ...input };
|
|
1858
|
+
if (override?.model) next.model = override.model;
|
|
1859
|
+
else delete next.model;
|
|
1860
|
+
return next;
|
|
1861
|
+
}
|
|
1862
|
+
function normalizeClaudeAcpModelOverride(rawModel) {
|
|
1863
|
+
const raw = rawModel?.trim();
|
|
1864
|
+
if (!raw) return;
|
|
1865
|
+
const prefix = raw.match(CLAUDE_ACP_OPENCLAW_PREFIX);
|
|
1866
|
+
if (!prefix) return raw;
|
|
1867
|
+
return raw.slice(prefix[0].length).trim() || void 0;
|
|
1868
|
+
}
|
|
1869
|
+
function appendCodexAcpConfigOverrides(command, override) {
|
|
1870
|
+
const config = {
|
|
1871
|
+
...override.model ? { model: override.model } : {},
|
|
1872
|
+
...override.reasoningEffort ? { model_reasoning_effort: override.reasoningEffort } : {}
|
|
1873
|
+
};
|
|
1874
|
+
if (Object.keys(config).length === 0) return command;
|
|
1875
|
+
return [
|
|
1876
|
+
...splitCommandParts(command),
|
|
1877
|
+
OPENCLAW_CODEX_CONFIG_ARG,
|
|
1878
|
+
JSON.stringify(config)
|
|
1879
|
+
];
|
|
1880
|
+
}
|
|
1881
|
+
function withManagedToolsMcpSessionEnv(params) {
|
|
1882
|
+
const sessionKey = params.sessionKey.trim();
|
|
1883
|
+
if (!params.pluginToolsEnabled && !params.openclawToolsEnabled || !sessionKey || !params.mcpServers?.length) return params.mcpServers;
|
|
1884
|
+
let changed = false;
|
|
1885
|
+
const nextServers = params.mcpServers.map((server) => {
|
|
1886
|
+
const isManagedPluginTools = params.pluginToolsEnabled && server.name === ACPX_PLUGIN_TOOLS_MCP_SERVER_NAME;
|
|
1887
|
+
const isManagedOpenClawTools = params.openclawToolsEnabled && server.name === ACPX_OPENCLAW_TOOLS_MCP_SERVER_NAME;
|
|
1888
|
+
if (!isManagedPluginTools && !isManagedOpenClawTools || !("command" in server)) return server;
|
|
1889
|
+
changed = true;
|
|
1890
|
+
const env = [...server.env.filter((entry) => entry.name !== OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV), {
|
|
1891
|
+
name: OPENCLAW_TOOLS_MCP_AGENT_SESSION_KEY_ENV,
|
|
1892
|
+
value: sessionKey
|
|
1893
|
+
}];
|
|
1894
|
+
return {
|
|
1895
|
+
...server,
|
|
1896
|
+
env,
|
|
1897
|
+
args: params.agentId ? [
|
|
1898
|
+
...server.args,
|
|
1899
|
+
"--openclaw-agent-id",
|
|
1900
|
+
params.agentId
|
|
1901
|
+
] : server.args
|
|
1902
|
+
};
|
|
1903
|
+
});
|
|
1904
|
+
return changed ? nextServers : params.mcpServers;
|
|
1905
|
+
}
|
|
1906
|
+
function resolveBridgeSession(handle) {
|
|
1907
|
+
return handle.bridgeSession === void 0 ? handle : handle.bridgeSession;
|
|
1908
|
+
}
|
|
1909
|
+
/** OpenClaw-managed ACP runtime implementation backed by the upstream acpx runtime. */
|
|
1910
|
+
var AcpxRuntime$1 = class {
|
|
1911
|
+
constructor(options, testOptions) {
|
|
1912
|
+
this.ownerAwareSessions = 1;
|
|
1913
|
+
this.launchCommandScope = new AsyncLocalStorage();
|
|
1914
|
+
this.sessionScope = new AsyncLocalStorage();
|
|
1915
|
+
this.launchLeaseScope = new AsyncLocalStorage();
|
|
1916
|
+
this.legacyBareSessionKeys = new Set(options.openclawLegacyBareSessionKeys);
|
|
1917
|
+
const { openclawProcessCleanup, ...delegateTestOptions } = testOptions ?? {};
|
|
1918
|
+
this.processCleanupDeps = openclawProcessCleanup;
|
|
1919
|
+
this.wrapperRoot = options.openclawWrapperRoot;
|
|
1920
|
+
this.gatewayInstanceId = options.openclawGatewayInstanceId;
|
|
1921
|
+
this.processLeaseStore = options.openclawProcessLeaseStore;
|
|
1922
|
+
this.pluginToolsMcpBridgeEnabled = options.pluginToolsMcpBridgeEnabled === true;
|
|
1923
|
+
this.openclawToolsMcpBridgeEnabled = options.openclawToolsMcpBridgeEnabled === true;
|
|
1924
|
+
this.managedToolsMcpBridgeEnabled = this.pluginToolsMcpBridgeEnabled || this.openclawToolsMcpBridgeEnabled;
|
|
1925
|
+
this.cwd = options.cwd;
|
|
1926
|
+
this.sessionStore = createResetAwareSessionStore(options.sessionStore, {
|
|
1927
|
+
gatewayInstanceId: this.gatewayInstanceId,
|
|
1928
|
+
leaseStore: this.processLeaseStore,
|
|
1929
|
+
launchScope: this.launchLeaseScope,
|
|
1930
|
+
wrapperRoot: this.wrapperRoot
|
|
1931
|
+
});
|
|
1932
|
+
this.agentRegistry = options.agentRegistry;
|
|
1933
|
+
this.scopedAgentRegistry = {
|
|
1934
|
+
resolve: (agentName) => {
|
|
1935
|
+
const launch = this.launchCommandScope.getStore();
|
|
1936
|
+
return launch && launch.agent === normalizeAgentName(agentName) && launch.command ? launch.command : this.agentRegistry.resolve(agentName);
|
|
1937
|
+
},
|
|
1938
|
+
list: () => this.agentRegistry.list()
|
|
1939
|
+
};
|
|
1940
|
+
const createDelegate = (probeAgent = options.probeAgent) => new AcpxRuntime({
|
|
1941
|
+
...options,
|
|
1942
|
+
probeAgent,
|
|
1943
|
+
sessionStore: this.sessionStore,
|
|
1944
|
+
agentRegistry: this.scopedAgentRegistry,
|
|
1945
|
+
sessionPermissions: (context) => {
|
|
1946
|
+
const permissions = options.sessionPermissions?.(context);
|
|
1947
|
+
const session = this.sessionScope.getStore();
|
|
1948
|
+
return {
|
|
1949
|
+
...permissions,
|
|
1950
|
+
...session?.native ? { permissionMode: "approve-all" } : {},
|
|
1951
|
+
...session === null || session?.native ? { onPermissionRequest: async () => ({ outcome: "cancel" }) } : {}
|
|
1952
|
+
};
|
|
1953
|
+
},
|
|
1954
|
+
mcpServers: (context) => {
|
|
1955
|
+
const servers = typeof options.mcpServers === "function" ? options.mcpServers(context) : options.mcpServers ?? [];
|
|
1956
|
+
if (isOpenClawBridgeCommand(context.agentArgv ?? context.agentCommand)) return [];
|
|
1957
|
+
const target = this.sessionScope.getStore();
|
|
1958
|
+
if (target === null) return [];
|
|
1959
|
+
if (!this.managedToolsMcpBridgeEnabled) return servers;
|
|
1960
|
+
if (!target) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP tool bridge has no session owner");
|
|
1961
|
+
return withManagedToolsMcpSessionEnv({
|
|
1962
|
+
pluginToolsEnabled: this.pluginToolsMcpBridgeEnabled,
|
|
1963
|
+
openclawToolsEnabled: this.openclawToolsMcpBridgeEnabled,
|
|
1964
|
+
mcpServers: servers,
|
|
1965
|
+
...target
|
|
1966
|
+
});
|
|
1967
|
+
},
|
|
1968
|
+
processLifecycle: {
|
|
1969
|
+
onBeforeSpawn: async (launch) => {
|
|
1970
|
+
await options.processLifecycle?.onBeforeSpawn?.(launch);
|
|
1971
|
+
await this.recordProcessLaunch(launch);
|
|
1972
|
+
},
|
|
1973
|
+
onSpawned: async (process) => {
|
|
1974
|
+
await this.recordProcessLaunch(process);
|
|
1975
|
+
await options.processLifecycle?.onSpawned?.(process);
|
|
1976
|
+
},
|
|
1977
|
+
onSpawnFailed: options.processLifecycle?.onSpawnFailed,
|
|
1978
|
+
onExit: options.processLifecycle?.onExit
|
|
1979
|
+
}
|
|
1980
|
+
}, delegateTestOptions);
|
|
1981
|
+
this.delegate = createDelegate();
|
|
1982
|
+
this.generationRegistry = new AcpxGenerationRegistry(this.sessionStore, this.delegate, createDelegate);
|
|
1983
|
+
this.probe = new AcpxRuntimeProbe({
|
|
1984
|
+
getAgent: () => normalizeAgentName(options.getProbeAgent?.() ?? options.probeAgent) ?? "codex",
|
|
1985
|
+
createRuntime: createDelegate,
|
|
1986
|
+
assertRunning: () => this.generationRegistry.assertRunning(),
|
|
1987
|
+
runWithLease: (agent, run) => this.runWithLaunchLease({
|
|
1988
|
+
agent,
|
|
1989
|
+
sessionKey: ACPX_PROBE_LEASE_SESSION_KEY,
|
|
1990
|
+
command: resolveAgentCommand({
|
|
1991
|
+
agentName: agent,
|
|
1992
|
+
agentRegistry: this.agentRegistry
|
|
1993
|
+
}),
|
|
1994
|
+
finalizeCompletedProbe: true,
|
|
1995
|
+
run
|
|
1996
|
+
})
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
async runInGeneration(target, scope, run) {
|
|
2000
|
+
const release = this.generationRegistry.retainGenerationOperation(scope.generation, scope.recordId ?? target.acpxRecordId ?? scope.generation.resource);
|
|
2001
|
+
try {
|
|
2002
|
+
return await this.sessionScope.run(resolveBridgeSession(target), () => acpxOperationScope.run(scope, run));
|
|
2003
|
+
} finally {
|
|
2004
|
+
release();
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
generationForHandle(handle) {
|
|
2008
|
+
const resource = assertAcpxSessionOwnerLocator({
|
|
2009
|
+
...handle,
|
|
2010
|
+
persistedHandle: handle
|
|
2011
|
+
}, this.legacyBareSessionKeys);
|
|
2012
|
+
const capturedGeneration = handle[acpxGenerationKey];
|
|
2013
|
+
return this.generationRegistry.fromCaptured(resource, capturedGeneration);
|
|
2014
|
+
}
|
|
2015
|
+
async loadOperationSnapshotForHandle(handle, generation, allowRetired = false) {
|
|
2016
|
+
const resource = generation.resource;
|
|
2017
|
+
if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
|
|
2018
|
+
const ownedRecord = generation.records.get(handle.acpxRecordId ?? resource);
|
|
2019
|
+
if (ownedRecord && (handle.acpxRecordId && ownedRecord.acpxRecordId !== handle.acpxRecordId || handle.backendSessionId && ownedRecord.acpSessionId && ownedRecord.acpSessionId !== handle.backendSessionId)) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP handle no longer owns this runtime generation.");
|
|
2020
|
+
let record = allowRetired ? generation.retired ? ownedRecord : await this.sessionStore.loadForClose(handle.acpxRecordId ?? resource) : await acpxOperationScope.run({ generation }, () => this.sessionStore.load(handle.acpxRecordId ?? resource));
|
|
2021
|
+
if (allowRetired && generation.retired && ownedRecord) record = ownedRecord;
|
|
2022
|
+
if (allowRetired && record) captureGenerationRecord(generation, record);
|
|
2023
|
+
if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
|
|
2024
|
+
if (record && (handle.acpxRecordId && handle.acpxRecordId !== record.acpxRecordId || handle.backendSessionId && record.acpSessionId && handle.backendSessionId !== record.acpSessionId)) throw new AcpRuntimeError("ACP_TURN_FAILED", "ACP handle no longer owns this runtime record.");
|
|
2025
|
+
const command = readRecordAgentCommand(record) ?? resolveAgentCommand({
|
|
2026
|
+
agentName: readAgentFromHandle(handle),
|
|
2027
|
+
agentRegistry: this.agentRegistry
|
|
2028
|
+
});
|
|
2029
|
+
const identity = readAcpxProcessLeaseIdentity(command);
|
|
2030
|
+
if (identity && this.processLeaseStore && this.gatewayInstanceId && this.wrapperRoot) {
|
|
2031
|
+
const lease = await this.processLeaseStore.load(identity.leaseId);
|
|
2032
|
+
if (identity.gatewayInstanceId !== this.gatewayInstanceId) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another gateway`);
|
|
2033
|
+
if (lease && (lease.gatewayInstanceId !== identity.gatewayInstanceId || lease.sessionKey !== resolveAcpxSessionResource(handle) || lease.wrapperRoot !== this.wrapperRoot)) throw new AcpRuntimeError("ACP_TURN_FAILED", `ACPX process lease ${identity.leaseId} belongs to another session`);
|
|
2034
|
+
}
|
|
2035
|
+
if (!allowRetired) this.generationRegistry.assertCurrentGeneration(generation);
|
|
2036
|
+
return {
|
|
2037
|
+
record,
|
|
2038
|
+
command,
|
|
2039
|
+
generation
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
async runWithOperationSnapshot(handle, run) {
|
|
2043
|
+
const generation = this.generationForHandle(handle);
|
|
2044
|
+
return await this.runInGeneration(handle, { generation }, async () => {
|
|
2045
|
+
const snapshot = await this.loadOperationSnapshotForHandle(handle, generation);
|
|
2046
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2047
|
+
return await this.runInGeneration(handle, {
|
|
2048
|
+
generation,
|
|
2049
|
+
recordId: snapshot.record?.acpxRecordId
|
|
2050
|
+
}, () => run(snapshot));
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
resolveDelegateForOperationSnapshot(handle, snapshot) {
|
|
2054
|
+
return this.generationRegistry.resolveDelegate(snapshot.generation, snapshot.generation.nativeTools ?? resolveBridgeSession(handle)?.native === true);
|
|
2055
|
+
}
|
|
2056
|
+
async readReusablePersistentSessionCommand(params) {
|
|
2057
|
+
if (params.mode !== "persistent" || !params.command) return;
|
|
2058
|
+
const existing = await this.sessionStore.load(params.sessionKey);
|
|
2059
|
+
if (!existing || readRecordResetOnNextEnsure(existing)) return;
|
|
2060
|
+
const recordCwd = readRecordCwd(existing);
|
|
2061
|
+
if (!recordCwd || resolve(recordCwd) !== resolve(params.cwd?.trim() || this.cwd)) return;
|
|
2062
|
+
const recordCommand = readRecordAgentCommand(existing);
|
|
2063
|
+
if (!recordCommand) return;
|
|
2064
|
+
const leaseIdentity = readAcpxProcessLeaseIdentity(recordCommand);
|
|
2065
|
+
if (leaseIdentity && leaseIdentity.gatewayInstanceId !== this.gatewayInstanceId) return;
|
|
2066
|
+
const stableRecordCommand = leaseIdentity ? withAcpxLeaseArgs({
|
|
2067
|
+
command: params.command,
|
|
2068
|
+
leaseId: leaseIdentity.leaseId,
|
|
2069
|
+
gatewayInstanceId: leaseIdentity.gatewayInstanceId
|
|
2070
|
+
}) : params.command;
|
|
2071
|
+
if (!isDeepStrictEqual(splitCommandParts(recordCommand), splitCommandParts(stableRecordCommand))) return;
|
|
2072
|
+
return !params.resumeSessionId || existing.acpSessionId === params.resumeSessionId ? recordCommand : void 0;
|
|
2073
|
+
}
|
|
2074
|
+
async runWithLaunchLease(params) {
|
|
2075
|
+
if (!params.command || !this.wrapperRoot || !this.gatewayInstanceId || !this.processLeaseStore || !isOpenClawLeaseAwareAcpxProcessCommand({
|
|
2076
|
+
command: params.command,
|
|
2077
|
+
wrapperRoot: this.wrapperRoot
|
|
2078
|
+
})) return await this.launchCommandScope.run({
|
|
2079
|
+
agent: normalizeAgentName(params.agent) ?? params.agent,
|
|
2080
|
+
command: params.reusableCommand ?? params.command
|
|
2081
|
+
}, params.run);
|
|
2082
|
+
const reusableIdentity = readAcpxProcessLeaseIdentity(params.reusableCommand);
|
|
2083
|
+
const leaseId = reusableIdentity?.gatewayInstanceId === this.gatewayInstanceId ? reusableIdentity.leaseId : params.finalizeCompletedProbe ? `probe-${hashAcpxProcessCommand(`${this.gatewayInstanceId}\0${extractGeneratedWrapperPath(params.command)}`)}` : randomUUID();
|
|
2084
|
+
const leasedCommand = withAcpxLeaseArgs({
|
|
2085
|
+
command: params.command,
|
|
2086
|
+
leaseId,
|
|
2087
|
+
gatewayInstanceId: this.gatewayInstanceId
|
|
2088
|
+
});
|
|
2089
|
+
const launch = {
|
|
2090
|
+
leaseId,
|
|
2091
|
+
gatewayInstanceId: this.gatewayInstanceId,
|
|
2092
|
+
sessionKey: params.sessionKey,
|
|
2093
|
+
wrapperRoot: this.wrapperRoot,
|
|
2094
|
+
resolvedCommand: params.reusableCommand ?? leasedCommand,
|
|
2095
|
+
leasedCommand
|
|
2096
|
+
};
|
|
2097
|
+
const result = await this.launchLeaseScope.run(launch, () => this.launchCommandScope.run({
|
|
2098
|
+
agent: normalizeAgentName(params.agent) ?? params.agent,
|
|
2099
|
+
command: launch.resolvedCommand
|
|
2100
|
+
}, params.run));
|
|
2101
|
+
if (params.finalizeCompletedProbe) await cleanupOpenClawOwnedAcpxPendingLease({
|
|
2102
|
+
leaseId,
|
|
2103
|
+
gatewayInstanceId: launch.gatewayInstanceId,
|
|
2104
|
+
wrapperRoot: launch.wrapperRoot,
|
|
2105
|
+
wrapperPath: extractGeneratedWrapperPath(leasedCommand),
|
|
2106
|
+
deps: this.processCleanupDeps
|
|
2107
|
+
});
|
|
2108
|
+
return result;
|
|
2109
|
+
}
|
|
2110
|
+
async recordProcessLaunch(process) {
|
|
2111
|
+
const command = [process.command, ...process.args];
|
|
2112
|
+
const identity = readAcpxProcessLeaseIdentity(command);
|
|
2113
|
+
if (!identity || !this.processLeaseStore || !this.wrapperRoot) return;
|
|
2114
|
+
const sessionKey = process.scope.kind === "runtime-session" ? process.scope.sessionKey : ACPX_PROBE_LEASE_SESSION_KEY;
|
|
2115
|
+
const existing = await this.processLeaseStore.load(identity.leaseId);
|
|
2116
|
+
if (identity.gatewayInstanceId !== this.gatewayInstanceId || existing && (existing.gatewayInstanceId !== identity.gatewayInstanceId || existing.sessionKey !== sessionKey || existing.wrapperRoot !== this.wrapperRoot)) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP process lease belongs to another owner");
|
|
2117
|
+
if (!isOpenClawLeaseAwareAcpxProcessCommand({
|
|
2118
|
+
command,
|
|
2119
|
+
wrapperRoot: this.wrapperRoot
|
|
2120
|
+
})) throw new AcpRuntimeError("ACP_SESSION_INIT_FAILED", "ACP process lease has no owned wrapper");
|
|
2121
|
+
await this.processLeaseStore.save({
|
|
2122
|
+
...identity,
|
|
2123
|
+
sessionKey,
|
|
2124
|
+
wrapperRoot: this.wrapperRoot,
|
|
2125
|
+
wrapperPath: extractGeneratedWrapperPath(command),
|
|
2126
|
+
rootPid: "pid" in process ? process.pid : 0,
|
|
2127
|
+
commandHash: hashAcpxProcessCommand(command),
|
|
2128
|
+
startedAt: "startedAt" in process ? Date.parse(process.startedAt) : Date.now(),
|
|
2129
|
+
state: "open"
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
async withCodexWrapperDiagnostics(params) {
|
|
2133
|
+
try {
|
|
2134
|
+
return await params.run();
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
if (!isCodexAcpCommand(params.command) || !isGenericInternalAcpError(error)) throw error;
|
|
2137
|
+
const stderrTail = params.handle ? await this.readCodexTurnFailureStderr({ handle: params.handle }) : await readCodexWrapperStderrTail({
|
|
2138
|
+
wrapperRoot: this.wrapperRoot,
|
|
2139
|
+
leaseId: this.launchLeaseScope.getStore()?.leaseId
|
|
2140
|
+
});
|
|
2141
|
+
if (!stderrTail) throw error;
|
|
2142
|
+
throw new AcpRuntimeError(params.fallbackCode, `Internal error: ${stderrTail}`, { cause: error });
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
async readCodexTurnFailureStderr(params) {
|
|
2146
|
+
const record = await this.sessionStore.load(params.handle.acpxRecordId ?? resolveAcpxSessionResource(params.handle));
|
|
2147
|
+
return readCodexWrapperStderrTail({
|
|
2148
|
+
wrapperRoot: this.wrapperRoot,
|
|
2149
|
+
leaseId: readOpenClawLeaseIdFromRecord(record)
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
async findSession(input) {
|
|
2153
|
+
const resource = assertAcpxSessionOwnerLocator(input, this.legacyBareSessionKeys);
|
|
2154
|
+
const generation = this.generationRegistry.currentGeneration(resource);
|
|
2155
|
+
return this.runInGeneration(input, { generation }, async () => {
|
|
2156
|
+
const handle = await (generation.delegate ?? this.delegate).findSession({
|
|
2157
|
+
sessionKey: resource,
|
|
2158
|
+
agent: input.agent
|
|
2159
|
+
});
|
|
2160
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2161
|
+
return handle ? {
|
|
2162
|
+
...handle,
|
|
2163
|
+
sessionKey: input.sessionKey,
|
|
2164
|
+
agentId: input.agentId,
|
|
2165
|
+
[acpxGenerationKey]: generation
|
|
2166
|
+
} : void 0;
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
async shutdown() {
|
|
2170
|
+
const [sessions] = await Promise.allSettled([this.generationRegistry.shutdown(), this.probe.shutdown()]);
|
|
2171
|
+
if (sessions.status === "rejected") throw sessions.reason;
|
|
2172
|
+
}
|
|
2173
|
+
isHealthy() {
|
|
2174
|
+
return this.probe.isHealthy();
|
|
2175
|
+
}
|
|
2176
|
+
async doctor() {
|
|
2177
|
+
return await this.probe.doctor();
|
|
2178
|
+
}
|
|
2179
|
+
async ensureSession(input) {
|
|
2180
|
+
const resource = assertAcpxSessionOwnerLocator(input, this.legacyBareSessionKeys);
|
|
2181
|
+
return await this.generationRegistry.runAdmission(resource, (generation) => this.runInGeneration(input, { generation }, async () => {
|
|
2182
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2183
|
+
const handle = {
|
|
2184
|
+
...await this.ensureSessionUnlocked(input, generation),
|
|
2185
|
+
[acpxGenerationKey]: generation
|
|
2186
|
+
};
|
|
2187
|
+
if (generation.retired && !this.generationRegistry.isStopping) await this.close({
|
|
2188
|
+
handle,
|
|
2189
|
+
reason: "superseded-initialization",
|
|
2190
|
+
discardPersistentState: true
|
|
2191
|
+
});
|
|
2192
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2193
|
+
return handle;
|
|
2194
|
+
}));
|
|
2195
|
+
}
|
|
2196
|
+
async ensureSessionUnlocked(logicalInput, generation) {
|
|
2197
|
+
assertSupportedRuntimeSessionMode(logicalInput.mode);
|
|
2198
|
+
const command = logicalInput.agentCommand ?? resolveAgentCommand({
|
|
2199
|
+
agentName: logicalInput.agent,
|
|
2200
|
+
agentRegistry: this.agentRegistry
|
|
2201
|
+
});
|
|
2202
|
+
const delegate = this.generationRegistry.resolveDelegate(generation, resolveBridgeSession(logicalInput)?.native === true);
|
|
2203
|
+
const logicalTarget = {
|
|
2204
|
+
sessionKey: logicalInput.sessionKey,
|
|
2205
|
+
agentId: logicalInput.agentId,
|
|
2206
|
+
bridgeSession: logicalInput.bridgeSession
|
|
2207
|
+
};
|
|
2208
|
+
const input = {
|
|
2209
|
+
...logicalInput,
|
|
2210
|
+
sessionKey: resolveAcpxSessionResource(logicalInput)
|
|
2211
|
+
};
|
|
2212
|
+
const isCodexAcp = normalizeAgentName(input.agent) === CODEX_ACP_AGENT_ID && isCodexAcpCommand(command);
|
|
2213
|
+
const dropInheritedCodexMax = isCodexAcp && input.thinking === "max" && input.thinkingExplicit === false;
|
|
2214
|
+
const effectiveInput = dropInheritedCodexMax ? { ...input } : input;
|
|
2215
|
+
if (dropInheritedCodexMax) delete effectiveInput.thinking;
|
|
2216
|
+
const claudeModelOverride = isClaudeAcpCommand(command) ? normalizeClaudeAcpModelOverride(input.model) : void 0;
|
|
2217
|
+
const codexClassification = isCodexAcp ? classifyCodexAcpModelRequest(effectiveInput.model, effectiveInput.thinking) : void 0;
|
|
2218
|
+
if (codexClassification?.kind === "unsupported" && input.modelExplicit) failUnsupportedCodexAcpModel(input.model ?? "");
|
|
2219
|
+
const classifiedCodexOverride = codexClassification?.kind === "override" ? codexClassification.override : codexClassification?.thinkingOverride;
|
|
2220
|
+
const codexModelOverride = classifiedCodexOverride && Object.keys(classifiedCodexOverride).length > 0 ? classifiedCodexOverride : void 0;
|
|
2221
|
+
const requestedModel = effectiveInput.model?.trim();
|
|
2222
|
+
const appliedModel = isCodexAcp && requestedModel ? codexModelOverride?.model ? {
|
|
2223
|
+
kind: "applied",
|
|
2224
|
+
model: requestedModel
|
|
2225
|
+
} : { kind: "dropped" } : void 0;
|
|
2226
|
+
const ensureInput = isCodexAcp ? withCodexSessionModel(effectiveInput, codexModelOverride) : claudeModelOverride ? {
|
|
2227
|
+
...effectiveInput,
|
|
2228
|
+
model: claudeModelOverride
|
|
2229
|
+
} : effectiveInput;
|
|
2230
|
+
const stableLaunchCommand = codexModelOverride && command ? appendCodexAcpConfigOverrides(command, codexModelOverride) : command;
|
|
2231
|
+
const reusableCommand = await this.readReusablePersistentSessionCommand({
|
|
2232
|
+
sessionKey: input.sessionKey,
|
|
2233
|
+
mode: input.mode,
|
|
2234
|
+
cwd: input.cwd,
|
|
2235
|
+
command: stableLaunchCommand,
|
|
2236
|
+
resumeSessionId: input.resumeSessionId
|
|
2237
|
+
});
|
|
2238
|
+
return {
|
|
2239
|
+
...await this.runWithLaunchLease({
|
|
2240
|
+
agent: ensureInput.agent,
|
|
2241
|
+
sessionKey: ensureInput.sessionKey,
|
|
2242
|
+
command: stableLaunchCommand,
|
|
2243
|
+
reusableCommand,
|
|
2244
|
+
run: () => this.withCodexWrapperDiagnostics({
|
|
2245
|
+
command: stableLaunchCommand,
|
|
2246
|
+
fallbackCode: "ACP_SESSION_INIT_FAILED",
|
|
2247
|
+
run: () => codexModelOverride ? delegate.ensureSession(withAcpxSessionOptions(ensureInput)) : ensureSessionWithModelRef((request) => {
|
|
2248
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2249
|
+
return delegate.ensureSession(request);
|
|
2250
|
+
}, ensureInput)
|
|
2251
|
+
})
|
|
2252
|
+
}),
|
|
2253
|
+
...logicalTarget,
|
|
2254
|
+
...appliedModel ? { appliedModel } : {},
|
|
2255
|
+
...dropInheritedCodexMax ? { appliedThinking: { kind: "dropped" } } : {}
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
async *runTurn(input) {
|
|
2259
|
+
const turn = this.startTurn(input);
|
|
2260
|
+
turn.result.catch(() => {});
|
|
2261
|
+
let completed = false;
|
|
2262
|
+
try {
|
|
2263
|
+
yield* turn.events;
|
|
2264
|
+
const result = await turn.result;
|
|
2265
|
+
completed = true;
|
|
2266
|
+
yield result.status === "failed" ? {
|
|
2267
|
+
type: "error",
|
|
2268
|
+
...result.error
|
|
2269
|
+
} : {
|
|
2270
|
+
type: "done",
|
|
2271
|
+
...result.stopReason ? { stopReason: result.stopReason } : {}
|
|
2272
|
+
};
|
|
2273
|
+
} finally {
|
|
2274
|
+
if (!completed) {
|
|
2275
|
+
await turn.cancel({ reason: "stream-closed" }).catch(() => {});
|
|
2276
|
+
await turn.closeStream({ reason: "stream-closed" }).catch(() => {});
|
|
2277
|
+
await turn.result.catch(() => {});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
startTurn(input) {
|
|
2282
|
+
const withTurnDiagnostics = (command, run) => this.withCodexWrapperDiagnostics({
|
|
2283
|
+
command,
|
|
2284
|
+
handle: input.handle,
|
|
2285
|
+
fallbackCode: "ACP_TURN_FAILED",
|
|
2286
|
+
run
|
|
2287
|
+
});
|
|
2288
|
+
const turnPromise = this.runWithOperationSnapshot(input.handle, (snapshot) => {
|
|
2289
|
+
const { command, generation } = snapshot;
|
|
2290
|
+
this.generationRegistry.assertCurrentGeneration(generation);
|
|
2291
|
+
const delegate = this.resolveDelegateForOperationSnapshot(input.handle, snapshot);
|
|
2292
|
+
return this.sessionScope.run(resolveBridgeSession(input.handle), () => acpxOperationScope.run({ generation }, () => withTurnDiagnostics(command, async () => {
|
|
2293
|
+
const release = this.generationRegistry.retainGenerationOperation(generation, snapshot.record?.acpxRecordId ?? input.handle.acpxRecordId ?? generation.resource);
|
|
2294
|
+
try {
|
|
2295
|
+
const turn = delegate.startTurn({
|
|
2296
|
+
...toAcpxResourceInput(input),
|
|
2297
|
+
timeoutMs: 0
|
|
2298
|
+
});
|
|
2299
|
+
turn.result.then(release, release);
|
|
2300
|
+
return {
|
|
2301
|
+
command,
|
|
2302
|
+
turn
|
|
2303
|
+
};
|
|
2304
|
+
} catch (error) {
|
|
2305
|
+
release();
|
|
2306
|
+
throw error;
|
|
2307
|
+
}
|
|
2308
|
+
})));
|
|
2309
|
+
});
|
|
2310
|
+
return {
|
|
2311
|
+
requestId: input.requestId,
|
|
2312
|
+
get promptStarted() {
|
|
2313
|
+
return turnPromise.then(({ turn }) => turn.promptStarted);
|
|
2314
|
+
},
|
|
2315
|
+
events: { async *[Symbol.asyncIterator]() {
|
|
2316
|
+
const { command, turn } = await turnPromise;
|
|
2317
|
+
try {
|
|
2318
|
+
yield* turn.events;
|
|
2319
|
+
} catch (error) {
|
|
2320
|
+
if (!isGenericInternalAcpError(error)) throw error;
|
|
2321
|
+
await withTurnDiagnostics(command, () => Promise.reject(error));
|
|
2322
|
+
}
|
|
2323
|
+
} },
|
|
2324
|
+
result: turnPromise.then(({ command, turn }) => withTurnDiagnostics(command, async () => {
|
|
2325
|
+
const result = await turn.result;
|
|
2326
|
+
if (result.status !== "failed" || !isCodexAcpCommand(command) || !isGenericInternalAcpErrorMessage(result.error.message)) return result;
|
|
2327
|
+
const stderrTail = await this.readCodexTurnFailureStderr({ handle: input.handle });
|
|
2328
|
+
if (!stderrTail) return result;
|
|
2329
|
+
return {
|
|
2330
|
+
status: "failed",
|
|
2331
|
+
error: {
|
|
2332
|
+
...result.error,
|
|
2333
|
+
code: "ACP_TURN_FAILED",
|
|
2334
|
+
message: `Internal error: ${stderrTail}`
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
})),
|
|
2338
|
+
cancel(inputArgs) {
|
|
2339
|
+
return turnPromise.then(({ turn }) => turn.cancel(inputArgs));
|
|
2340
|
+
},
|
|
2341
|
+
closeStream(inputArgs) {
|
|
2342
|
+
return turnPromise.then(({ turn }) => turn.closeStream(inputArgs));
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
async getCapabilities(input) {
|
|
2347
|
+
const capabilities = await this.delegate.getCapabilities(input?.handle ? toAcpxResourceInput({ handle: input.handle }) : input);
|
|
2348
|
+
return {
|
|
2349
|
+
...capabilities,
|
|
2350
|
+
controls: capabilities.controls.filter((control) => control !== "session/set_model")
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
async getStatus(input) {
|
|
2354
|
+
return this.runWithOperationSnapshot(input.handle, (snapshot) => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).getStatus(toAcpxResourceInput(input)));
|
|
2355
|
+
}
|
|
2356
|
+
async setModel(input) {
|
|
2357
|
+
await this.runWithOperationSnapshot(input.handle, (snapshot) => {
|
|
2358
|
+
input.signal?.throwIfAborted();
|
|
2359
|
+
input.assertActive?.();
|
|
2360
|
+
return this.resolveDelegateForOperationSnapshot(input.handle, snapshot).setModel(toAcpxResourceInput(input));
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
async setMode(input) {
|
|
2364
|
+
await this.runWithOperationSnapshot(input.handle, (snapshot) => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).setMode(toAcpxResourceInput(input)));
|
|
2365
|
+
}
|
|
2366
|
+
async setConfigOption(input) {
|
|
2367
|
+
return await this.runWithOperationSnapshot(input.handle, (snapshot) => this.setConfigOptionUnlocked(input, snapshot));
|
|
2368
|
+
}
|
|
2369
|
+
async setConfigOptionUnlocked(logicalInput, snapshot) {
|
|
2370
|
+
const { command } = snapshot;
|
|
2371
|
+
const delegate = this.resolveDelegateForOperationSnapshot(logicalInput.handle, snapshot);
|
|
2372
|
+
const input = toAcpxResourceInput(logicalInput);
|
|
2373
|
+
const key = input.key.trim().toLowerCase();
|
|
2374
|
+
const isCodexAcp = isCodexAcpCommand(command);
|
|
2375
|
+
if (WIRE_TIMEOUT_CONFIG_KEYS.has(key) && (isCodexAcp || isClaudeAcpCommand(command))) return;
|
|
2376
|
+
if (isCodexAcp) {
|
|
2377
|
+
if (key === "model") {
|
|
2378
|
+
const classification = classifyCodexAcpModelRequest(input.value);
|
|
2379
|
+
if (classification.kind === "unsupported") failUnsupportedCodexAcpModel(input.value);
|
|
2380
|
+
const { override } = classification;
|
|
2381
|
+
const modelResult = override.model ? await delegate.setConfigOption({
|
|
2382
|
+
...input,
|
|
2383
|
+
key: "model",
|
|
2384
|
+
value: override.model
|
|
2385
|
+
}) : void 0;
|
|
2386
|
+
this.generationRegistry.assertCurrentGeneration(snapshot.generation);
|
|
2387
|
+
if (override.reasoningEffort) return await delegate.setConfigOption({
|
|
2388
|
+
...input,
|
|
2389
|
+
key: "reasoning_effort",
|
|
2390
|
+
value: override.reasoningEffort
|
|
2391
|
+
});
|
|
2392
|
+
return modelResult;
|
|
2393
|
+
}
|
|
2394
|
+
if (key === "thinking" || key === "thought_level" || key === "reasoning_effort") {
|
|
2395
|
+
const classification = classifyCodexAcpModelRequest(void 0, input.value);
|
|
2396
|
+
const reasoningEffort = classification.kind === "override" ? classification.override.reasoningEffort : void 0;
|
|
2397
|
+
if (!reasoningEffort) throw new AcpRuntimeError("ACP_BACKEND_UNSUPPORTED_CONTROL", "Clearing Codex reasoning effort on an existing session is unsupported. Choose a supported explicit effort; the current effort is unchanged.");
|
|
2398
|
+
return await delegate.setConfigOption({
|
|
2399
|
+
...input,
|
|
2400
|
+
key: "reasoning_effort",
|
|
2401
|
+
value: reasoningEffort
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (isClaudeAcpCommand(command) && key === "model") return await delegate.setConfigOption({
|
|
2406
|
+
...input,
|
|
2407
|
+
value: normalizeClaudeAcpModelOverride(input.value) ?? input.value
|
|
2408
|
+
});
|
|
2409
|
+
if (key === "model") return await withOpenClawModelRef(input.value, (value) => {
|
|
2410
|
+
this.generationRegistry.assertCurrentGeneration(snapshot.generation);
|
|
2411
|
+
return delegate.setConfigOption({
|
|
2412
|
+
...input,
|
|
2413
|
+
value
|
|
2414
|
+
});
|
|
2415
|
+
});
|
|
2416
|
+
return await delegate.setConfigOption(input);
|
|
2417
|
+
}
|
|
2418
|
+
async cancel(input) {
|
|
2419
|
+
await this.runWithOperationSnapshot(input.handle, (snapshot) => this.resolveDelegateForOperationSnapshot(input.handle, snapshot).cancel(toAcpxResourceInput(input)));
|
|
2420
|
+
}
|
|
2421
|
+
async prepareFreshSession(input) {
|
|
2422
|
+
if ("handle" in input) {
|
|
2423
|
+
await this.closeSession({
|
|
2424
|
+
handle: input.handle,
|
|
2425
|
+
reason: "prepare-fresh"
|
|
2426
|
+
}, "prepare-fresh");
|
|
2427
|
+
return;
|
|
2428
|
+
}
|
|
2429
|
+
const resource = assertAcpxSessionOwnerLocator(input, this.legacyBareSessionKeys);
|
|
2430
|
+
this.generationRegistry.prepareFresh(resource);
|
|
2431
|
+
this.legacyBareSessionKeys.delete(resource);
|
|
2432
|
+
}
|
|
2433
|
+
async close(input) {
|
|
2434
|
+
await this.closeSession(input, "close");
|
|
2435
|
+
}
|
|
2436
|
+
async closeSession(input, intent) {
|
|
2437
|
+
const generation = this.generationForHandle(input.handle);
|
|
2438
|
+
await this.runInGeneration(input.handle, { generation }, async () => {
|
|
2439
|
+
const snapshot = await this.loadOperationSnapshotForHandle(input.handle, generation, true);
|
|
2440
|
+
const delegate = this.resolveDelegateForOperationSnapshot(input.handle, snapshot);
|
|
2441
|
+
await acpxOperationScope.run({
|
|
2442
|
+
generation,
|
|
2443
|
+
closeRecord: snapshot.record
|
|
2444
|
+
}, async () => {
|
|
2445
|
+
if ((intent === "prepare-fresh" || input.discardPersistentState) && decodeAcpxRuntimeHandleState(input.handle.runtimeSessionName)?.mode !== "oneshot") {
|
|
2446
|
+
this.generationRegistry.retireGeneration(generation);
|
|
2447
|
+
this.legacyBareSessionKeys.delete(generation.resource);
|
|
2448
|
+
}
|
|
2449
|
+
const cleanup = await prepareAcpxProcessCleanup({
|
|
2450
|
+
record: snapshot.record,
|
|
2451
|
+
command: snapshot.command,
|
|
2452
|
+
sessionKey: resolveAcpxSessionResource(input.handle),
|
|
2453
|
+
gatewayInstanceId: this.gatewayInstanceId,
|
|
2454
|
+
wrapperRoot: this.wrapperRoot,
|
|
2455
|
+
leaseStore: this.processLeaseStore,
|
|
2456
|
+
deps: this.processCleanupDeps
|
|
2457
|
+
}).catch((error) => async () => {
|
|
2458
|
+
throw error;
|
|
2459
|
+
});
|
|
2460
|
+
try {
|
|
2461
|
+
if (intent === "prepare-fresh") await delegate.prepareFreshSession(toAcpxResourceInput(input));
|
|
2462
|
+
else await delegate.close(toAcpxResourceInput(input));
|
|
2463
|
+
} finally {
|
|
2464
|
+
await cleanup();
|
|
2465
|
+
}
|
|
2466
|
+
const recordId = snapshot.record?.acpxRecordId ?? input.handle.acpxRecordId ?? generation.resource;
|
|
2467
|
+
const currentRecord = generation.records.get(recordId);
|
|
2468
|
+
if (!currentRecord || currentRecord.acpSessionId === snapshot.record?.acpSessionId && currentRecord.createdAt === snapshot.record?.createdAt) {
|
|
2469
|
+
generation.records.delete(recordId);
|
|
2470
|
+
if (generation.activeRecordOperations.has(recordId)) generation.closedRecordIds.add(recordId);
|
|
2471
|
+
}
|
|
2472
|
+
generation.closeCompleted = true;
|
|
2473
|
+
});
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
//#endregion
|
|
2478
|
+
//#region extensions/acpx/src/service.ts
|
|
2479
|
+
/**
|
|
2480
|
+
* ACPX plugin service lifecycle. It resolves config, prepares isolated adapter
|
|
2481
|
+
* wrappers, registers the ACP backend, and manages startup/cleanup probes.
|
|
2482
|
+
*/
|
|
2483
|
+
const ENABLE_STARTUP_PROBE_ENV = "OPENCLAW_ACPX_RUNTIME_STARTUP_PROBE";
|
|
2484
|
+
const SKIP_RUNTIME_PROBE_ENV = "OPENCLAW_SKIP_ACPX_RUNTIME_PROBE";
|
|
2485
|
+
const MAX_ACPX_TOKIO_WORKER_THREADS = 8;
|
|
2486
|
+
function resolveAcpxTimerTimeoutMs(timeoutSeconds) {
|
|
2487
|
+
if (timeoutSeconds === void 0) return;
|
|
2488
|
+
return finiteSecondsToTimerSafeMilliseconds(timeoutSeconds) ?? 1;
|
|
2489
|
+
}
|
|
2490
|
+
function resolveAgentProcessEnv() {
|
|
2491
|
+
if (process.env.TOKIO_WORKER_THREADS?.trim()) return;
|
|
2492
|
+
return { TOKIO_WORKER_THREADS: String(Math.min(availableParallelism(), MAX_ACPX_TOKIO_WORKER_THREADS)) };
|
|
2493
|
+
}
|
|
2494
|
+
async function createDefaultRuntime(params) {
|
|
2495
|
+
const names = await fs$1.readdir(path.join(params.pluginConfig.stateDir, "sessions")).catch((error) => {
|
|
2496
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return [];
|
|
2497
|
+
throw error;
|
|
2498
|
+
});
|
|
2499
|
+
const legacyBareSessionKeys = /* @__PURE__ */ new Set();
|
|
2500
|
+
for (const name of names) {
|
|
2501
|
+
if (!name.endsWith(".json")) continue;
|
|
2502
|
+
const recordId = decodeURIComponent(name.slice(0, -5));
|
|
2503
|
+
if (!recordId.startsWith("agent:") && !recordId.startsWith(".openclaw-owner-") && !recordId.includes(":oneshot:")) legacyBareSessionKeys.add(recordId.toLowerCase());
|
|
2504
|
+
}
|
|
2505
|
+
return new AcpxRuntime$1({
|
|
2506
|
+
cwd: params.pluginConfig.cwd,
|
|
2507
|
+
agentProcessEnv: resolveAgentProcessEnv(),
|
|
2508
|
+
openclawLegacyBareSessionKeys: legacyBareSessionKeys,
|
|
2509
|
+
openclawGatewayInstanceId: params.gatewayInstanceId,
|
|
2510
|
+
openclawProcessLeaseStore: params.processLeaseStore,
|
|
2511
|
+
openclawWrapperRoot: params.wrapperRoot,
|
|
2512
|
+
sessionStore: createFileSessionStore({ stateDir: params.pluginConfig.stateDir }),
|
|
2513
|
+
agentRegistry: createAgentRegistry({ overrides: params.pluginConfig.agents }),
|
|
2514
|
+
getProbeAgent: params.getProbeAgent,
|
|
2515
|
+
mcpServers: toAcpMcpServers(params.pluginConfig.mcpServers),
|
|
2516
|
+
pluginToolsMcpBridgeEnabled: params.pluginConfig.pluginToolsMcpBridge,
|
|
2517
|
+
openclawToolsMcpBridgeEnabled: params.pluginConfig.openClawToolsMcpBridge,
|
|
2518
|
+
permissionMode: params.pluginConfig.permissionMode,
|
|
2519
|
+
nonInteractivePermissions: params.pluginConfig.nonInteractivePermissions,
|
|
2520
|
+
elicitationModes: ["form", "url"],
|
|
2521
|
+
timeoutMs: resolveAcpxTimerTimeoutMs(params.pluginConfig.timeoutSeconds)
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
function formatDoctorFailureMessage(report) {
|
|
2525
|
+
const detailText = report.details?.map((detail) => detail.trim()).filter(Boolean).join("; ");
|
|
2526
|
+
return detailText ? `${report.message} (${detailText})` : report.message;
|
|
2527
|
+
}
|
|
2528
|
+
async function measureAcpxStartup(ctx, name, run) {
|
|
2529
|
+
return ctx.startupTrace ? await ctx.startupTrace.measure(name, run) : await run();
|
|
2530
|
+
}
|
|
2531
|
+
function shouldProbeRuntimeAtStartup(env = process.env) {
|
|
2532
|
+
return env[ENABLE_STARTUP_PROBE_ENV] !== "0" && env[SKIP_RUNTIME_PROBE_ENV] !== "1";
|
|
2533
|
+
}
|
|
2534
|
+
async function withStartupProbeTimeout(params) {
|
|
2535
|
+
let timeout;
|
|
2536
|
+
const timeoutMs = resolveAcpxTimerTimeoutMs(params.timeoutSeconds) ?? 1;
|
|
2537
|
+
try {
|
|
2538
|
+
return await Promise.race([params.promise, new Promise((_, reject) => {
|
|
2539
|
+
timeout = setTimeout(() => {
|
|
2540
|
+
reject(/* @__PURE__ */ new Error(`embedded acpx runtime backend startup probe timed out after ${params.timeoutSeconds}s`));
|
|
2541
|
+
}, timeoutMs);
|
|
2542
|
+
timeout.unref?.();
|
|
2543
|
+
})]);
|
|
2544
|
+
} finally {
|
|
2545
|
+
if (timeout) clearTimeout(timeout);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function openGatewayInstanceStateStore(openKeyedStore) {
|
|
2549
|
+
return openKeyedStore({
|
|
2550
|
+
namespace: ACPX_GATEWAY_INSTANCE_NAMESPACE,
|
|
2551
|
+
maxEntries: 1
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
async function resolveGatewayInstanceId(openKeyedStore) {
|
|
2555
|
+
const store = openGatewayInstanceStateStore(openKeyedStore);
|
|
2556
|
+
const existing = normalizeAcpxGatewayInstanceRecord(await store.lookup(ACPX_GATEWAY_INSTANCE_KEY));
|
|
2557
|
+
if (existing) return existing.instanceId;
|
|
2558
|
+
const next = randomUUID();
|
|
2559
|
+
await store.register(ACPX_GATEWAY_INSTANCE_KEY, {
|
|
2560
|
+
instanceId: next,
|
|
2561
|
+
createdAt: Date.now()
|
|
2562
|
+
});
|
|
2563
|
+
return next;
|
|
2564
|
+
}
|
|
2565
|
+
async function reapOpenAcpxProcessLeases(params) {
|
|
2566
|
+
const assertCurrent = () => {
|
|
2567
|
+
params.assertCurrent?.();
|
|
2568
|
+
params.deps?.assertCurrent?.();
|
|
2569
|
+
};
|
|
2570
|
+
const deps = {
|
|
2571
|
+
...params.deps,
|
|
2572
|
+
assertCurrent
|
|
2573
|
+
};
|
|
2574
|
+
const leases = await params.leaseStore.listOpen(params.gatewayInstanceId);
|
|
2575
|
+
const inspectedPids = [];
|
|
2576
|
+
const terminatedPids = [];
|
|
2577
|
+
const legacyWrapperRoots = /* @__PURE__ */ new Set();
|
|
2578
|
+
for (const lease of leases) {
|
|
2579
|
+
if (lease.rootPid <= 0) {
|
|
2580
|
+
legacyWrapperRoots.add(lease.wrapperRoot);
|
|
2581
|
+
assertCurrent();
|
|
2582
|
+
await params.leaseStore.markState(lease.leaseId, "closing");
|
|
2583
|
+
assertCurrent();
|
|
2584
|
+
const result = await cleanupOpenClawOwnedAcpxPendingLease({
|
|
2585
|
+
leaseId: lease.leaseId,
|
|
2586
|
+
gatewayInstanceId: lease.gatewayInstanceId,
|
|
2587
|
+
wrapperRoot: lease.wrapperRoot,
|
|
2588
|
+
wrapperPath: lease.wrapperPath,
|
|
2589
|
+
deps
|
|
2590
|
+
});
|
|
2591
|
+
inspectedPids.push(...result.inspectedPids);
|
|
2592
|
+
terminatedPids.push(...result.terminatedPids);
|
|
2593
|
+
const retryableEvidenceFailure = result.skippedReason === "ambiguous-root" || result.skippedReason === "process-list-unavailable" || result.skippedReason === "unsupported-platform" || result.skippedReason === "unverified-root" || lease.sessionKey === "openclaw:acpx:probe" && result.skippedReason === "missing-root";
|
|
2594
|
+
assertCurrent();
|
|
2595
|
+
await params.leaseStore.markState(lease.leaseId, retryableEvidenceFailure ? "open" : result.terminatedPids.length > 0 ? "closed" : "lost");
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
assertCurrent();
|
|
2599
|
+
await params.leaseStore.markState(lease.leaseId, "closing");
|
|
2600
|
+
assertCurrent();
|
|
2601
|
+
const result = await cleanupOpenClawOwnedAcpxProcessTree({
|
|
2602
|
+
rootPid: lease.rootPid,
|
|
2603
|
+
expectedLeaseId: lease.leaseId,
|
|
2604
|
+
expectedGatewayInstanceId: lease.gatewayInstanceId,
|
|
2605
|
+
wrapperRoot: lease.wrapperRoot,
|
|
2606
|
+
deps
|
|
2607
|
+
});
|
|
2608
|
+
inspectedPids.push(...result.inspectedPids);
|
|
2609
|
+
terminatedPids.push(...result.terminatedPids);
|
|
2610
|
+
assertCurrent();
|
|
2611
|
+
await params.leaseStore.markState(lease.leaseId, result.skippedReason === "process-list-unavailable" || result.skippedReason === "unsupported-platform" ? "open" : result.terminatedPids.length > 0 ? "closed" : "lost");
|
|
2612
|
+
}
|
|
2613
|
+
for (const wrapperRoot of legacyWrapperRoots) {
|
|
2614
|
+
assertCurrent();
|
|
2615
|
+
const legacyResult = await reapStaleOpenClawOwnedAcpxOrphans({
|
|
2616
|
+
wrapperRoot,
|
|
2617
|
+
deps
|
|
2618
|
+
});
|
|
2619
|
+
inspectedPids.push(...legacyResult.inspectedPids);
|
|
2620
|
+
terminatedPids.push(...legacyResult.terminatedPids);
|
|
2621
|
+
}
|
|
2622
|
+
return {
|
|
2623
|
+
inspectedPids,
|
|
2624
|
+
terminatedPids
|
|
2625
|
+
};
|
|
2626
|
+
}
|
|
2627
|
+
/** Create the ACPX plugin service that owns runtime registration and cleanup. */
|
|
2628
|
+
function createAcpxRuntimeService(params) {
|
|
2629
|
+
let runtime = null;
|
|
2630
|
+
let recoverProcesses;
|
|
2631
|
+
let recoveryPromise;
|
|
2632
|
+
let lifecycleRevision = 0;
|
|
2633
|
+
const promote = async (ctx, assertOwner = params.assertCurrent) => {
|
|
2634
|
+
const recover = recoverProcesses;
|
|
2635
|
+
if (!recover) throw new Error("ACPX runtime service is not initialized");
|
|
2636
|
+
const revision = lifecycleRevision;
|
|
2637
|
+
const assertCurrent = () => {
|
|
2638
|
+
if (revision !== lifecycleRevision || recoverProcesses !== recover) throw new Error("ACPX runtime service stopped during recovery");
|
|
2639
|
+
assertOwner?.();
|
|
2640
|
+
};
|
|
2641
|
+
assertCurrent();
|
|
2642
|
+
recoveryPromise ??= measureAcpxStartup(ctx, "process-leases.reap", async () => {
|
|
2643
|
+
const result = await recover(assertCurrent);
|
|
2644
|
+
assertCurrent();
|
|
2645
|
+
if (result.terminatedPids.length > 0) ctx.logger.info(`reaped ${result.terminatedPids.length} stale OpenClaw-owned ACPX processes`);
|
|
2646
|
+
});
|
|
2647
|
+
await recoveryPromise;
|
|
2648
|
+
assertCurrent();
|
|
2649
|
+
};
|
|
2650
|
+
return {
|
|
2651
|
+
id: "acpx-runtime",
|
|
2652
|
+
promote,
|
|
2653
|
+
async start(ctx) {
|
|
2654
|
+
if (process.env.OPENCLAW_SKIP_ACPX_RUNTIME === "1") {
|
|
2655
|
+
ctx.logger.info("skipping embedded acpx runtime backend (OPENCLAW_SKIP_ACPX_RUNTIME=1)");
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const openKeyedStore = params.openKeyedStore;
|
|
2659
|
+
if (!openKeyedStore) throw new Error("ACPX runtime service requires plugin keyed state");
|
|
2660
|
+
const basePluginConfig = await measureAcpxStartup(ctx, "config.resolve", () => resolveAcpxPluginConfig({
|
|
2661
|
+
rawConfig: params.pluginConfig,
|
|
2662
|
+
workspaceDir: ctx.workspaceDir
|
|
2663
|
+
}));
|
|
2664
|
+
const pluginConfig = await measureAcpxStartup(ctx, "config.prepare-codex-auth", () => prepareAcpxCodexAuthConfig({
|
|
2665
|
+
pluginConfig: basePluginConfig,
|
|
2666
|
+
stateDir: ctx.stateDir,
|
|
2667
|
+
logger: ctx.logger
|
|
2668
|
+
}));
|
|
2669
|
+
const wrapperRoot = path.join(ctx.stateDir, "acpx");
|
|
2670
|
+
await measureAcpxStartup(ctx, "filesystem.prepare", async () => {
|
|
2671
|
+
await fs$1.mkdir(pluginConfig.stateDir, { recursive: true });
|
|
2672
|
+
await fs$1.mkdir(wrapperRoot, { recursive: true });
|
|
2673
|
+
});
|
|
2674
|
+
const gatewayInstanceId = await measureAcpxStartup(ctx, "gateway-instance-id", () => resolveGatewayInstanceId(openKeyedStore));
|
|
2675
|
+
const processLeaseStore = createAcpxProcessLeaseStore({ store: openAcpxProcessLeaseStateStore(openKeyedStore) });
|
|
2676
|
+
recoverProcesses = (assertCurrent) => reapOpenAcpxProcessLeases({
|
|
2677
|
+
gatewayInstanceId,
|
|
2678
|
+
leaseStore: processLeaseStore,
|
|
2679
|
+
deps: params.processCleanupDeps,
|
|
2680
|
+
assertCurrent
|
|
2681
|
+
});
|
|
2682
|
+
if (params.startupPurpose !== "inspection") await promote(ctx);
|
|
2683
|
+
const getAllowedAgents = params.getAllowedAgents ?? (() => ctx.config.acp?.allowedAgents);
|
|
2684
|
+
const getProbeAgent = () => pluginConfig.probeAgent ?? getAllowedAgents()?.map(normalizeLowercaseStringOrEmpty).find(Boolean);
|
|
2685
|
+
const startedRuntime = await measureAcpxStartup(ctx, "runtime.create", () => (params.runtimeFactory ?? createDefaultRuntime)({
|
|
2686
|
+
pluginConfig,
|
|
2687
|
+
getProbeAgent,
|
|
2688
|
+
gatewayInstanceId,
|
|
2689
|
+
processLeaseStore,
|
|
2690
|
+
wrapperRoot,
|
|
2691
|
+
logger: ctx.logger
|
|
2692
|
+
}));
|
|
2693
|
+
runtime = startedRuntime;
|
|
2694
|
+
const shouldProbeRuntime = params.probeAtStartup !== false && shouldProbeRuntimeAtStartup();
|
|
2695
|
+
ctx.startupTrace?.detail?.("probe-policy", [["startupProbeEnabledCount", shouldProbeRuntime ? 1 : 0], ["probeAgent", getProbeAgent() ?? "default"]]);
|
|
2696
|
+
await measureAcpxStartup(ctx, "backend.register", () => {
|
|
2697
|
+
const backend = {
|
|
2698
|
+
runtime: startedRuntime,
|
|
2699
|
+
...shouldProbeRuntime ? { healthy: () => runtime?.isHealthy() ?? false } : {}
|
|
2700
|
+
};
|
|
2701
|
+
params.backendLifecycle.publish(backend);
|
|
2702
|
+
ctx.logger.info(`embedded acpx runtime backend registered (cwd: ${pluginConfig.cwd})`);
|
|
2703
|
+
});
|
|
2704
|
+
if (!shouldProbeRuntime) return;
|
|
2705
|
+
lifecycleRevision += 1;
|
|
2706
|
+
const currentRevision = lifecycleRevision;
|
|
2707
|
+
try {
|
|
2708
|
+
const doctorReport = await measureAcpxStartup(ctx, "probe.availability", () => withStartupProbeTimeout({
|
|
2709
|
+
promise: startedRuntime.doctor(),
|
|
2710
|
+
timeoutSeconds: pluginConfig.timeoutSeconds ?? 120
|
|
2711
|
+
}));
|
|
2712
|
+
if (currentRevision !== lifecycleRevision) return;
|
|
2713
|
+
if (doctorReport.ok) {
|
|
2714
|
+
ctx.startupTrace?.detail?.("probe.result", [["healthyCount", 1]]);
|
|
2715
|
+
ctx.logger.info("embedded acpx runtime backend ready");
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
ctx.startupTrace?.detail?.("probe.result", [["healthyCount", 0]]);
|
|
2719
|
+
ctx.logger.warn(`embedded acpx runtime backend probe failed: ${formatDoctorFailureMessage(doctorReport)}`);
|
|
2720
|
+
} catch (err) {
|
|
2721
|
+
if (currentRevision !== lifecycleRevision) return;
|
|
2722
|
+
ctx.startupTrace?.detail?.("probe.result", [["healthyCount", 0]]);
|
|
2723
|
+
ctx.logger.warn(`embedded acpx runtime setup failed: ${formatErrorMessage(err)}`);
|
|
2724
|
+
}
|
|
2725
|
+
},
|
|
2726
|
+
async stop(_ctx) {
|
|
2727
|
+
lifecycleRevision += 1;
|
|
2728
|
+
if (runtime) {
|
|
2729
|
+
params.backendLifecycle.retract(runtime);
|
|
2730
|
+
const [shutdown] = await Promise.allSettled([runtime.shutdown(), recoveryPromise]);
|
|
2731
|
+
if (shutdown.status === "rejected") throw shutdown.reason;
|
|
2732
|
+
} else await recoveryPromise?.catch(() => void 0);
|
|
2733
|
+
runtime = null;
|
|
2734
|
+
recoverProcesses = void 0;
|
|
2735
|
+
recoveryPromise = void 0;
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
//#endregion
|
|
2740
|
+
export { createAcpxRuntimeService };
|