@kodelyth/acpx 2026.5.39 → 2026.5.42
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/AGENTS.md +54 -0
- package/CLAUDE.md +54 -0
- package/dist/index.js +14 -0
- package/dist/process-reaper-DdVqzAA_.js +370 -0
- package/dist/register.runtime.js +53 -0
- package/dist/runtime-D9qhNKmy.js +741 -0
- package/dist/runtime-api.js +4 -0
- package/dist/service-CXeUME_-.js +1483 -0
- package/dist/setup-api.js +16 -0
- package/index.test.ts +119 -0
- package/index.ts +19 -0
- package/klaw.plugin.json +12 -27
- package/package.json +2 -2
- package/register.runtime.test.ts +104 -0
- package/register.runtime.ts +86 -0
- package/runtime-api.ts +49 -0
- package/setup-api.ts +18 -0
- package/src/acpx-runtime-compat.d.ts +65 -0
- package/src/claude-agent-acp-completion.test.ts +187 -0
- package/src/codex-auth-bridge.test.ts +688 -0
- package/src/codex-auth-bridge.ts +780 -0
- package/src/codex-trust-config.ts +297 -0
- package/src/config-schema.ts +118 -0
- package/src/config.test.ts +285 -0
- package/src/config.ts +281 -0
- package/src/manifest.test.ts +21 -0
- package/src/process-lease.test.ts +89 -0
- package/src/process-lease.ts +179 -0
- package/src/process-reaper.test.ts +330 -0
- package/src/process-reaper.ts +434 -0
- package/src/runtime-internals/error-format.mjs +6 -0
- package/src/runtime-internals/mcp-command-line.mjs +123 -0
- package/src/runtime-internals/mcp-command-line.test.ts +59 -0
- package/src/runtime-internals/mcp-proxy.mjs +121 -0
- package/src/runtime-internals/mcp-proxy.test.ts +130 -0
- package/src/runtime.test.ts +1817 -0
- package/src/runtime.ts +1261 -0
- package/src/service.test.ts +802 -0
- package/src/service.ts +630 -0
- package/tsconfig.json +16 -0
- package/index.js +0 -7
- package/register.runtime.js +0 -7
- package/runtime-api.js +0 -7
- package/setup-api.js +0 -7
- /package/{error-format.mjs → dist/error-format.mjs} +0 -0
- /package/{mcp-command-line.mjs → dist/mcp-command-line.mjs} +0 -0
- /package/{mcp-proxy.mjs → dist/mcp-proxy.mjs} +0 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# ACPX Extension Notes
|
|
2
|
+
|
|
3
|
+
This file applies to work under `extensions/acpx/`.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The ACPX extension is a thin Klaw wrapper around the published `acpx` package. Keep reusable ACP runtime logic in `klaw/acpx`, not in this extension.
|
|
8
|
+
|
|
9
|
+
## Default Version Policy
|
|
10
|
+
|
|
11
|
+
- `extensions/acpx/package.json` should point at a published npm release by default.
|
|
12
|
+
- Do not leave the extension pinned to a temporary GitHub commit or local checkout once the ACPX release exists.
|
|
13
|
+
- Do not leave temporary pnpm build-script allowlist exceptions behind after switching back to a published ACPX package.
|
|
14
|
+
|
|
15
|
+
## Unreleased ACPX Development Flow
|
|
16
|
+
|
|
17
|
+
Use this flow when Klaw needs unreleased ACPX changes before the ACPX version is published.
|
|
18
|
+
|
|
19
|
+
1. Make the ACPX code change in the `klaw/acpx` repo first.
|
|
20
|
+
2. In Klaw, temporarily point `extensions/acpx/package.json` at the ACPX GitHub commit you need.
|
|
21
|
+
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx: true` to `allowBuilds` in `pnpm-workspace.yaml`.
|
|
22
|
+
4. Refresh the root workspace lock:
|
|
23
|
+
- `pnpm install --lockfile-only --filter ./extensions/acpx`
|
|
24
|
+
5. Refresh the extension-local npm lock for install metadata:
|
|
25
|
+
- `cd extensions/acpx && npm install --package-lock-only --ignore-scripts`
|
|
26
|
+
6. Rebuild Klaw and restart the gateway before doing live ACP validation.
|
|
27
|
+
7. Once ACPX is released, switch `extensions/acpx/package.json` back to the published npm version and refresh the same lockfiles again.
|
|
28
|
+
8. Remove any temporary `acpx` build-script allowlist entry that was only needed for the GitHub-sourced development pin.
|
|
29
|
+
|
|
30
|
+
## Lockfile Notes
|
|
31
|
+
|
|
32
|
+
- `pnpm-lock.yaml` is the tracked workspace lockfile and must match the ACPX version referenced by `extensions/acpx/package.json`.
|
|
33
|
+
- `extensions/acpx/package-lock.json` is useful local install metadata for the plugin package.
|
|
34
|
+
- If `extensions/acpx/package-lock.json` is gitignored in this repo state, regenerating it is still useful for local verification, but it will not appear in `git status`.
|
|
35
|
+
|
|
36
|
+
## Local Runtime Validation
|
|
37
|
+
|
|
38
|
+
When ACPX integration changes here, prefer this sequence:
|
|
39
|
+
|
|
40
|
+
1. `pnpm install --filter ./extensions/acpx`
|
|
41
|
+
2. `pnpm test:extension acpx`
|
|
42
|
+
3. `pnpm build`
|
|
43
|
+
4. Restart the local gateway if ACP runtime behavior or bundled plugin wiring changed.
|
|
44
|
+
5. If the change affects direct ACP behavior in chat, run a real ACP smoke after restart.
|
|
45
|
+
|
|
46
|
+
## Direct ACPX Binary Policy
|
|
47
|
+
|
|
48
|
+
- Prefer the plugin-local ACPX binary under `extensions/acpx/node_modules/.bin/acpx`.
|
|
49
|
+
- Do not rely on a globally installed `acpx` binary for Klaw ACP validation.
|
|
50
|
+
- If the plugin-local ACPX binary is missing or on the wrong version, reinstall it from the version pinned in `extensions/acpx/package.json`.
|
|
51
|
+
|
|
52
|
+
## Boundary Rule
|
|
53
|
+
|
|
54
|
+
If a change feels like shared ACP runtime behavior instead of Klaw-specific glue, move it to `klaw/acpx` and consume it from here instead of re-implementing it inside `extensions/acpx`.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# ACPX Extension Notes
|
|
2
|
+
|
|
3
|
+
This file applies to work under `extensions/acpx/`.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The ACPX extension is a thin Klaw wrapper around the published `acpx` package. Keep reusable ACP runtime logic in `klaw/acpx`, not in this extension.
|
|
8
|
+
|
|
9
|
+
## Default Version Policy
|
|
10
|
+
|
|
11
|
+
- `extensions/acpx/package.json` should point at a published npm release by default.
|
|
12
|
+
- Do not leave the extension pinned to a temporary GitHub commit or local checkout once the ACPX release exists.
|
|
13
|
+
- Do not leave temporary pnpm build-script allowlist exceptions behind after switching back to a published ACPX package.
|
|
14
|
+
|
|
15
|
+
## Unreleased ACPX Development Flow
|
|
16
|
+
|
|
17
|
+
Use this flow when Klaw needs unreleased ACPX changes before the ACPX version is published.
|
|
18
|
+
|
|
19
|
+
1. Make the ACPX code change in the `klaw/acpx` repo first.
|
|
20
|
+
2. In Klaw, temporarily point `extensions/acpx/package.json` at the ACPX GitHub commit you need.
|
|
21
|
+
3. If pnpm blocks ACPX lifecycle/build scripts for that temporary GitHub-sourced package, temporarily add `acpx: true` to `allowBuilds` in `pnpm-workspace.yaml`.
|
|
22
|
+
4. Refresh the root workspace lock:
|
|
23
|
+
- `pnpm install --lockfile-only --filter ./extensions/acpx`
|
|
24
|
+
5. Refresh the extension-local npm lock for install metadata:
|
|
25
|
+
- `cd extensions/acpx && npm install --package-lock-only --ignore-scripts`
|
|
26
|
+
6. Rebuild Klaw and restart the gateway before doing live ACP validation.
|
|
27
|
+
7. Once ACPX is released, switch `extensions/acpx/package.json` back to the published npm version and refresh the same lockfiles again.
|
|
28
|
+
8. Remove any temporary `acpx` build-script allowlist entry that was only needed for the GitHub-sourced development pin.
|
|
29
|
+
|
|
30
|
+
## Lockfile Notes
|
|
31
|
+
|
|
32
|
+
- `pnpm-lock.yaml` is the tracked workspace lockfile and must match the ACPX version referenced by `extensions/acpx/package.json`.
|
|
33
|
+
- `extensions/acpx/package-lock.json` is useful local install metadata for the plugin package.
|
|
34
|
+
- If `extensions/acpx/package-lock.json` is gitignored in this repo state, regenerating it is still useful for local verification, but it will not appear in `git status`.
|
|
35
|
+
|
|
36
|
+
## Local Runtime Validation
|
|
37
|
+
|
|
38
|
+
When ACPX integration changes here, prefer this sequence:
|
|
39
|
+
|
|
40
|
+
1. `pnpm install --filter ./extensions/acpx`
|
|
41
|
+
2. `pnpm test:extension acpx`
|
|
42
|
+
3. `pnpm build`
|
|
43
|
+
4. Restart the local gateway if ACP runtime behavior or bundled plugin wiring changed.
|
|
44
|
+
5. If the change affects direct ACP behavior in chat, run a real ACP smoke after restart.
|
|
45
|
+
|
|
46
|
+
## Direct ACPX Binary Policy
|
|
47
|
+
|
|
48
|
+
- Prefer the plugin-local ACPX binary under `extensions/acpx/node_modules/.bin/acpx`.
|
|
49
|
+
- Do not rely on a globally installed `acpx` binary for Klaw ACP validation.
|
|
50
|
+
- If the plugin-local ACPX binary is missing or on the wrong version, reinstall it from the version pinned in `extensions/acpx/package.json`.
|
|
51
|
+
|
|
52
|
+
## Boundary Rule
|
|
53
|
+
|
|
54
|
+
If a change feels like shared ACP runtime behavior instead of Klaw-specific glue, move it to `klaw/acpx` and consume it from here instead of re-implementing it inside `extensions/acpx`.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createAcpxRuntimeService } from "./register.runtime.js";
|
|
2
|
+
import { tryDispatchAcpReplyHook } from "klaw/plugin-sdk/acp-runtime-backend";
|
|
3
|
+
//#region extensions/acpx/index.ts
|
|
4
|
+
const plugin = {
|
|
5
|
+
id: "acpx",
|
|
6
|
+
name: "ACPX Runtime",
|
|
7
|
+
description: "Embedded ACP runtime backend with plugin-owned session and transport management.",
|
|
8
|
+
register(api) {
|
|
9
|
+
api.registerService(createAcpxRuntimeService({ pluginConfig: api.pluginConfig }));
|
|
10
|
+
api.on("reply_dispatch", tryDispatchAcpReplyHook);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { plugin as default };
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { readJsonFileWithFallback, writeJsonFileAtomically } from "klaw/plugin-sdk/json-store";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
//#region extensions/acpx/src/process-lease.ts
|
|
9
|
+
const KLAW_ACPX_LEASE_ID_ENV = "KLAW_ACPX_LEASE_ID";
|
|
10
|
+
const KLAW_GATEWAY_INSTANCE_ID_ENV = "KLAW_GATEWAY_INSTANCE_ID";
|
|
11
|
+
const KLAW_ACPX_LEASE_ID_ARG = "--klaw-acpx-lease-id";
|
|
12
|
+
const KLAW_GATEWAY_INSTANCE_ID_ARG = "--klaw-gateway-instance-id";
|
|
13
|
+
const LEASE_FILE = "process-leases.json";
|
|
14
|
+
function normalizeLease(value) {
|
|
15
|
+
if (typeof value !== "object" || value === null) return;
|
|
16
|
+
const record = value;
|
|
17
|
+
if (typeof record.leaseId !== "string" || typeof record.gatewayInstanceId !== "string" || typeof record.sessionKey !== "string" || typeof record.wrapperRoot !== "string" || typeof record.wrapperPath !== "string" || typeof record.rootPid !== "number" || typeof record.commandHash !== "string" || typeof record.startedAt !== "number" || ![
|
|
18
|
+
"open",
|
|
19
|
+
"closing",
|
|
20
|
+
"closed",
|
|
21
|
+
"lost"
|
|
22
|
+
].includes(String(record.state))) return;
|
|
23
|
+
return {
|
|
24
|
+
leaseId: record.leaseId,
|
|
25
|
+
gatewayInstanceId: record.gatewayInstanceId,
|
|
26
|
+
sessionKey: record.sessionKey,
|
|
27
|
+
wrapperRoot: record.wrapperRoot,
|
|
28
|
+
wrapperPath: record.wrapperPath,
|
|
29
|
+
rootPid: record.rootPid,
|
|
30
|
+
...typeof record.processGroupId === "number" ? { processGroupId: record.processGroupId } : {},
|
|
31
|
+
commandHash: record.commandHash,
|
|
32
|
+
startedAt: record.startedAt,
|
|
33
|
+
state: record.state
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function readLeaseFile(filePath) {
|
|
37
|
+
const { value } = await readJsonFileWithFallback(filePath, {
|
|
38
|
+
version: 1,
|
|
39
|
+
leases: []
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
version: 1,
|
|
43
|
+
leases: Array.isArray(value.leases) ? value.leases.map(normalizeLease).filter((lease) => !!lease) : []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function writeLeaseFile(filePath, value) {
|
|
47
|
+
return writeJsonFileAtomically(filePath, value);
|
|
48
|
+
}
|
|
49
|
+
function createAcpxProcessLeaseStore(params) {
|
|
50
|
+
const filePath = path.join(params.stateDir, LEASE_FILE);
|
|
51
|
+
let updateQueue = Promise.resolve();
|
|
52
|
+
async function update(mutator) {
|
|
53
|
+
const run = updateQueue.then(async () => {
|
|
54
|
+
await fs.mkdir(params.stateDir, { recursive: true });
|
|
55
|
+
await writeLeaseFile(filePath, {
|
|
56
|
+
version: 1,
|
|
57
|
+
leases: mutator((await readLeaseFile(filePath)).leases)
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
updateQueue = run.catch(() => {});
|
|
61
|
+
await run;
|
|
62
|
+
}
|
|
63
|
+
async function readCurrent() {
|
|
64
|
+
await updateQueue;
|
|
65
|
+
return await readLeaseFile(filePath);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
async load(leaseId) {
|
|
69
|
+
return (await readCurrent()).leases.find((lease) => lease.leaseId === leaseId);
|
|
70
|
+
},
|
|
71
|
+
async listOpen(gatewayInstanceId) {
|
|
72
|
+
return (await readCurrent()).leases.filter((lease) => (lease.state === "open" || lease.state === "closing") && (!gatewayInstanceId || lease.gatewayInstanceId === gatewayInstanceId));
|
|
73
|
+
},
|
|
74
|
+
async save(lease) {
|
|
75
|
+
await update((leases) => [...leases.filter((entry) => entry.leaseId !== lease.leaseId), lease]);
|
|
76
|
+
},
|
|
77
|
+
async markState(leaseId, state) {
|
|
78
|
+
await update((leases) => leases.map((lease) => lease.leaseId === leaseId ? {
|
|
79
|
+
...lease,
|
|
80
|
+
state
|
|
81
|
+
} : lease));
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function createAcpxProcessLeaseId() {
|
|
86
|
+
return randomUUID();
|
|
87
|
+
}
|
|
88
|
+
function hashAcpxProcessCommand(command) {
|
|
89
|
+
return createHash("sha256").update(command).digest("hex");
|
|
90
|
+
}
|
|
91
|
+
function quoteEnvValue(value) {
|
|
92
|
+
return /^[A-Za-z0-9_./:=@+-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
|
|
93
|
+
}
|
|
94
|
+
function appendAcpxLeaseArgs(params) {
|
|
95
|
+
return [
|
|
96
|
+
params.command,
|
|
97
|
+
KLAW_ACPX_LEASE_ID_ARG,
|
|
98
|
+
quoteEnvValue(params.leaseId),
|
|
99
|
+
KLAW_GATEWAY_INSTANCE_ID_ARG,
|
|
100
|
+
quoteEnvValue(params.gatewayInstanceId)
|
|
101
|
+
].join(" ");
|
|
102
|
+
}
|
|
103
|
+
function withAcpxLeaseEnvironment(params) {
|
|
104
|
+
if ((params.platform ?? process.platform) === "win32") return appendAcpxLeaseArgs(params);
|
|
105
|
+
return [
|
|
106
|
+
"env",
|
|
107
|
+
`${KLAW_ACPX_LEASE_ID_ENV}=${quoteEnvValue(params.leaseId)}`,
|
|
108
|
+
`${KLAW_GATEWAY_INSTANCE_ID_ENV}=${quoteEnvValue(params.gatewayInstanceId)}`,
|
|
109
|
+
appendAcpxLeaseArgs(params)
|
|
110
|
+
].join(" ");
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region extensions/acpx/src/process-reaper.ts
|
|
114
|
+
const execFileAsync = promisify(execFile);
|
|
115
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
116
|
+
const GENERATED_WRAPPER_BASENAMES = new Set(["codex-acp-wrapper.mjs", "claude-agent-acp-wrapper.mjs"]);
|
|
117
|
+
const KLAW_PLUGIN_DEPS_MARKER = "/plugin-runtime-deps/";
|
|
118
|
+
const OWNED_ACP_PACKAGE_NAMES = [
|
|
119
|
+
"@zed-industries/codex-acp",
|
|
120
|
+
"@zed-industries/codex-acp-darwin-arm64",
|
|
121
|
+
"@zed-industries/codex-acp-darwin-x64",
|
|
122
|
+
"@zed-industries/codex-acp-linux-arm64",
|
|
123
|
+
"@zed-industries/codex-acp-linux-x64",
|
|
124
|
+
"@zed-industries/codex-acp-win32-arm64",
|
|
125
|
+
"@zed-industries/codex-acp-win32-x64",
|
|
126
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
127
|
+
"acpx"
|
|
128
|
+
];
|
|
129
|
+
const ACP_PACKAGE_MARKERS = [
|
|
130
|
+
"/@zed-industries/codex-acp/",
|
|
131
|
+
"/@agentclientprotocol/claude-agent-acp/",
|
|
132
|
+
"/acpx/dist/"
|
|
133
|
+
];
|
|
134
|
+
function normalizePathLike(value) {
|
|
135
|
+
return value.replaceAll("\\", "/");
|
|
136
|
+
}
|
|
137
|
+
function resolvePackageRoot(packageName) {
|
|
138
|
+
try {
|
|
139
|
+
return normalizePathLike(path.dirname(requireFromHere.resolve(`${packageName}/package.json`)));
|
|
140
|
+
} catch {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const OWNED_ACP_PACKAGE_ROOTS = OWNED_ACP_PACKAGE_NAMES.map(resolvePackageRoot).filter((root) => Boolean(root));
|
|
145
|
+
function commandBelongsToResolvedAcpPackage(command) {
|
|
146
|
+
return OWNED_ACP_PACKAGE_ROOTS.some((root) => command.includes(`${root}/`));
|
|
147
|
+
}
|
|
148
|
+
function commandMentionsGeneratedWrapper(command) {
|
|
149
|
+
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => command.includes(basename));
|
|
150
|
+
}
|
|
151
|
+
function commandWrapperBelongsToRoot(command, wrapperRoot) {
|
|
152
|
+
if (!wrapperRoot) return true;
|
|
153
|
+
const normalizedCommand = normalizePathLike(command);
|
|
154
|
+
const normalizedRoot = normalizePathLike(wrapperRoot).replace(/\/+$/, "");
|
|
155
|
+
return Array.from(GENERATED_WRAPPER_BASENAMES).some((basename) => normalizedCommand.includes(`${normalizedRoot}/${basename}`));
|
|
156
|
+
}
|
|
157
|
+
function isKlawLeaseAwareAcpxProcessCommand(params) {
|
|
158
|
+
const command = params.command?.trim();
|
|
159
|
+
if (!command) return false;
|
|
160
|
+
const normalized = normalizePathLike(command);
|
|
161
|
+
return commandMentionsGeneratedWrapper(normalized) && commandWrapperBelongsToRoot(normalized, params.wrapperRoot);
|
|
162
|
+
}
|
|
163
|
+
function commandsReferToSameRootCommand(liveCommand, storedCommand) {
|
|
164
|
+
if (!storedCommand?.trim()) return true;
|
|
165
|
+
return normalizePathLike(liveCommand).trim() === normalizePathLike(storedCommand).trim();
|
|
166
|
+
}
|
|
167
|
+
function splitCommandParts(value) {
|
|
168
|
+
const parts = [];
|
|
169
|
+
let current = "";
|
|
170
|
+
let quote = null;
|
|
171
|
+
let escaping = false;
|
|
172
|
+
for (const ch of value) {
|
|
173
|
+
if (escaping) {
|
|
174
|
+
current += ch;
|
|
175
|
+
escaping = false;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (ch === "\\" && quote !== "'") {
|
|
179
|
+
escaping = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (quote) {
|
|
183
|
+
if (ch === quote) quote = null;
|
|
184
|
+
else current += ch;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "'" || ch === "\"") {
|
|
188
|
+
quote = ch;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (/\s/.test(ch)) {
|
|
192
|
+
if (current) {
|
|
193
|
+
parts.push(current);
|
|
194
|
+
current = "";
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
current += ch;
|
|
199
|
+
}
|
|
200
|
+
if (escaping) current += "\\";
|
|
201
|
+
if (current) parts.push(current);
|
|
202
|
+
return parts;
|
|
203
|
+
}
|
|
204
|
+
function commandOptionEquals(parts, option, expected) {
|
|
205
|
+
if (!expected) return true;
|
|
206
|
+
const index = parts.indexOf(option);
|
|
207
|
+
return index >= 0 && parts[index + 1] === expected;
|
|
208
|
+
}
|
|
209
|
+
function liveCommandMatchesLeaseIdentity(params) {
|
|
210
|
+
if (!params.expectedLeaseId && !params.expectedGatewayInstanceId) return true;
|
|
211
|
+
const parts = splitCommandParts(params.command ?? "");
|
|
212
|
+
return commandOptionEquals(parts, "--klaw-acpx-lease-id", params.expectedLeaseId) && commandOptionEquals(parts, "--klaw-gateway-instance-id", params.expectedGatewayInstanceId);
|
|
213
|
+
}
|
|
214
|
+
function isKlawOwnedAcpxProcessCommand(params) {
|
|
215
|
+
const command = params.command?.trim();
|
|
216
|
+
if (!command) return false;
|
|
217
|
+
const normalized = normalizePathLike(command);
|
|
218
|
+
if (isKlawLeaseAwareAcpxProcessCommand({
|
|
219
|
+
command: normalized,
|
|
220
|
+
wrapperRoot: params.wrapperRoot
|
|
221
|
+
})) return true;
|
|
222
|
+
if (commandBelongsToResolvedAcpPackage(normalized)) return true;
|
|
223
|
+
if (!normalized.includes(KLAW_PLUGIN_DEPS_MARKER)) return false;
|
|
224
|
+
return ACP_PACKAGE_MARKERS.some((marker) => normalized.includes(marker));
|
|
225
|
+
}
|
|
226
|
+
function parseProcessList(stdout) {
|
|
227
|
+
const processes = [];
|
|
228
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
229
|
+
const match = /^\s*(?<pid>\d+)\s+(?<ppid>\d+)\s+(?<command>.+?)\s*$/.exec(line);
|
|
230
|
+
if (!match?.groups) continue;
|
|
231
|
+
processes.push({
|
|
232
|
+
pid: Number.parseInt(match.groups.pid, 10),
|
|
233
|
+
ppid: Number.parseInt(match.groups.ppid, 10),
|
|
234
|
+
command: match.groups.command
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return processes;
|
|
238
|
+
}
|
|
239
|
+
async function listPlatformProcesses() {
|
|
240
|
+
if (process.platform === "win32") return [];
|
|
241
|
+
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], { maxBuffer: 8 * 1024 * 1024 });
|
|
242
|
+
return parseProcessList(stdout);
|
|
243
|
+
}
|
|
244
|
+
function collectProcessTree(processes, rootPid) {
|
|
245
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
246
|
+
for (const processInfo of processes) {
|
|
247
|
+
const children = childrenByParent.get(processInfo.ppid) ?? [];
|
|
248
|
+
children.push(processInfo);
|
|
249
|
+
childrenByParent.set(processInfo.ppid, children);
|
|
250
|
+
}
|
|
251
|
+
const root = new Map(processes.map((processInfo) => [processInfo.pid, processInfo])).get(rootPid);
|
|
252
|
+
const collected = [];
|
|
253
|
+
if (root) collected.push(root);
|
|
254
|
+
const queue = [...childrenByParent.get(rootPid) ?? []];
|
|
255
|
+
while (queue.length > 0) {
|
|
256
|
+
const next = queue.shift();
|
|
257
|
+
if (!next || collected.some((processInfo) => processInfo.pid === next.pid)) continue;
|
|
258
|
+
collected.push(next);
|
|
259
|
+
queue.push(...childrenByParent.get(next.pid) ?? []);
|
|
260
|
+
}
|
|
261
|
+
return collected;
|
|
262
|
+
}
|
|
263
|
+
function uniquePids(processes) {
|
|
264
|
+
return Array.from(new Set(processes.map((processInfo) => processInfo.pid).filter((pid) => Number.isInteger(pid) && pid > 0 && pid !== process.pid)));
|
|
265
|
+
}
|
|
266
|
+
function isProcessAlive(pid) {
|
|
267
|
+
try {
|
|
268
|
+
process.kill(pid, 0);
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async function terminatePids(pids, deps) {
|
|
275
|
+
const killProcess = deps?.killProcess ?? ((pid, signal) => process.kill(pid, signal));
|
|
276
|
+
const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
277
|
+
const terminated = [];
|
|
278
|
+
for (const pid of pids) try {
|
|
279
|
+
killProcess(pid, "SIGTERM");
|
|
280
|
+
terminated.push(pid);
|
|
281
|
+
} catch {}
|
|
282
|
+
if (terminated.length === 0) return terminated;
|
|
283
|
+
await sleep(750);
|
|
284
|
+
for (const pid of terminated) if (deps?.killProcess || isProcessAlive(pid)) try {
|
|
285
|
+
killProcess(pid, "SIGKILL");
|
|
286
|
+
} catch {}
|
|
287
|
+
return terminated;
|
|
288
|
+
}
|
|
289
|
+
async function cleanupKlawOwnedAcpxProcessTree(params) {
|
|
290
|
+
const rootPid = params.rootPid;
|
|
291
|
+
if (!rootPid || rootPid <= 0 || rootPid === process.pid) return {
|
|
292
|
+
inspectedPids: [],
|
|
293
|
+
terminatedPids: [],
|
|
294
|
+
skippedReason: "missing-root"
|
|
295
|
+
};
|
|
296
|
+
let processes = [];
|
|
297
|
+
try {
|
|
298
|
+
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
|
299
|
+
} catch {
|
|
300
|
+
processes = [];
|
|
301
|
+
}
|
|
302
|
+
const listedTree = collectProcessTree(processes, rootPid);
|
|
303
|
+
if (listedTree.length === 0) return {
|
|
304
|
+
inspectedPids: [],
|
|
305
|
+
terminatedPids: [],
|
|
306
|
+
skippedReason: "unverified-root"
|
|
307
|
+
};
|
|
308
|
+
const rootCommand = listedTree[0]?.command ?? params.rootCommand;
|
|
309
|
+
const liveCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(rootCommand ?? ""));
|
|
310
|
+
const storedCommandWasGeneratedWrapper = commandMentionsGeneratedWrapper(normalizePathLike(params.rootCommand ?? ""));
|
|
311
|
+
if (!liveCommandWasGeneratedWrapper && storedCommandWasGeneratedWrapper) return {
|
|
312
|
+
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
|
313
|
+
terminatedPids: [],
|
|
314
|
+
skippedReason: "not-klaw-owned"
|
|
315
|
+
};
|
|
316
|
+
if (!liveCommandWasGeneratedWrapper && !commandsReferToSameRootCommand(rootCommand ?? "", params.rootCommand)) return {
|
|
317
|
+
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
|
318
|
+
terminatedPids: [],
|
|
319
|
+
skippedReason: "not-klaw-owned"
|
|
320
|
+
};
|
|
321
|
+
if (!isKlawOwnedAcpxProcessCommand({
|
|
322
|
+
command: rootCommand,
|
|
323
|
+
wrapperRoot: params.wrapperRoot
|
|
324
|
+
})) return {
|
|
325
|
+
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
|
326
|
+
terminatedPids: [],
|
|
327
|
+
skippedReason: "not-klaw-owned"
|
|
328
|
+
};
|
|
329
|
+
if (!liveCommandMatchesLeaseIdentity({
|
|
330
|
+
command: rootCommand,
|
|
331
|
+
expectedLeaseId: params.expectedLeaseId,
|
|
332
|
+
expectedGatewayInstanceId: params.expectedGatewayInstanceId
|
|
333
|
+
})) return {
|
|
334
|
+
inspectedPids: listedTree.map((processInfo) => processInfo.pid),
|
|
335
|
+
terminatedPids: [],
|
|
336
|
+
skippedReason: "not-klaw-owned"
|
|
337
|
+
};
|
|
338
|
+
const pids = uniquePids(listedTree.toReversed());
|
|
339
|
+
return {
|
|
340
|
+
inspectedPids: uniquePids(listedTree),
|
|
341
|
+
terminatedPids: await terminatePids(pids, params.deps)
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function reapStaleKlawOwnedAcpxOrphans(params) {
|
|
345
|
+
if (process.platform === "win32") return {
|
|
346
|
+
inspectedPids: [],
|
|
347
|
+
terminatedPids: [],
|
|
348
|
+
skippedReason: "unsupported-platform"
|
|
349
|
+
};
|
|
350
|
+
let processes;
|
|
351
|
+
try {
|
|
352
|
+
processes = await (params.deps?.listProcesses ?? listPlatformProcesses)();
|
|
353
|
+
} catch {
|
|
354
|
+
return {
|
|
355
|
+
inspectedPids: [],
|
|
356
|
+
terminatedPids: [],
|
|
357
|
+
skippedReason: "process-list-unavailable"
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const orphanTrees = processes.filter((processInfo) => processInfo.ppid === 1 && isKlawOwnedAcpxProcessCommand({
|
|
361
|
+
command: processInfo.command,
|
|
362
|
+
wrapperRoot: params.wrapperRoot
|
|
363
|
+
})).map((orphan) => collectProcessTree(processes, orphan.pid));
|
|
364
|
+
return {
|
|
365
|
+
inspectedPids: uniquePids(orphanTrees.flat()),
|
|
366
|
+
terminatedPids: await terminatePids(uniquePids(orphanTrees.flatMap((tree) => tree.toReversed())), params.deps)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
export { KLAW_ACPX_LEASE_ID_ENV as a, createAcpxProcessLeaseStore as c, KLAW_ACPX_LEASE_ID_ARG as i, hashAcpxProcessCommand as l, isKlawLeaseAwareAcpxProcessCommand as n, KLAW_GATEWAY_INSTANCE_ID_ARG as o, reapStaleKlawOwnedAcpxOrphans as r, createAcpxProcessLeaseId as s, cleanupKlawOwnedAcpxProcessTree as t, withAcpxLeaseEnvironment as u };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getAcpRuntimeBackend, unregisterAcpRuntimeBackend } from "klaw/plugin-sdk/acp-runtime-backend";
|
|
2
|
+
//#region extensions/acpx/register.runtime.ts
|
|
3
|
+
const ACPX_BACKEND_ID = "acpx";
|
|
4
|
+
let serviceModulePromise = null;
|
|
5
|
+
function loadServiceModule() {
|
|
6
|
+
serviceModulePromise ??= import("./service-CXeUME_-.js");
|
|
7
|
+
return serviceModulePromise;
|
|
8
|
+
}
|
|
9
|
+
async function startRealService(state) {
|
|
10
|
+
if (state.realRuntime) return state.realRuntime;
|
|
11
|
+
if (!state.ctx) throw new Error("ACPX runtime service is not started");
|
|
12
|
+
state.startPromise ??= (async () => {
|
|
13
|
+
const { createAcpxRuntimeService } = await loadServiceModule();
|
|
14
|
+
const service = createAcpxRuntimeService(state.params);
|
|
15
|
+
state.realService = service;
|
|
16
|
+
await service.start(state.ctx);
|
|
17
|
+
const backend = getAcpRuntimeBackend(ACPX_BACKEND_ID);
|
|
18
|
+
if (!backend?.runtime) throw new Error("ACPX runtime service did not register an ACP backend");
|
|
19
|
+
state.realRuntime = backend.runtime;
|
|
20
|
+
return state.realRuntime;
|
|
21
|
+
})();
|
|
22
|
+
return await state.startPromise;
|
|
23
|
+
}
|
|
24
|
+
function createAcpxRuntimeService(params = {}) {
|
|
25
|
+
const state = {
|
|
26
|
+
ctx: null,
|
|
27
|
+
params,
|
|
28
|
+
realRuntime: null,
|
|
29
|
+
realService: null,
|
|
30
|
+
startPromise: null
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
id: "acpx-runtime",
|
|
34
|
+
async start(ctx) {
|
|
35
|
+
if (process.env.KLAW_SKIP_ACPX_RUNTIME === "1") {
|
|
36
|
+
ctx.logger.info("skipping embedded acpx runtime backend (KLAW_SKIP_ACPX_RUNTIME=1)");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
state.ctx = ctx;
|
|
40
|
+
await startRealService(state);
|
|
41
|
+
},
|
|
42
|
+
async stop(ctx) {
|
|
43
|
+
if (state.realService) await state.realService.stop?.(ctx);
|
|
44
|
+
else unregisterAcpRuntimeBackend(ACPX_BACKEND_ID);
|
|
45
|
+
state.ctx = null;
|
|
46
|
+
state.realRuntime = null;
|
|
47
|
+
state.realService = null;
|
|
48
|
+
state.startPromise = null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { createAcpxRuntimeService };
|