Files
openclaw/scripts/run-additional-boundary-checks.mjs
Jesse Merhi d5b0083300 fix: proxy direct APNs HTTP2 sessions (#74905)
Summary:
- This PR routes direct APNs HTTP/2 sends through an APNs allowlisted managed-proxy CONNECT wrapper, adds APNs proxy validation/docs/guardrails, and expands regression and live-test coverage.
- Reproducibility: yes. source-reproducible: current main `sendApnsRequest()` still uses raw `http2.connect(au ... nly covers HTTP/global-agent/Undici hooks. I did not run a live APNs reproduction in this read-only review.

Automerge notes:
- PR branch already contained follow-up commit before automerge: test: guard raw HTTP2 APNs connections
- PR branch already contained follow-up commit before automerge: test: guard raw HTTP2 with OpenGrep
- PR branch already contained follow-up commit before automerge: lint: ban raw HTTP2 imports
- PR branch already contained follow-up commit before automerge: fix: use managed proxy state for APNs
- PR branch already contained follow-up commit before automerge: test: exercise APNs active proxy state
- PR branch already contained follow-up commit before automerge: fix: reject conflicting managed proxy activation

Validation:
- ClawSweeper review passed for head dab7c86a75.
- Required merge gates passed before the squash merge.

Prepared head SHA: dab7c86a75
Review: https://github.com/openclaw/openclaw/pull/74905#issuecomment-4350181159

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
2026-05-04 11:04:17 +00:00

247 lines
7.5 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { performance } from "node:perf_hooks";
export const BOUNDARY_CHECKS = [
["prompt:snapshots:check", "pnpm", ["prompt:snapshots:check"]],
["plugin-extension-boundary", "pnpm", ["run", "lint:plugins:no-extension-imports"]],
["lint:tmp:no-random-messaging", "pnpm", ["run", "lint:tmp:no-random-messaging"]],
["lint:tmp:channel-agnostic-boundaries", "pnpm", ["run", "lint:tmp:channel-agnostic-boundaries"]],
["lint:tmp:tsgo-core-boundary", "pnpm", ["run", "lint:tmp:tsgo-core-boundary"]],
["lint:tmp:no-raw-channel-fetch", "pnpm", ["run", "lint:tmp:no-raw-channel-fetch"]],
["lint:tmp:no-raw-http2-imports", "pnpm", ["run", "lint:tmp:no-raw-http2-imports"]],
["lint:agent:ingress-owner", "pnpm", ["run", "lint:agent:ingress-owner"]],
[
"lint:plugins:no-register-http-handler",
"pnpm",
["run", "lint:plugins:no-register-http-handler"],
],
[
"lint:plugins:no-monolithic-plugin-sdk-entry-imports",
"pnpm",
["run", "lint:plugins:no-monolithic-plugin-sdk-entry-imports"],
],
[
"lint:plugins:no-extension-src-imports",
"pnpm",
["run", "lint:plugins:no-extension-src-imports"],
],
[
"lint:plugins:no-extension-test-core-imports",
"pnpm",
["run", "lint:plugins:no-extension-test-core-imports"],
],
[
"lint:plugins:plugin-sdk-subpaths-exported",
"pnpm",
["run", "lint:plugins:plugin-sdk-subpaths-exported"],
],
["deps:root-ownership:check", "pnpm", ["deps:root-ownership:check"]],
["web-search-provider-boundary", "pnpm", ["run", "lint:web-search-provider-boundaries"]],
["web-fetch-provider-boundary", "pnpm", ["run", "lint:web-fetch-provider-boundaries"]],
[
"extension-src-outside-plugin-sdk-boundary",
"pnpm",
["run", "lint:extensions:no-src-outside-plugin-sdk"],
],
[
"extension-plugin-sdk-internal-boundary",
"pnpm",
["run", "lint:extensions:no-plugin-sdk-internal"],
],
[
"extension-relative-outside-package-boundary",
"pnpm",
["run", "lint:extensions:no-relative-outside-package"],
],
["lint:ui:no-raw-window-open", "pnpm", ["lint:ui:no-raw-window-open"]],
].map(([label, command, args]) => ({ label, command, args }));
export function resolveConcurrency(value, fallback = 4) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (!Number.isFinite(parsed) || parsed < 1) {
return fallback;
}
return parsed;
}
export function parseShardSpec(value) {
if (!value) {
return null;
}
const match = String(value).match(/^(\d+)\/(\d+)$/u);
if (!match) {
throw new Error(`Invalid shard spec '${value}' (expected N/TOTAL)`);
}
const index = Number.parseInt(match[1], 10);
const count = Number.parseInt(match[2], 10);
if (
!Number.isInteger(index) ||
!Number.isInteger(count) ||
index < 1 ||
count < 1 ||
index > count
) {
throw new Error(`Invalid shard spec '${value}' (expected 1 <= N <= TOTAL)`);
}
return { count, index: index - 1, label: `${index}/${count}` };
}
export function selectChecksForShard(checks, shardSpec) {
const shard = typeof shardSpec === "string" ? parseShardSpec(shardSpec) : shardSpec;
if (!shard) {
return checks;
}
return checks.filter((_check, index) => index % shard.count === shard.index);
}
export function formatCommand({ command, args }) {
return [command, ...args].join(" ");
}
function runSingleCheck(check, { cwd, env }) {
return new Promise((resolve) => {
const startedAt = performance.now();
const child = spawn(check.command, check.args, {
cwd,
env,
shell: false,
stdio: ["ignore", "pipe", "pipe"],
});
const chunks = [];
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => chunks.push(chunk));
child.stderr.on("data", (chunk) => chunks.push(chunk));
child.on("error", (error) => {
chunks.push(`${error.stack ?? error.message}\n`);
resolve({
check,
code: 1,
durationMs: Math.round(performance.now() - startedAt),
signal: null,
output: chunks.join(""),
});
});
child.on("close", (code, signal) => {
resolve({
check,
code: code ?? 1,
durationMs: Math.round(performance.now() - startedAt),
signal,
output: chunks.join(""),
});
});
});
}
function formatDuration(ms) {
if (!Number.isFinite(ms)) {
return "";
}
if (ms < 1000) {
return `${ms}ms`;
}
return `${(ms / 1000).toFixed(1)}s`;
}
function writeGroupedResult(result, output) {
const success = result.code === 0;
output.write(`::group::${result.check.label}\n`);
output.write(`$ ${formatCommand(result.check)}\n`);
if (result.output) {
output.write(result.output.endsWith("\n") ? result.output : `${result.output}\n`);
}
if (success) {
output.write(`[ok] ${result.check.label} in ${formatDuration(result.durationMs)}\n`);
} else {
const suffix = result.signal ? ` (signal ${result.signal})` : ` (exit ${result.code})`;
output.write(
`::error title=${result.check.label} failed::${result.check.label} failed${suffix} after ${formatDuration(result.durationMs)}\n`,
);
}
output.write("::endgroup::\n");
}
function writeTimingSummary(results, output) {
output.write("Additional boundary check timings:\n");
for (const result of [...results].toSorted((left, right) => right.durationMs - left.durationMs)) {
output.write(
`${result.check.label.padEnd(48)} ${formatDuration(result.durationMs).padStart(8)}\n`,
);
}
}
export async function runChecks(
checks = BOUNDARY_CHECKS,
{ concurrency = 4, cwd = process.cwd(), env = process.env, output = process.stdout } = {},
) {
const results = Array.from({ length: checks.length });
let nextIndex = 0;
let active = 0;
await new Promise((resolve) => {
const launch = () => {
if (nextIndex >= checks.length && active === 0) {
resolve();
return;
}
while (active < concurrency && nextIndex < checks.length) {
const index = nextIndex;
const check = checks[nextIndex++];
active += 1;
void runSingleCheck(check, { cwd, env })
.then((result) => {
results[index] = result;
})
.finally(() => {
active -= 1;
launch();
});
}
};
launch();
});
let failures = 0;
for (const result of results) {
writeGroupedResult(result, output);
if (result.code !== 0) {
failures += 1;
}
}
writeTimingSummary(results, output);
return failures;
}
function resolveCliShardSpec(args, env) {
const shardIndex = args.indexOf("--shard");
if (shardIndex !== -1) {
return args[shardIndex + 1] ?? "";
}
const inlineShard = args.find((arg) => arg.startsWith("--shard="));
if (inlineShard) {
return inlineShard.slice("--shard=".length);
}
return env.OPENCLAW_ADDITIONAL_BOUNDARY_SHARD ?? "";
}
if (import.meta.url === `file://${process.argv[1]}`) {
const concurrency = resolveConcurrency(
process.env.OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY ??
process.env.OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY,
);
const shard = parseShardSpec(resolveCliShardSpec(process.argv.slice(2), process.env));
const checks = selectChecksForShard(BOUNDARY_CHECKS, shard);
if (shard) {
process.stdout.write(
`Running ${checks.length}/${BOUNDARY_CHECKS.length} additional boundary checks (shard ${shard.label})\n`,
);
}
const failures = await runChecks(checks, { concurrency });
process.exitCode = failures === 0 ? 0 : 1;
}