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:
Bob
2026-03-17 17:27:52 +01:00
committed by GitHub
parent 8139f83175
commit ea15819ecf
102 changed files with 6606 additions and 1199 deletions

View File

@@ -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(
{

View File

@@ -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;