mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 09:50:23 +00:00
ACP: harden startup and move configured routing behind plugin seams (#48197)
* ACPX: keep plugin-local runtime installs out of dist * Gateway: harden ACP startup and service PATH * ACP: reinitialize error-state configured bindings * ACP: classify pre-turn runtime failures as session init failures * Plugins: move configured ACP routing behind channel seams * Telegram tests: align startup probe assertions after rebase * Discord: harden ACP configured binding recovery * ACP: recover Discord bindings after stale runtime exits * ACPX: replace dead sessions during ensure * Discord: harden ACP binding recovery * Discord: fix review follow-ups * ACP bindings: load channel snapshots across workspaces * ACP bindings: cache snapshot channel plugin resolution * Experiments: add ACP pluginification holy grail plan * Experiments: rename ACP pluginification plan doc * Experiments: drop old ACP pluginification doc path * ACP: move configured bindings behind plugin services * Experiments: update bindings capability architecture plan * Bindings: isolate configured binding routing and targets * Discord tests: fix runtime env helper path * Tests: fix channel binding CI regressions * Tests: normalize ACP workspace assertion on Windows * Bindings: isolate configured binding registry * Bindings: finish configured binding cleanup * Bindings: finish generic cleanup * Bindings: align runtime approval callbacks * ACP: delete residual bindings barrel * Bindings: restore legacy compatibility * Revert "Bindings: restore legacy compatibility" This reverts commit ac2ed68fa2426ecc874d68278c71c71ad363fcfe. * Tests: drop ACP route legacy helper names * Discord/ACP: fix binding regressions --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -64,6 +64,58 @@ describe("resolveSpawnCommand", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("routes node shebang wrappers through the current node runtime on posix", async () => {
|
||||
const dir = await createTempDir();
|
||||
const scriptPath = path.join(dir, "acpx");
|
||||
await writeFile(scriptPath, "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8");
|
||||
await chmod(scriptPath, 0o755);
|
||||
|
||||
const resolved = resolveSpawnCommand(
|
||||
{
|
||||
command: scriptPath,
|
||||
args: ["--help"],
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
platform: "linux",
|
||||
env: {},
|
||||
execPath: "/custom/node",
|
||||
},
|
||||
);
|
||||
|
||||
expect(resolved).toEqual({
|
||||
command: "/custom/node",
|
||||
args: [scriptPath, "--help"],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes PATH-resolved node shebang wrappers through the current node runtime on posix", async () => {
|
||||
const dir = await createTempDir();
|
||||
const binDir = path.join(dir, "bin");
|
||||
const scriptPath = path.join(binDir, "acpx");
|
||||
await mkdir(binDir, { recursive: true });
|
||||
await writeFile(scriptPath, "#!/usr/bin/env node\nconsole.log('ok')\n", "utf8");
|
||||
await chmod(scriptPath, 0o755);
|
||||
|
||||
const resolved = resolveSpawnCommand(
|
||||
{
|
||||
command: "acpx",
|
||||
args: ["--help"],
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
platform: "linux",
|
||||
env: { PATH: binDir },
|
||||
execPath: "/custom/node",
|
||||
},
|
||||
);
|
||||
|
||||
expect(resolved).toEqual({
|
||||
command: "/custom/node",
|
||||
args: [scriptPath, "--help"],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes .js command execution through node on windows", () => {
|
||||
const resolved = resolveSpawnCommand(
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { accessSync, constants as fsConstants, existsSync, readFileSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type {
|
||||
WindowsSpawnProgram,
|
||||
WindowsSpawnProgramCandidate,
|
||||
@@ -57,11 +58,76 @@ const DEFAULT_RUNTIME: SpawnRuntime = {
|
||||
execPath: process.execPath,
|
||||
};
|
||||
|
||||
function isExecutableFile(filePath: string, platform: NodeJS.Platform): boolean {
|
||||
try {
|
||||
const stat = statSync(filePath);
|
||||
if (!stat.isFile()) {
|
||||
return false;
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return true;
|
||||
}
|
||||
accessSync(filePath, fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveExecutableFromPath(command: string, runtime: SpawnRuntime): string | undefined {
|
||||
const pathEnv = runtime.env.PATH ?? runtime.env.Path;
|
||||
if (!pathEnv) {
|
||||
return undefined;
|
||||
}
|
||||
for (const entry of pathEnv.split(path.delimiter).filter(Boolean)) {
|
||||
const candidate = path.join(entry, command);
|
||||
if (isExecutableFile(candidate, runtime.platform)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveNodeShebangScriptPath(command: string, runtime: SpawnRuntime): string | undefined {
|
||||
const commandPath =
|
||||
path.isAbsolute(command) || command.includes(path.sep)
|
||||
? command
|
||||
: resolveExecutableFromPath(command, runtime);
|
||||
if (!commandPath || !isExecutableFile(commandPath, runtime.platform)) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const firstLine = readFileSync(commandPath, "utf8").split(/\r?\n/, 1)[0] ?? "";
|
||||
if (/^#!.*(?:\/usr\/bin\/env\s+node\b|\/node(?:js)?\b)/.test(firstLine)) {
|
||||
return commandPath;
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveSpawnCommand(
|
||||
params: { command: string; args: string[] },
|
||||
options?: SpawnCommandOptions,
|
||||
runtime: SpawnRuntime = DEFAULT_RUNTIME,
|
||||
): ResolvedSpawnCommand {
|
||||
if (runtime.platform !== "win32") {
|
||||
const nodeShebangScript = resolveNodeShebangScriptPath(params.command, runtime);
|
||||
if (nodeShebangScript) {
|
||||
options?.onResolved?.({
|
||||
command: params.command,
|
||||
cacheHit: false,
|
||||
strictWindowsCmdWrapper: options?.strictWindowsCmdWrapper === true,
|
||||
resolution: "direct",
|
||||
});
|
||||
return {
|
||||
command: runtime.execPath,
|
||||
args: [nodeShebangScript, ...params.args],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const strictWindowsCmdWrapper = options?.strictWindowsCmdWrapper === true;
|
||||
const cacheKey = params.command;
|
||||
const cachedProgram = options?.cache;
|
||||
|
||||
Reference in New Issue
Block a user