






















@@ -22,7 +22,14 @@ const OPENAI_LIVE_PROOF_MODEL = "openai/gpt-5.5";
2222const COMMAND_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_COMMAND_MS, 120000);
2323const READY_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_READY_MS, 120000);
2424const RPC_TIMEOUT_MS = readPositiveInt(process.env.OPENCLAW_SECRET_PROOF_RPC_MS, 15000);
25-const TEARDOWN_GRACE_MS = 5000;
25+const TEARDOWN_GRACE_MS = readPositiveInt(
26+process.env.OPENCLAW_SECRET_PROOF_TEARDOWN_GRACE_MS,
27+5000,
28+);
29+const OUTPUT_CAPTURE_LIMIT_BYTES = readPositiveInt(
30+process.env.OPENCLAW_SECRET_PROOF_OUTPUT_BYTES,
31+4 * 1024 * 1024,
32+);
2633const RESULTS_PATH =
2734process.env.OPENCLAW_SECRET_PROOF_RESULTS_PATH?.trim() ||
2835path.join(os.tmpdir(), `openclaw-secret-provider-e2e-results-${process.pid}.json`);
@@ -84,6 +91,44 @@ function scrub(text) {
8491.replace(/sk-[A-Za-z0-9_-]{20,}/gu, "<openai-key>");
8592}
869394+function createOutputCapture(label, options = {}) {
95+let output = "";
96+let bytes = 0;
97+let truncated = false;
98+let scanTail = "";
99+let leakedForbiddenValue = null;
100+const forbiddenValues = options.forbiddenValues ?? [];
101+const scanTailLength = Math.max(0, ...forbiddenValues.map((value) => value.length - 1));
102+return {
103+append(chunk) {
104+const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
105+if (forbiddenValues.length > 0) {
106+const scanText = `${scanTail}${buffer.toString("utf8")}`;
107+leakedForbiddenValue ??= forbiddenValues.find((value) => scanText.includes(value)) ?? null;
108+scanTail = scanTailLength > 0 ? scanText.slice(-scanTailLength) : "";
109+}
110+const remaining = Math.max(0, OUTPUT_CAPTURE_LIMIT_BYTES - bytes);
111+if (remaining > 0) {
112+const slice = buffer.subarray(0, remaining);
113+output += slice.toString("utf8");
114+bytes += slice.length;
115+}
116+if (buffer.length > remaining && !truncated) {
117+truncated = true;
118+output += `\n[secret-provider-proof] ${label} truncated after ${String(
119+ OUTPUT_CAPTURE_LIMIT_BYTES,
120+ )} bytes\n`;
121+}
122+},
123+text() {
124+return output;
125+},
126+leakedForbiddenValue() {
127+return leakedForbiddenValue;
128+},
129+};
130+}
131+87132function parseJsonOutput(stdout) {
88133const text = stdout.trim();
89134if (!text) {
@@ -183,31 +228,33 @@ function runCommand(command, args, options = {}) {
183228stdio: options.stdio ?? ["pipe", "pipe", "pipe"],
184229windowsVerbatimArguments: options.windowsVerbatimArguments,
185230});
186-let stdout = "";
187-let stderr = "";
231+const stdout = createOutputCapture("stdout");
232+const stderr = createOutputCapture("stderr");
188233const timer = setTimeout(() => {
189234child.kill("SIGTERM");
190235setTimeout(() => child.kill("SIGKILL"), 1000).unref();
191236reject(new Error(scrub(`command timed out: ${command} ${args.join(" ")}`)));
192237}, timeoutMs);
193238child.stdout.on("data", (chunk) => {
194-stdout += chunk.toString("utf8");
239+stdout.append(chunk);
195240});
196241child.stderr.on("data", (chunk) => {
197-stderr += chunk.toString("utf8");
242+stderr.append(chunk);
198243});
199244child.on("error", (error) => {
200245clearTimeout(timer);
201-reject(error);
246+reject(error instanceof Error ? error : new Error(formatErrorMessage(error)));
202247});
203248child.on("close", (code, signal) => {
204249clearTimeout(timer);
205-const result = { code: code ?? 0, signal, stdout, stderr };
250+const result = { code: code ?? 0, signal, stdout: stdout.text(), stderr: stderr.text() };
206251if (result.code !== 0 && options.allowFailure !== true) {
207252reject(
208253new Error(
209254scrub(
210-`command failed (${result.code}): ${command} ${args.join(" ")}\n${stderr || stdout}`,
255+`command failed (${result.code}): ${command} ${args.join(" ")}\n${
256+ result.stderr || result.stdout
257+ }`,
211258),
212259),
213260);
@@ -271,7 +318,7 @@ async function allocatePort() {
271318const address = server.address();
272319const port = typeof address === "object" && address ? address.port : 0;
273320await new Promise((resolve, reject) => {
274-server.close((error) => (error ? reject(error) : resolve()));
321+server.close((error) => (error ? reject(new Error(formatErrorMessage(error))) : resolve()));
275322});
276323if (!port) {
277324throw new Error("failed to allocate a local port");
@@ -568,21 +615,23 @@ async function startGateway(envCtx, port, token = TOKEN_V1) {
568615detached: process.platform !== "win32",
569616stdio: ["ignore", "pipe", "pipe"],
570617});
571-let stdout = "";
572-let stderr = "";
618+const stdout = createOutputCapture("gateway stdout");
619+const stderr = createOutputCapture("gateway stderr");
573620child.stdout.on("data", (chunk) => {
574-stdout += chunk.toString("utf8");
621+stdout.append(chunk);
575622});
576623child.stderr.on("data", (chunk) => {
577-stderr += chunk.toString("utf8");
624+stderr.append(chunk);
578625});
579626const started = Date.now();
580627let lastHealthResult;
581628let lastHealthError;
582629while (Date.now() - started < READY_TIMEOUT_MS) {
583630if (child.exitCode !== null) {
584631throw new Error(
585-scrub(`gateway exited during startup (${child.exitCode})\n${stderr || stdout}`),
632+scrub(
633+`gateway exited during startup (${child.exitCode})\n${stderr.text() || stdout.text()}`,
634+),
586635);
587636}
588637const remainingMs = remainingDeadlineMs(started, READY_TIMEOUT_MS);
@@ -602,7 +651,7 @@ async function startGateway(envCtx, port, token = TOKEN_V1) {
602651if (health.code === 0) {
603652return {
604653 child,
605-output: () => ({ stdout, stderr }),
654+output: () => ({ stdout: stdout.text(), stderr: stderr.text() }),
606655stop: async () => {
607656await stopGateway(child);
608657},
@@ -613,7 +662,7 @@ async function startGateway(envCtx, port, token = TOKEN_V1) {
613662}
614663await delay(Math.min(500, remainingDeadlineMs(started, READY_TIMEOUT_MS)));
615664}
616-terminateProcessTree(child, "SIGTERM");
665+await stopGateway(child);
617666const lastHealthOutput =
618667lastHealthError instanceof Error
619668 ? lastHealthError.message
@@ -622,22 +671,58 @@ async function startGateway(envCtx, port, token = TOKEN_V1) {
622671 : lastHealthResult
623672 ? lastHealthResult.stderr || lastHealthResult.stdout
624673 : "";
625-throw new Error(scrub(`gateway did not become ready\n${lastHealthOutput}\n${stderr || stdout}`));
674+throw new Error(
675+scrub(`gateway did not become ready\n${lastHealthOutput}\n${stderr.text() || stdout.text()}`),
676+);
626677}
627678628679async function stopGateway(child) {
629-if (!child || child.exitCode !== null) {
680+if (!child || !processTreeIsAlive(child)) {
630681return;
631682}
632683terminateProcessTree(child, "SIGTERM");
633684const started = Date.now();
634685while (Date.now() - started < TEARDOWN_GRACE_MS) {
635-if (child.exitCode !== null) {
686+if (!processTreeIsAlive(child)) {
636687return;
637688}
638689await delay(100);
639690}
640691terminateProcessTree(child, "SIGKILL");
692+await waitForProcessTreeExit(child, 1000);
693+}
694+695+function childHasExited(child) {
696+return child.exitCode !== null || child.signalCode !== null;
697+}
698+699+function processTreeIsAlive(child) {
700+if (!child || typeof child.pid !== "number") {
701+return false;
702+}
703+if (process.platform === "win32") {
704+return !childHasExited(child);
705+}
706+try {
707+process.kill(-child.pid, 0);
708+return true;
709+} catch (error) {
710+if (error && error.code === "EPERM") {
711+return true;
712+}
713+return false;
714+}
715+}
716+717+async function waitForProcessTreeExit(child, timeoutMs) {
718+const started = Date.now();
719+while (Date.now() - started < timeoutMs) {
720+if (!processTreeIsAlive(child)) {
721+return true;
722+}
723+await delay(50);
724+}
725+return !processTreeIsAlive(child);
641726}
642727643728function terminateProcessTree(child, signal) {
@@ -729,30 +814,51 @@ async function expectGatewayStartupFails(envCtx, port, reason) {
729814detached: process.platform !== "win32",
730815stdio: ["ignore", "pipe", "pipe"],
731816});
732-let stdout = "";
733-let stderr = "";
817+const forbiddenStartupSecrets = [TOKEN_V1, TOKEN_V2, PLUGIN_EXEC_TOKEN];
818+const stdout = createOutputCapture("startup stdout", {
819+forbiddenValues: forbiddenStartupSecrets,
820+});
821+const stderr = createOutputCapture("startup stderr", {
822+forbiddenValues: forbiddenStartupSecrets,
823+});
734824child.stdout.on("data", (chunk) => {
735-stdout += chunk.toString("utf8");
825+stdout.append(chunk);
736826});
737827child.stderr.on("data", (chunk) => {
738-stderr += chunk.toString("utf8");
828+stderr.append(chunk);
739829});
740830const code = await new Promise((resolve, reject) => {
831+let timedOut = false;
741832const timer = setTimeout(() => {
742-terminateProcessTree(child, "SIGTERM");
743-reject(new Error(`gateway did not fail closed for ${reason}`));
833+timedOut = true;
834+void stopGateway(child).then(() => {
835+reject(new Error(`gateway did not fail closed for ${reason}`));
836+}, reject);
744837}, 20000);
745838child.on("close", (exitCode) => {
839+if (timedOut) {
840+return;
841+}
746842clearTimeout(timer);
747843resolve(exitCode);
748844});
749-child.on("error", reject);
845+child.on("error", (error) => {
846+if (timedOut) {
847+return;
848+}
849+clearTimeout(timer);
850+reject(error instanceof Error ? error : new Error(formatErrorMessage(error)));
851+});
750852});
751853if (code === 0) {
752854throw new Error(`gateway unexpectedly started for ${reason}`);
753855}
754-const rawCombined = `${stdout}\n${stderr}`;
755-for (const forbidden of [TOKEN_V1, TOKEN_V2, PLUGIN_EXEC_TOKEN]) {
856+const rawCombined = `${stdout.text()}\n${stderr.text()}`;
857+const streamedLeak = stdout.leakedForbiddenValue() ?? stderr.leakedForbiddenValue();
858+if (streamedLeak) {
859+throw new Error(`startup failure for ${reason} leaked a secret value`);
860+}
861+for (const forbidden of forbiddenStartupSecrets) {
756862if (rawCombined.includes(forbidden)) {
757863throw new Error(`startup failure for ${reason} leaked a secret value`);
758864}
@@ -1586,7 +1692,13 @@ async function main() {
15861692}
15871693}
158816941589-export { gatewayCall, runCommand, startGateway, waitForManagedGatewayStatus };
1695+export {
1696+expectGatewayStartupFails,
1697+gatewayCall,
1698+runCommand,
1699+startGateway,
1700+waitForManagedGatewayStatus,
1701+};
1590170215911703if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
15921704await main();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。