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