@paperclipai/adapter-utils 2026.1007.0-canary.13 → 2026.1007.0-canary.14

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.
@@ -940,6 +940,7 @@ describe("sandbox adapter execution targets", () => {
940
940
  // ACP keeps stdin open while it waits for a handshake. A remote child can
941
941
  // exit before replying; that must close the proxy and fail the handshake.
942
942
  const output = "final output\n".repeat(16_384);
943
+ const logs = [];
943
944
  const bridge = await startAdapterExecutionTargetProcessSessionBridge({
944
945
  runId: "run-open-stdin",
945
946
  target: {
@@ -957,11 +958,17 @@ describe("sandbox adapter execution targets", () => {
957
958
  env: {},
958
959
  timeoutSec: 10,
959
960
  streamOutputViaSession,
961
+ onLog: async (stream, chunk) => { if (stream === "stderr")
962
+ logs.push(chunk); },
960
963
  });
961
964
  expect(bridge).not.toBeNull();
962
965
  try {
963
966
  const result = await runProxyWithInput(bridge.agentCommand, "initialize\n", true);
964
967
  expect(result.code).toBe(exitCode ?? 1);
968
+ const terminal = logs.filter((line) => line.includes("ACP process session terminal:"));
969
+ expect(terminal).toEqual([
970
+ `[paperclip] ACP process session terminal: source=remote_event outcome=${exitCode === null ? "error" : "exit"} exitCode=${exitCode ?? "unknown"} signal=unknown.\n`,
971
+ ]);
965
972
  if (exitCode === null) {
966
973
  expect(result.stdout).toBe("");
967
974
  expect(result.stderr).toContain("ENOENT");
@@ -975,6 +982,204 @@ describe("sandbox adapter execution targets", () => {
975
982
  await bridge?.stop();
976
983
  }
977
984
  }, 15_000);
985
+ it("flushes remote errors under backpressure and retains only bounded terminal evidence", async () => {
986
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-flush-"));
987
+ cleanupDirs.push(rootDir);
988
+ const delegate = createLocalSandboxRunner();
989
+ const logs = [];
990
+ let emit;
991
+ let complete;
992
+ let failInput;
993
+ let inputStarted = false;
994
+ const inputFailure = new Promise((_resolve, reject) => { failInput = reject; });
995
+ void inputFailure.catch(() => { });
996
+ const commandComplete = new Promise((resolve) => { complete = resolve; });
997
+ const frame = {
998
+ type: "error", message: "synthetic-private-detail".repeat(200_000),
999
+ code: 1e100, signal: "untrusted-private-signal",
1000
+ };
1001
+ const bridge = await startAdapterExecutionTargetProcessSessionBridge({
1002
+ runId: "terminal-flush", adapterKey: "acpx", runtimeRootDir: rootDir,
1003
+ target: {
1004
+ kind: "remote", transport: "sandbox", remoteCwd: rootDir,
1005
+ runner: { execute: async (input) => {
1006
+ if (!input.useSession) {
1007
+ const script = input.args?.[1] ?? "";
1008
+ if (script.startsWith("mkdir -p") && script.includes("/stdin/000000000001.json")) {
1009
+ inputStarted = true;
1010
+ return inputFailure;
1011
+ }
1012
+ return delegate.execute(input);
1013
+ }
1014
+ emit = async (text) => { await input.onLog?.("stdout", text); };
1015
+ await commandComplete;
1016
+ return { exitCode: 1, stdout: "", stderr: "", timedOut: false, signal: null, pid: null, startedAt: null };
1017
+ } },
1018
+ },
1019
+ command: "cat", args: [], cwd: rootDir, env: {}, streamOutputViaSession: true,
1020
+ onLog: async (stream, chunk) => { if (stream === "stderr")
1021
+ logs.push(chunk); },
1022
+ });
1023
+ let peer;
1024
+ try {
1025
+ const source = await readFile(bridge.agentCommand, "utf8");
1026
+ const port = Number(/port: (\d+)/.exec(source)[1]);
1027
+ const token = JSON.parse(/const token = (".*?");/.exec(source)[1]);
1028
+ peer = net.createConnection({ host: "127.0.0.1", port });
1029
+ peer.on("error", () => { });
1030
+ peer.setEncoding("utf8");
1031
+ let output = "";
1032
+ peer.on("data", (chunk) => { output += chunk; });
1033
+ const closed = new Promise((resolve) => peer.once("close", resolve));
1034
+ await new Promise((resolve) => peer.once("connect", resolve));
1035
+ peer.write(JSON.stringify({ token, type: "hello" }) + "\n");
1036
+ // Confirm authentication by receiving a marker before applying backpressure.
1037
+ await emit(JSON.stringify({ type: "data", stream: "stdout", data: "" }) + "\n");
1038
+ await waitForCondition(() => output.length > 0, "Missing authenticated marker.");
1039
+ output = "";
1040
+ peer.write(JSON.stringify({ token, type: "stdin", data: "aW5wdXQ=" }) + "\n");
1041
+ await waitForCondition(() => inputStarted, "Input write did not start.");
1042
+ peer.pause();
1043
+ // The real wrapper emits error then exit when a child fails to spawn.
1044
+ // A later frame must not write after end() and abort the pending error.
1045
+ await emit([
1046
+ frame,
1047
+ { type: "exit", code: 1 },
1048
+ { type: "data", stream: "stdout", data: "bGF0ZQ==" },
1049
+ { type: "shutdownAck" },
1050
+ ].map((event) => JSON.stringify(event) + "\n").join(""));
1051
+ // The reverse race matters too: an already-accepted input write can
1052
+ // reject after the remote error started draining. Its end(data) must
1053
+ // not abort or replace the first terminal frame.
1054
+ failInput(new Error("synthetic-private-input-detail"));
1055
+ await waitForCondition(() => logs.some((line) => line.includes("ACP process session input delivery failed.")), "Input failure was not handled.");
1056
+ peer.resume();
1057
+ await closed;
1058
+ expect(JSON.parse(output)).toEqual(frame);
1059
+ expect(logs.filter((line) => line.includes("ACP process session terminal:"))).toEqual([
1060
+ "[paperclip] ACP process session terminal: source=remote_event outcome=error exitCode=unknown signal=unknown.\n",
1061
+ ]);
1062
+ }
1063
+ finally {
1064
+ failInput(new Error("test cleanup"));
1065
+ complete();
1066
+ peer?.destroy();
1067
+ await bridge?.stop();
1068
+ }
1069
+ }, 15_000);
1070
+ it("reports a streamed transport failure even when diagnostic persistence stalls", async () => {
1071
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-stream-"));
1072
+ cleanupDirs.push(rootDir);
1073
+ const delegate = createLocalSandboxRunner();
1074
+ const logs = [];
1075
+ let releaseLog;
1076
+ const stalledLog = new Promise((resolve) => { releaseLog = resolve; });
1077
+ const bridge = await startAdapterExecutionTargetProcessSessionBridge({
1078
+ runId: "terminal-stream", adapterKey: "acpx", runtimeRootDir: rootDir,
1079
+ target: {
1080
+ kind: "remote", transport: "sandbox", remoteCwd: rootDir,
1081
+ runner: { execute: async (input) => {
1082
+ if (input.useSession)
1083
+ throw new Error("synthetic-private-provider-detail");
1084
+ return delegate.execute(input);
1085
+ } },
1086
+ },
1087
+ command: "cat", args: [], cwd: rootDir, env: {}, streamOutputViaSession: true,
1088
+ onLog: async (stream, chunk) => {
1089
+ if (stream === "stderr") {
1090
+ logs.push(chunk);
1091
+ await stalledLog;
1092
+ }
1093
+ },
1094
+ });
1095
+ try {
1096
+ const result = await runProxyWithInput(bridge.agentCommand, "", true);
1097
+ expect(result.code).toBe(1);
1098
+ expect(logs).toEqual([
1099
+ "[paperclip] ACP process session terminal: source=output_stream outcome=error exitCode=unknown signal=unknown.\n",
1100
+ ]);
1101
+ }
1102
+ finally {
1103
+ releaseLog();
1104
+ await bridge?.stop();
1105
+ }
1106
+ }, 10_000);
1107
+ it("closes a failed output poll without waiting for the run log", async () => {
1108
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-poll-"));
1109
+ cleanupDirs.push(rootDir);
1110
+ const delegate = createLocalSandboxRunner();
1111
+ const logs = [];
1112
+ let releaseLog;
1113
+ const stalledLog = new Promise((resolve) => { releaseLog = resolve; });
1114
+ const bridge = await startAdapterExecutionTargetProcessSessionBridge({
1115
+ runId: "terminal-poll", adapterKey: "acpx", runtimeRootDir: rootDir,
1116
+ target: {
1117
+ kind: "remote", transport: "sandbox", remoteCwd: rootDir,
1118
+ runner: { execute: async (input) => {
1119
+ const script = input.args?.[1] ?? "";
1120
+ if (script.includes("/events'/*.json"))
1121
+ throw new Error("synthetic-private-provider-detail");
1122
+ return delegate.execute(input);
1123
+ } },
1124
+ },
1125
+ command: "cat", args: [], cwd: rootDir, env: {},
1126
+ onLog: async (stream, chunk) => {
1127
+ if (stream === "stderr") {
1128
+ logs.push(chunk);
1129
+ await stalledLog;
1130
+ }
1131
+ },
1132
+ });
1133
+ try {
1134
+ const result = await runProxyWithInput(bridge.agentCommand, "", true);
1135
+ expect(result.code).toBe(1);
1136
+ expect(logs).toEqual([
1137
+ "[paperclip] ACP process session terminal: source=output_poll outcome=error exitCode=unknown signal=unknown.\n",
1138
+ ]);
1139
+ }
1140
+ finally {
1141
+ releaseLog();
1142
+ await bridge?.stop();
1143
+ }
1144
+ }, 10_000);
1145
+ it("records an unexpected authenticated proxy close once and ignores unauthenticated peers", async () => {
1146
+ const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-terminal-proxy-"));
1147
+ cleanupDirs.push(rootDir);
1148
+ const logs = [];
1149
+ const bridge = await startAdapterExecutionTargetProcessSessionBridge({
1150
+ runId: "terminal-proxy", adapterKey: "acpx", runtimeRootDir: rootDir,
1151
+ target: { kind: "remote", transport: "sandbox", remoteCwd: rootDir, runner: createLocalSandboxRunner() },
1152
+ command: "cat", args: [], cwd: rootDir, env: {},
1153
+ onLog: async (stream, chunk) => { if (stream === "stderr")
1154
+ logs.push(chunk); },
1155
+ });
1156
+ try {
1157
+ const source = await readFile(bridge.agentCommand, "utf8");
1158
+ const port = Number(/port: (\d+)/.exec(source)[1]);
1159
+ const token = JSON.parse(/const token = (".*?");/.exec(source)[1]);
1160
+ for (const authenticated of [false, true]) {
1161
+ const peer = net.createConnection({ host: "127.0.0.1", port });
1162
+ peer.on("error", () => { });
1163
+ const closed = new Promise((resolve) => peer.once("close", resolve));
1164
+ await new Promise((resolve) => peer.once("connect", resolve));
1165
+ if (authenticated)
1166
+ peer.end(JSON.stringify({ token, type: "hello" }) + "\n");
1167
+ else
1168
+ peer.end();
1169
+ await closed;
1170
+ if (!authenticated)
1171
+ expect(logs).toEqual([]);
1172
+ }
1173
+ await waitForCondition(() => logs.length === 1, "Missing proxy-close diagnostic.");
1174
+ await bridge.stop();
1175
+ expect(logs.filter((line) => line.includes("ACP process session terminal:"))).toEqual([
1176
+ "[paperclip] ACP process session terminal: source=proxy_close outcome=error exitCode=unknown signal=unknown.\n",
1177
+ ]);
1178
+ }
1179
+ finally {
1180
+ await bridge?.stop();
1181
+ }
1182
+ }, 10_000);
978
1183
  it("buffers sandbox process session output until the local proxy connects", async () => {
979
1184
  const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-process-session-buffer-"));
980
1185
  cleanupDirs.push(rootDir);