fix(acpx): decouple host proxy spawn cwd from in-sandbox remoteCwd (#10122)

This commit is contained in:
Nicky Leach 2026-07-23 16:48:52 -07:00 committed by GitHub
parent e41ba306c5
commit b517b887ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 356 additions and 37 deletions

View File

@ -776,12 +776,17 @@ describe("shared ACPX engine runtime behavior", () => {
const root = await makeTempRoot();
const localCwd = path.join(root, "local");
const remoteCwd = "/workspace/remote";
const { sessionInputs } = await runExecutor(
const { sessionInputs, runtimeOptions } = await runExecutor(
{ agent: "custom", agentCommand: "node ./fake-acp.js", cwd: localCwd, stateDir: path.join(root, "state") },
{ context: { paperclipWorkspace: { cwd: localCwd, workspaceWorktreePath: localCwd } }, executionTarget: { kind: "remote", transport: "ssh", remoteCwd } },
);
const env = (sessionInputs[0]!.sessionOptions as { env: Record<string, string> }).env;
expect(env.PAPERCLIP_WORKSPACE_CWD).toBe(localCwd);
// The ssh remote transport is NOT the runner-backed process-session lane, so
// it stays byte-identical: no host-spawn redirect. `cwd` is the host cwd and
// `spawnCwd` is unset.
expect(runtimeOptions[0]!.cwd).toBe(localCwd);
expect(runtimeOptions[0]!.spawnCwd).toBeUndefined();
});
it("does not materialize credential wrapper scripts", async () => {
@ -888,6 +893,9 @@ describe("shared ACPX engine runtime behavior", () => {
const { runtimeOptions } = await runExecutor({ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, "state") });
expect(runtimeOptions[0]!.verbose).toBe(false);
expect(runtimeOptions[0]!.onAgentStderr).toBeTypeOf("function");
// Local lane is byte-identical: no host-spawn redirect, so `spawnCwd` is
// unset and acpx falls back to `cwd`.
expect(runtimeOptions[0]!.spawnCwd).toBeUndefined();
});
it("starts sandbox ACP process sessions in the remote execution cwd", async () => {
@ -911,7 +919,7 @@ describe("shared ACPX engine runtime behavior", () => {
},
);
await runExecutor(
const { runtimeOptions, sessionInputs } = await runExecutor(
{ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir, cwd: localCwd },
{
authToken: "real-run-jwt",
@ -930,6 +938,18 @@ describe("shared ACPX engine runtime behavior", () => {
args: ["-lc", "exec node ./fake-acp.js"],
cwd: remoteCwd,
});
// Host-spawn cwd decoupling: on the remote process-session lane the acpx
// runtime host-spawns the relay proxy, whose `chdir` must land in a
// HOST-valid dir — the engine's host `cwd` (`localCwd`) — while the advertised
// ACP `session/new` cwd and the in-sandbox `commandPayload.cwd` stay
// `remoteCwd`. `spawnCwd` carries the host-only redirect; it must differ from
// the advertised session cwd. (Threading proof; the acpx runtime honoring
// `spawnCwd ?? cwd` at the real host spawn is proven in remote-spawn-smoke.)
expect(runtimeOptions[0]!.cwd).toBe(remoteCwd);
expect(sessionInputs[0]!.cwd).toBe(remoteCwd);
expect(runtimeOptions[0]!.spawnCwd).toBe(localCwd);
expect(runtimeOptions[0]!.spawnCwd).not.toBe(sessionInputs[0]!.cwd);
const payloadEnv = ((sessionPayload as Record<string, unknown> | null)?.env ?? {}) as Record<string, unknown>;
expect(payloadEnv).toMatchObject({
PAPERCLIP_API_BRIDGE_MODE: "queue_v1",
@ -941,6 +961,48 @@ describe("shared ACPX engine runtime behavior", () => {
expect(payloadEnv.PAPERCLIP_API_KEY).not.toBe("real-run-jwt");
});
it("keeps the session fingerprint stable when only the host spawn cwd changes", async () => {
// `spawnCwd` (the host-only spawn redirect = the host `cwd`) must NOT enter
// the session fingerprint or compat key: two runs of the same session that
// stage into the same in-sandbox `remoteCwd` from DIFFERENT host worktrees
// must reuse — not invalidate — the staged runtime. So the fingerprint has to
// ignore the host cwd and key only on the advertised session cwd (`remoteCwd`).
const root = await makeTempRoot();
const remoteCwd = path.join(root, "remote-workspace");
await fs.mkdir(remoteCwd, { recursive: true });
const runOnce = async (hostWorktree: string) => {
const localCwd = path.join(root, hostWorktree);
await fs.mkdir(localCwd, { recursive: true });
return runExecutor(
{ agent: "custom", agentCommand: "node ./fake-acp.js", stateDir: path.join(root, hostWorktree, "state"), cwd: localCwd },
{
authToken: "real-run-jwt",
executionTarget: {
kind: "remote",
transport: "sandbox",
providerKey: "fake-plugin",
remoteCwd,
runner: createLocalSandboxRunner(),
},
},
);
};
const first = await runOnce("worktree-a");
const second = await runOnce("worktree-b");
// Host cwd (and therefore `spawnCwd`) differs between the two runs...
expect(first.runtimeOptions[0]!.spawnCwd).not.toBe(second.runtimeOptions[0]!.spawnCwd);
// ...but the advertised session cwd — and thus the fingerprint — is identical.
expect(first.sessionInputs[0]!.cwd).toBe(remoteCwd);
expect(second.sessionInputs[0]!.cwd).toBe(remoteCwd);
const fp = (r: { result: { sessionParams?: unknown } }) =>
(r.result.sessionParams as { configFingerprint?: string } | undefined)?.configFingerprint;
expect(fp(first)).toBeDefined();
expect(fp(second)).toBe(fp(first));
});
it("routes child stderr in-process while keeping the unfiltered run log", async () => {
const root = await makeTempRoot();
const stateDir = path.join(root, "state");

View File

@ -318,6 +318,13 @@ interface AcpxPreparedRuntime {
acpxAgent: string;
mode: "persistent" | "oneshot";
cwd: string;
// Host-only spawn cwd for the acpx runtime's host `spawn()` of the relay
// proxy on the remote process-session lane. On that lane `cwd` is the
// IN-SANDBOX `remoteCwd` (host-nonexistent), so the host proxy must `chdir`
// into a HOST-valid dir instead — the engine's host `cwd`. `undefined` on
// every other lane, where acpx falls back to `cwd` (byte-identical). It is
// deliberately NOT part of the session fingerprint / compat key.
hostSpawnCwd: string | undefined;
workspaceId: string;
workspaceRepoUrl: string;
workspaceRepoRef: string;
@ -1720,6 +1727,11 @@ async function buildRuntime(input: {
// → the HOST cwd (`sessionCwd` resolves both). Every cwd-keyed session site
// reads `prepared.cwd`, so binding it once here keeps them consistent.
cwd: sessionCwd,
// Only the remote process-session lane needs the host proxy's `spawn()`
// `chdir` redirected off the in-sandbox `sessionCwd` and onto the host
// `cwd` (which is where the workspace was staged FROM, so it is host-valid).
// Every other lane leaves it `undefined` → acpx falls back to `cwd`.
hostSpawnCwd: useRemoteProcessSession ? cwd : undefined,
workspaceId,
workspaceRepoUrl,
workspaceRepoRef,
@ -2510,6 +2522,12 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
childStderrState.logPath = prepared.childStderrLogPath;
const runtimeOptions: AcpRuntimeOptions = {
cwd: prepared.cwd,
// Host-only spawn cwd for the relay proxy on the remote process-session
// lane; `undefined` elsewhere so acpx falls back to `cwd` (byte-identical).
// The advertised `session/new` cwd (`prepared.cwd` = `remoteCwd`) and the
// fingerprint / compat key are unaffected — this redirects ONLY the host
// `spawn()` `chdir`, not the in-sandbox data path.
spawnCwd: prepared.hostSpawnCwd,
sessionStore: createRuntimeStore({ stateDir: prepared.stateDir }),
agentRegistry: prepared.agentRegistry,
permissionMode: prepared.permissionMode,

View File

@ -0,0 +1,126 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, expect, it } from "vitest";
import type { AcpRuntimeOptions } from "acpx/runtime";
import { createAcpRuntime, createAgentRegistry, createRuntimeStore } from "acpx/runtime";
// Load-bearing repro for the remote-ACP "process session" lane host-spawn bug.
//
// The engine threads ONE cwd (`sessionCwd`) into every cwd-keyed site: the ACP
// `session/new` cwd, the session fingerprint/compat key, AND the acpx HOST
// `spawn()` of the host-local relay proxy. On the remote lane that value is the
// IN-SANDBOX `remoteCwd`, which does not exist on the host, so libuv's pre-`exec`
// `chdir` fails and acpx raises `AgentSpawnError` at `ensure_session`.
//
// A faithful full-engine remote-lane repro is NOT possible without a live
// sandbox: the only local stand-in for a sandbox runs its commands as host child
// processes, so every `remoteCwd`-derived operation (workspace staging, the
// callback bridge, the process-session bridge) executes on the HOST filesystem —
// either materializing `remoteCwd` on the host (masking the host-spawn ENOENT)
// or failing earlier at a different phase. So we split the proof at its two real
// seams: (1) here — the acpx runtime's REAL host `spawn()` honoring
// `spawnCwd ?? cwd` (real `createAcpRuntime`, real libuv `chdir`); and (2) the
// engine → `runtimeOptions` threading (`execute.test.ts`, which asserts the
// engine sets `spawnCwd` host-valid on the remote lane and `undefined`
// elsewhere, with the advertised `session/new` cwd staying `remoteCwd`).
// End-to-end validation against a real sandbox is board-run.
const repoRoot = fileURLToPath(new URL("../../../..", import.meta.url));
// A dedicated ACP agent fixture that reports, on stderr, the working directory
// the host actually spawned it in (`SPAWN_CWD`) and the `cwd` advertised on
// `session/new` (`SESSION_NEW_CWD`).
const fixturePath = path.join(repoRoot, "scripts", "mcp-fixtures", "servers", "acp-cwd-report-agent.mjs");
const tempRoots: string[] = [];
type PatchedAcpRuntimeOptions = AcpRuntimeOptions & {
spawnCwd?: string;
};
type PatchedEnsureSessionOptions = Parameters<ReturnType<typeof createAcpRuntime>["ensureSession"]>[0];
afterEach(async () => {
await Promise.all(tempRoots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
});
async function makeTempDir(prefix: string): Promise<string> {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
tempRoots.push(dir);
return dir;
}
/**
* Drive a REAL acpx runtime through `ensureSession`, which performs the real
* host `spawn()` (and its libuv `chdir`). `cwd` is the advertised session cwd;
* `spawnCwd`, when set, is the host-only spawn cwd the acpx patch consumes as
* `spawnCwd ?? cwd`.
*/
async function ensureRealAcpSession(input: { cwd: string; spawnCwd?: string }) {
const stateRoot = await makeTempDir("paperclip-acpx-remote-spawn-state-");
const stderrChunks: string[] = [];
const agentCommand = `${JSON.stringify(process.execPath.replaceAll("\\", "/"))} ${JSON.stringify(fixturePath.replaceAll("\\", "/"))}`;
const runtimeOptions: PatchedAcpRuntimeOptions = {
cwd: input.cwd,
// `spawnCwd` is the host-only knob added by patches/acpx@0.12.0.patch; when
// unset acpx falls back to `cwd`, so every non-proxy lane is byte-identical.
...(input.spawnCwd ? { spawnCwd: input.spawnCwd } : {}),
sessionStore: createRuntimeStore({ stateDir: path.join(stateRoot, "state") }),
agentRegistry: createAgentRegistry({ overrides: { custom: agentCommand } }),
permissionMode: "approve-all",
nonInteractivePermissions: "deny",
onAgentStderr: (chunk: string) => stderrChunks.push(chunk),
};
const runtime = createAcpRuntime(runtimeOptions);
try {
const sessionInput: PatchedEnsureSessionOptions = {
sessionKey: "remote-spawn-smoke",
agent: "custom",
mode: "oneshot",
cwd: input.cwd,
sessionOptions: { env: {} },
};
const handle = await runtime.ensureSession(sessionInput);
await (runtime as { close: (i: unknown) => Promise<void> }).close({ handle, reason: "done" }).catch(() => {});
return { resolved: true as const, stderr: stderrChunks.join("") };
} catch (err) {
return { resolved: false as const, error: err as NodeJS.ErrnoException & { cause?: NodeJS.ErrnoException }, stderr: stderrChunks.join("") };
}
}
it("reproduces host-spawn ENOENT when the advertised session cwd is host-nonexistent", async () => {
// The in-sandbox `remoteCwd` that does not exist on the host. Intentionally
// NOT created: this is what trips the acpx host `spawn()` `chdir`.
const sandboxParent = await makeTempDir("paperclip-acpx-remote-spawn-sandbox-");
const remoteCwd = path.join(sandboxParent, "does-not-exist-on-host", "workspace");
const outcome = await ensureRealAcpSession({ cwd: remoteCwd });
// Before the fix the engine feeds `remoteCwd` as the host spawn cwd, so acpx's
// real `spawn()` `chdir`s into a host-nonexistent dir and fails ENOENT. This is
// the exact `ensure_session` failure the remote lane hits in production.
expect(outcome.resolved, JSON.stringify(outcome)).toBe(false);
if (outcome.resolved) return;
expect(outcome.error.name).toBe("AgentSpawnError");
expect(outcome.error.cause?.code).toBe("ENOENT");
});
it("spawnCwd redirects the host spawn to a host-valid dir while the advertised session cwd stays remoteCwd", async () => {
const sandboxParent = await makeTempDir("paperclip-acpx-remote-spawn-sandbox-");
// Host-nonexistent in-sandbox cwd — the advertised `session/new` cwd.
const remoteCwd = path.join(sandboxParent, "does-not-exist-on-host", "workspace");
// Host-valid dir the proxy actually spawns in (the engine's host `cwd`).
const hostSpawnCwd = await makeTempDir("paperclip-acpx-remote-spawn-host-");
const outcome = await ensureRealAcpSession({ cwd: remoteCwd, spawnCwd: hostSpawnCwd });
// With `spawnCwd` set the host `spawn()` `chdir`s into the host-valid dir, so
// the session comes up instead of failing at `ensure_session`.
expect(outcome.resolved, JSON.stringify(outcome)).toBe(true);
// The host process really ran in `spawnCwd`...
expect(outcome.stderr).toContain(`SPAWN_CWD=${await fs.realpath(hostSpawnCwd)}`);
// ...while the in-sandbox data path (the advertised `session/new` cwd) is
// unchanged — still `remoteCwd`.
expect(outcome.stderr).toContain(`SESSION_NEW_CWD=${remoteCwd}`);
});

View File

@ -1,3 +1,4 @@
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@ -44,3 +45,31 @@ it("spawns a real Node ACP agent with per-session env on this platform", async (
expect(stderr).toContain("nes/close");
expect(stderr).toContain("paperclip-acp-echo-agent started");
});
it("captures the Node error shape for a host-invalid spawn cwd", async () => {
// Regression anchor for the primitive behind the remote-lane bug: a host
// `spawn()` whose `cwd` does not exist fails BEFORE `exec`, when libuv
// `chdir`s into it. The command itself (`process.execPath`) is valid, so the
// failure is unambiguously the missing cwd — the exact condition acpx hits
// when it host-spawns the relay proxy with the in-sandbox `remoteCwd`.
const missingCwd = path.join(os.tmpdir(), "paperclip-acpx-missing-spawn-cwd", "nested", "does-not-exist");
const err = await new Promise<NodeJS.ErrnoException>((resolve, reject) => {
const child = spawn(process.execPath, ["-e", "0"], {
cwd: missingCwd,
stdio: ["pipe", "pipe", "pipe"],
});
child.once("error", resolve);
child.once("spawn", () => {
child.kill("SIGKILL");
reject(new Error("expected spawn to fail with a host-invalid cwd, but it started"));
});
});
expect(err.code).toBe("ENOENT");
// libuv attributes the failed pre-`exec` `chdir` to the command spawn, not to
// the missing cwd — `syscall`/`path` point at the executable. This misdirection
// is precisely why the remote-lane failure was hard to diagnose.
expect(err.syscall).toBe(`spawn ${process.execPath}`);
expect(err.path).toBe(process.execPath);
});

View File

@ -1,38 +1,8 @@
--- a/dist/runtime.d.ts
+++ b/dist/runtime.d.ts
@@ -266,6 +266,7 @@
timeoutMs?: number;
probeAgent?: string;
verbose?: boolean;
+ onAgentStderr?: (chunk: string) => void;
onPermissionRequest?: (req: AcpPermissionRequest, ctx: {
signal: AbortSignal;
}) => Promise<AcpPermissionDecision | undefined>;
--- a/dist/session-options-jkYbBxGE.d.ts
+++ b/dist/session-options-jkYbBxGE.d.ts
@@ -84,6 +84,7 @@
terminal?: boolean;
suppressSdkConsoleErrors?: boolean;
verbose?: boolean;
+ onAgentStderr?: (chunk: string) => void;
sessionOptions?: {
model?: string;
allowedTools?: string[];
--- a/dist/runtime.js
+++ b/dist/runtime.js
@@ -744,7 +744,8 @@
this.deps = deps;
}
createClient(options) {
- return this.deps.clientFactory?.(options) ?? new AcpClient(options);
+ const clientOptions = { ...options, onAgentStderr: this.options.onAgentStderr };
+ return this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
}
async readPendingPersistentClient(record, options) {
const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
diff --git a/dist/live-checkpoint-ClPCSdrW.js b/dist/live-checkpoint-ClPCSdrW.js
index 243c9d13bcba520923b63adfddad75cf2d94362d..336a1699b80b9e99416f9da4346ca73ee2ad1626 100644
--- a/dist/live-checkpoint-ClPCSdrW.js
+++ b/dist/live-checkpoint-ClPCSdrW.js
@@ -1532,7 +1532,7 @@
@@ -1532,7 +1532,7 @@ const ZED_TAG_KEYS = /* @__PURE__ */ new Set([
"RedactedThinking",
"ToolUse"
]);
@ -41,10 +11,16 @@
const OPAQUE_VALUE_PATHS = /* @__PURE__ */ new Set([
"agent_capabilities",
"messages.Agent.content.ToolUse.input",
@@ -2562,1 +2562,1 @@
@@ -2557,7 +2557,7 @@ function readCommandLineChar(state) {
escaping: false,
hasPart: true
};
- if (state.ch === "\\" && state.quote !== "'") return {
+ if (process.platform !== "win32" && state.ch === "\\" && state.quote !== "'") return {
@@ -3960,6 +3960,10 @@
current: state.current,
quote: state.quote,
escaping: true,
@@ -3960,6 +3960,10 @@ var AcpClient = class {
const startupStderr = [];
child.stderr.on("data", (chunk) => {
this.captureStartupStderr(startupStderr, chunk);
@ -55,3 +31,52 @@
if (!this.options.verbose) return;
process.stderr.write(chunk);
});
@@ -3994,7 +3998,7 @@ var AcpClient = class {
geminiAcp: isGeminiAcpCommand(spawnCommand, args),
copilotAcp: isCopilotAcpCommand(spawnCommand, args),
claudeAcp: isClaudeAcpCommand(spawnCommand, args),
- spawnOptions: buildAgentSpawnOptions(this.options.cwd, this.options.authCredentials, this.options.sessionOptions?.env)
+ spawnOptions: buildAgentSpawnOptions(this.options.spawnCwd ?? this.options.cwd, this.options.authCredentials, this.options.sessionOptions?.env)
};
}
logAgentLaunch(plan) {
diff --git a/dist/runtime.d.ts b/dist/runtime.d.ts
index ccdbe5b032521518022223733049b8b38793473b..3d4e04231e78efeecd8540735b08e1e43547ff2d 100644
--- a/dist/runtime.d.ts
+++ b/dist/runtime.d.ts
@@ -266,6 +266,8 @@ type AcpRuntimeOptions = {
timeoutMs?: number;
probeAgent?: string;
verbose?: boolean;
+ onAgentStderr?: (chunk: string) => void;
+ spawnCwd?: string;
onPermissionRequest?: (req: AcpPermissionRequest, ctx: {
signal: AbortSignal;
}) => Promise<AcpPermissionDecision | undefined>;
diff --git a/dist/runtime.js b/dist/runtime.js
index 6c9cc999e50a11c399c68b3a0f1b7af4bc2317c0..33b5054b2906502d1d4b512bfa259bd2ba5a9f05 100644
--- a/dist/runtime.js
+++ b/dist/runtime.js
@@ -744,7 +744,8 @@ var AcpRuntimeManager = class {
this.deps = deps;
}
createClient(options) {
- return this.deps.clientFactory?.(options) ?? new AcpClient(options);
+ const clientOptions = { ...options, onAgentStderr: this.options.onAgentStderr, spawnCwd: this.options.spawnCwd };
+ return this.deps.clientFactory?.(clientOptions) ?? new AcpClient(clientOptions);
}
async readPendingPersistentClient(record, options) {
const pendingClient = this.pendingPersistentClients.get(record.acpxRecordId);
diff --git a/dist/session-options-jkYbBxGE.d.ts b/dist/session-options-jkYbBxGE.d.ts
index 9d37f377fb6a0828e0d2bc5a48754f3aa71509a4..680bc080fc5d6ffd266ed1b27d3d5056add9d980 100644
--- a/dist/session-options-jkYbBxGE.d.ts
+++ b/dist/session-options-jkYbBxGE.d.ts
@@ -84,6 +84,8 @@ type AcpClientOptions = {
terminal?: boolean;
suppressSdkConsoleErrors?: boolean;
verbose?: boolean;
+ onAgentStderr?: (chunk: string) => void;
+ spawnCwd?: string;
sessionOptions?: {
model?: string;
allowedTools?: string[];

View File

@ -0,0 +1,59 @@
#!/usr/bin/env node
// A minimal ACP agent fixture that reports, on its stderr, the working
// directory the host actually spawned it in (`process.cwd()`) and the `cwd`
// advertised on the `session/new` request. Used by the remote-lane host-spawn
// smoke test to prove the `spawnCwd` decoupling: the host `spawn()` chdir is
// redirected to a host-valid dir while the advertised session cwd is unchanged.
import { randomUUID } from "node:crypto";
import { createInterface } from "node:readline";
function writeMessage(message) {
process.stdout.write(`${JSON.stringify(message)}\n`);
}
// Emit the real spawn cwd as early as possible so a consumer capturing stderr
// sees it even if the session never advances past initialize.
process.stderr.write(`SPAWN_CWD=${process.cwd()}\n`);
async function handleRequest(request) {
if (request.method === "initialize") {
process.stderr.write("paperclip-acp-cwd-report-agent started\n");
return {
protocolVersion: 1,
agentCapabilities: { loadSession: false, sessionCapabilities: { close: {} } },
agentInfo: { name: "paperclip-acp-cwd-report-agent", version: "1.0.0" },
};
}
if (request.method === "session/new") {
process.stderr.write(`SESSION_NEW_CWD=${request.params?.cwd ?? ""}\n`);
return { sessionId: randomUUID() };
}
if (request.method === "session/prompt") {
writeMessage({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId: request.params.sessionId,
update: { sessionUpdate: "agent_message_chunk", content: { type: "text", text: "ok" } },
},
});
return { stopReason: "end_turn" };
}
if (request.method === "session/close" || request.method === "session/set_mode" || request.method === "session/set_config_option") return {};
if (request.method === "session/cancel") return null;
throw new Error(`Unsupported ACP method: ${request.method}`);
}
const lines = createInterface({ input: process.stdin });
lines.on("line", async (line) => {
let request;
try {
request = JSON.parse(line);
const result = await handleRequest(request);
if (request.id !== undefined && result !== null) writeMessage({ jsonrpc: "2.0", id: request.id, result });
} catch (error) {
if (request?.id !== undefined) {
writeMessage({ jsonrpc: "2.0", id: request.id, error: { code: -32603, message: String(error?.message ?? error) } });
}
}
});