pi-agent-browser-native 0.9.6 → 0.9.8

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +108 -1
  2. package/README.md +14 -6
  3. package/dist/extensions/agent-browser/index.js +1276 -350
  4. package/dist/packaged/lib/browser-session-conversion.js +157 -21
  5. package/dist/packaged/lib/playbook.js +42 -8
  6. package/dist/packaged/lib/results/snapshot.js +140 -11
  7. package/dist/packaged/lib/runtime.js +58 -10
  8. package/docs/ARCHITECTURE.md +19 -10
  9. package/docs/CODE_QUALITY.md +21 -5
  10. package/docs/COMMAND_REFERENCE.md +154 -38
  11. package/docs/ELECTRON.md +1 -1
  12. package/docs/PERFORMANCE.md +12 -3
  13. package/docs/REFERENCE.md +14 -14
  14. package/docs/RELEASE.md +44 -19
  15. package/docs/REQUIREMENTS.md +6 -0
  16. package/docs/SESSION_STORAGE.md +3 -3
  17. package/docs/SUPPORT_MATRIX.md +25 -23
  18. package/docs/TOOL_CONTRACT.md +37 -14
  19. package/docs/platform-smoke.md +18 -14
  20. package/package.json +6 -3
  21. package/platform-smoke.config.mjs +3 -2
  22. package/scripts/agent-browser-capability-baseline.mjs +16 -2
  23. package/scripts/agent-browser-target.mjs +1 -1
  24. package/scripts/capability-options.mjs +20 -0
  25. package/scripts/capability-samples.mjs +23 -10
  26. package/scripts/platform-smoke/commands.mjs +30 -4
  27. package/scripts/platform-smoke/doctor-support.mjs +11 -2
  28. package/scripts/platform-smoke/doctor-windows.mjs +1 -1
  29. package/scripts/platform-smoke/doctor.mjs +26 -11
  30. package/scripts/platform-smoke/linux-image/Dockerfile +6 -3
  31. package/scripts/platform-smoke/platform-build-windows.ps1 +23 -3
  32. package/scripts/platform-smoke/suite-checks.mjs +11 -1
  33. package/scripts/platform-smoke/targets.mjs +4 -3
  34. package/scripts/platform-smoke.mjs +68 -25
@@ -1,9 +1,10 @@
1
1
  /** Audited upstream source evidence and the native help commands sampled for drift. */
2
2
  export const sourceEvidence = Object.freeze({
3
3
  repository: "vercel-labs/agent-browser",
4
- upstreamHead: "aff6125c023b810ea3f2e5deec5379e9a4270bdc",
5
- upstreamPackageVersion: "0.38.1",
4
+ upstreamHead: "1453dd37a40c730fe00f592bc32198a896ff8747",
5
+ upstreamPackageVersion: "0.41.0",
6
6
  inspectedSources: Object.freeze([
7
+ "stock npm agent-browser@0.41.0 tarball and skill-data/",
7
8
  "agent-browser --version",
8
9
  "agent-browser --help",
9
10
  "selected agent-browser <command> --help output",
@@ -20,19 +21,28 @@ export const sourceEvidence = Object.freeze({
20
21
  "bin/agent-browser.js",
21
22
  "cli/src/ca_bundle.rs",
22
23
  "cli/src/commands.rs",
24
+ "cli/src/connection.rs",
23
25
  "cli/src/mcp.rs",
24
26
  "cli/src/flags.rs",
27
+ "cli/src/main.rs",
28
+ "cli/src/session_files.rs",
29
+ "cli/src/plugins.rs",
25
30
  "cli/src/read.rs",
31
+ "cli/src/tls.rs",
26
32
  "cli/src/doctor/ffmpeg.rs",
27
33
  "cli/src/doctor/webgpu.rs",
28
34
  "cli/src/native/actions.rs",
29
35
  "cli/src/native/a11y/mod.rs",
30
36
  "cli/src/native/browser.rs",
37
+ "cli/src/native/cdp/obscura.rs",
38
+ "cli/src/native/policy.rs",
39
+ "cli/src/native/providers.rs",
31
40
  "cli/src/native/tab_binding.rs",
32
41
  "cli/src/native/daemon.rs",
33
42
  "cli/src/native/element.rs",
34
43
  "cli/src/native/recording.rs",
35
44
  "cli/src/native/snapshot.rs",
45
+ "cli/src/native/state.rs",
36
46
  "cli/src/native/interaction.rs",
37
47
  "cli/src/native/screenshot.rs",
38
48
  "cli/src/native/stream/cdp_loop.rs",
@@ -41,13 +51,17 @@ export const sourceEvidence = Object.freeze({
41
51
  "cli/src/native/test_fixtures/webmcp_probe.html",
42
52
  "cli/src/native/webmcp.rs",
43
53
  "cli/src/output.rs",
44
- "docs/src/app/webgpu/page.mdx",
45
- "docs/src/app/webmcp/page.mdx",
46
- "docs/src/app/network/page.mdx",
47
- "docs/src/app/proxy/page.mdx",
48
- "docs/src/app/selectors/page.mdx",
49
- "docs/src/app/skills/page.mdx",
50
- "docs/src/app/commands/page.mdx",
54
+ "docs/content/docs/webgpu.mdx",
55
+ "docs/content/docs/webmcp.mdx",
56
+ "docs/content/docs/network.mdx",
57
+ "docs/content/docs/proxy.mdx",
58
+ "docs/content/docs/selectors.mdx",
59
+ "docs/content/docs/skills.mdx",
60
+ "docs/content/docs/commands.mdx",
61
+ "docs/content/docs/engines/obscura.mdx",
62
+ "docs/content/docs/providers/browser-use.mdx",
63
+ "docs/content/docs/providers/kernel.mdx",
64
+ "cli/tests/tls_cli.rs",
51
65
  "skill-data/derive-client/SKILL.md",
52
66
  "skill-data/core/SKILL.md",
53
67
  "skill-data/core/references/video-recording.md",
@@ -60,7 +74,6 @@ export const sourceEvidence = Object.freeze({
60
74
  "packages/@agent-browser/sandbox/README.md",
61
75
  "packages/@agent-browser/sandbox/src/shared.ts",
62
76
  "packages/@agent-browser/sandbox/src/vercel.ts",
63
- "packages/@agent-browser/sandbox/src/eve.ts",
64
77
  ]),
65
78
  });
66
79
 
@@ -10,10 +10,19 @@ function psSingleQuote(value) {
10
10
  return `'${String(value).replace(/'/g, "''")}'`;
11
11
  }
12
12
 
13
- export function buildPlatformBuildCommand(targetName, packageName, nodeValidationVersion) {
13
+ export function buildPlatformBuildCommand(
14
+ targetName,
15
+ packageName,
16
+ nodeValidationVersion,
17
+ candidate,
18
+ ) {
14
19
  if (platformFor(targetName) === "powershell") {
15
- return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${psSingleQuote(packageName)} -NodeValidationVersion ${psSingleQuote(nodeValidationVersion)}`;
20
+ const candidateArgs = candidate
21
+ ? ` -CandidatePath ${psSingleQuote(candidate.path)} -CandidateSha256 ${psSingleQuote(candidate.sha256)}`
22
+ : "";
23
+ return `powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\platform-smoke\\platform-build-windows.ps1 -PackageName ${psSingleQuote(packageName)} -NodeValidationVersion ${psSingleQuote(nodeValidationVersion)}${candidateArgs}`;
16
24
  }
25
+ const candidateHash = `node -e 'process.stdout.write(require("node:crypto").createHash("sha256").update(require("node:fs").readFileSync(process.argv[1])).digest("hex"))' "$PACK_FILE"`;
17
26
  return [
18
27
  `echo "Starting platform-build in $(pwd) at $(date -u +%Y-%m-%dT%H:%M:%SZ)"`,
19
28
  `RUN_ROOT=".platform-smoke-runs/platform-build-$(date -u +%Y%m%dT%H%M%SZ)-$$"`,
@@ -37,11 +46,21 @@ export function buildPlatformBuildCommand(targetName, packageName, nodeValidatio
37
46
  `cat "$PACK_DIR/npm-pack.stderr.txt"`,
38
47
  `PACK_FILE="$PACK_DIR/$PACK_TARBALL"`,
39
48
  `echo "PLATFORM_NPM_PACK_EXIT=$PACK_EXIT"`,
49
+ ...(candidate
50
+ ? [
51
+ `CANDIDATE_EXIT=0`,
52
+ `CANDIDATE_SOURCE=${shellQuote(candidate.path)}`,
53
+ `PACK_FILE="$PACK_DIR/release-candidate.tgz"`,
54
+ `if cp "$SOURCE_ROOT/$CANDIDATE_SOURCE" "$PACK_FILE"; then CANDIDATE_BEFORE=$(${candidateHash}); else CANDIDATE_BEFORE=""; fi`,
55
+ `echo "PLATFORM_CANDIDATE_SHA256_BEFORE=$CANDIDATE_BEFORE"`,
56
+ `if [ "$CANDIDATE_BEFORE" != ${shellQuote(candidate.sha256)} ]; then CANDIDATE_EXIT=1; fi`,
57
+ ]
58
+ : []),
40
59
  `echo "PLATFORM_PACKED_TARBALL=$PACK_FILE"`,
41
60
  `PI_CLI="$SOURCE_ROOT/node_modules/.bin/pi"`,
42
61
  `if [ ! -x "$PI_CLI" ]; then PI_CLI="$(command -v pi || true)"; fi`,
43
62
  `echo "PLATFORM_PI_CLI=$PI_CLI"`,
44
- `if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_FILE" ]; then (cd "$PI_PROJECT" && npm init -y >"$PACK_DIR/packed-node-install.stdout.txt" 2>"$PACK_DIR/packed-node-install.stderr.txt" && npm install --no-save "$PACK_FILE" >>"$PACK_DIR/packed-node-install.stdout.txt" 2>>"$PACK_DIR/packed-node-install.stderr.txt"); PACKED_NODE_INSTALL_EXIT=$?; else echo "missing tarball" >"$PACK_DIR/packed-node-install.stderr.txt"; PACKED_NODE_INSTALL_EXIT=1; fi`,
63
+ `if [ -n "$PACK_TARBALL" ] && [ -f "$PACK_FILE" ]${candidate ? ' && [ "$CANDIDATE_EXIT" -eq 0 ]' : ""}; then (cd "$PI_PROJECT" && npm init -y >"$PACK_DIR/packed-node-install.stdout.txt" 2>"$PACK_DIR/packed-node-install.stderr.txt" && npm install --no-save "$PACK_FILE" >>"$PACK_DIR/packed-node-install.stdout.txt" 2>>"$PACK_DIR/packed-node-install.stderr.txt"); PACKED_NODE_INSTALL_EXIT=$?; else echo "missing or invalid tarball" >"$PACK_DIR/packed-node-install.stderr.txt"; PACKED_NODE_INSTALL_EXIT=1; fi`,
45
64
  `echo "PLATFORM_PACKED_NODE_INSTALL_EXIT=$PACKED_NODE_INSTALL_EXIT"`,
46
65
  `echo "--- PACKED_NODE_INSTALL_STDOUT START ---"; cat "$PACK_DIR/packed-node-install.stdout.txt" 2>/dev/null || true; echo "--- PACKED_NODE_INSTALL_STDOUT END ---"`,
47
66
  `echo "--- PACKED_NODE_INSTALL_STDERR START ---"; cat "$PACK_DIR/packed-node-install.stderr.txt" 2>/dev/null || true; echo "--- PACKED_NODE_INSTALL_STDERR END ---"`,
@@ -53,7 +72,14 @@ export function buildPlatformBuildCommand(targetName, packageName, nodeValidatio
53
72
  `echo "PLATFORM_PI_LIST_EXIT=$PI_LIST_EXIT"`,
54
73
  `echo "--- PI_LIST_STDOUT START ---"; cat "$PACK_DIR/pi-list.stdout.txt" 2>/dev/null || true; echo "--- PI_LIST_STDOUT END ---"`,
55
74
  `echo "--- PI_LIST_STDERR START ---"; cat "$PACK_DIR/pi-list.stderr.txt" 2>/dev/null || true; echo "--- PI_LIST_STDERR END ---"`,
56
- `if [ "$NODE_VERSION_EXIT" -ne 0 ] || [ "$NPM_CI_EXIT" -ne 0 ] || [ "$VERIFY_EXIT" -ne 0 ] || [ "$PACK_EXIT" -ne 0 ] || [ "$PACKED_NODE_INSTALL_EXIT" -ne 0 ] || [ "$PI_INSTALL_EXIT" -ne 0 ] || [ "$PI_LIST_EXIT" -ne 0 ]; then echo "PLATFORM_BUILD_FAILED"; exit 1; fi`,
75
+ ...(candidate
76
+ ? [
77
+ `CANDIDATE_AFTER=$(${candidateHash})`,
78
+ `echo "PLATFORM_CANDIDATE_SHA256_AFTER=$CANDIDATE_AFTER"`,
79
+ `if [ "$CANDIDATE_AFTER" != ${shellQuote(candidate.sha256)} ]; then CANDIDATE_EXIT=1; fi`,
80
+ ]
81
+ : []),
82
+ `if [ "$NODE_VERSION_EXIT" -ne 0 ] || [ "$NPM_CI_EXIT" -ne 0 ] || [ "$VERIFY_EXIT" -ne 0 ] || [ "$PACK_EXIT" -ne 0 ] || [ "$PACKED_NODE_INSTALL_EXIT" -ne 0 ] || [ "$PI_INSTALL_EXIT" -ne 0 ] || [ "$PI_LIST_EXIT" -ne 0 ]${candidate ? ' || [ "$CANDIDATE_EXIT" -ne 0 ]' : ""}; then echo "PLATFORM_BUILD_FAILED"; exit 1; fi`,
57
83
  `echo "PLATFORM_BUILD_OK"`,
58
84
  ].join("\n");
59
85
  }
@@ -85,12 +85,21 @@ function isForbiddenProjectPath(path) {
85
85
  );
86
86
  }
87
87
  function npmPackFiles() {
88
- const output = silent("npm", ["pack", "--dry-run", "--json"]);
88
+ const windows = process.platform === "win32";
89
+ const output = silent(windows ? "npm.cmd" : "npm", ["pack", "--dry-run", "--json"], {
90
+ timeout: 0,
91
+ shell: windows,
92
+ });
89
93
  if (!output) {
90
94
  return null;
91
95
  }
92
96
  try {
93
- return JSON.parse(output)[0]?.files?.map((file) => file.path) ?? [];
97
+ const parsed = JSON.parse(output);
98
+ const packages = Array.isArray(parsed) ? parsed : Object.values(parsed);
99
+ if (packages.length !== 1 || !Array.isArray(packages[0]?.files)) {
100
+ return null;
101
+ }
102
+ return packages[0].files.map((file) => file.path);
94
103
  } catch {
95
104
  return null;
96
105
  }
@@ -183,7 +183,7 @@ export function checkWindows({
183
183
  }) {
184
184
  console.log("\n── Windows native / Parallels ──");
185
185
  if (!(config?.requiredTargets ?? []).includes("windows-native")) {
186
- report.warn("windows-native is not listed in requiredTargets for this configuration");
186
+ report.warn("windows-native not selected (best-effort); setup not checked, not a pass");
187
187
  return;
188
188
  }
189
189
  if (!commandPath("prlctl")) {
@@ -51,13 +51,19 @@ function crabboxProviders(cbox) {
51
51
  .filter(Boolean);
52
52
  }
53
53
 
54
- function checkRequiredProviders(cbox, report) {
54
+ function checkRequiredProviders(cbox, config, report) {
55
55
  const providers = crabboxProviders(cbox);
56
56
  if (providers.length === 0) {
57
57
  report.fail("could not read crabbox providers");
58
58
  return;
59
59
  }
60
- for (const provider of ["ssh", "local-container", "parallels"]) {
60
+ const providersByTarget = {
61
+ macos: "ssh",
62
+ ubuntu: "local-container",
63
+ "windows-native": "parallels",
64
+ };
65
+ for (const target of config.requiredTargets) {
66
+ const provider = providersByTarget[target];
61
67
  if (providers.includes(provider)) {
62
68
  report.ok(`crabbox provider available: ${provider}`);
63
69
  } else {
@@ -163,13 +169,18 @@ function macSettings(config, packageName) {
163
169
  }
164
170
 
165
171
  function checkProviders(cbox, config, settings, report) {
166
- checkRequiredProviders(cbox, report);
167
- checkCrabboxProvider(
168
- cbox,
169
- ["--provider", "local-container", "--local-container-image", settings.ubuntuImage],
170
- "ubuntu local-container",
171
- report,
172
- );
172
+ checkRequiredProviders(cbox, config, report);
173
+ if (config.requiredTargets.includes("ubuntu")) {
174
+ checkCrabboxProvider(
175
+ cbox,
176
+ ["--provider", "local-container", "--local-container-image", settings.ubuntuImage],
177
+ "ubuntu local-container",
178
+ report,
179
+ );
180
+ }
181
+ if (!config.requiredTargets.includes("macos")) {
182
+ return;
183
+ }
173
184
  const mac = macSettings(config, settings.packageName);
174
185
  checkCrabboxProvider(
175
186
  cbox,
@@ -251,8 +262,12 @@ export async function runDoctor(config) {
251
262
  if (cboxPath) {
252
263
  checkProviders(cbox, config, { packageName, ubuntuImage }, report);
253
264
  }
254
- checkDocker(ubuntuImage, report);
255
- checkMacSsh(config, packageName, report);
265
+ if (config.requiredTargets.includes("ubuntu")) {
266
+ checkDocker(ubuntuImage, report);
267
+ }
268
+ if (config.requiredTargets.includes("macos")) {
269
+ checkMacSsh(config, packageName, report);
270
+ }
256
271
  checkWindows({
257
272
  cbox,
258
273
  config,
@@ -1,6 +1,6 @@
1
1
  # Local Crabbox Ubuntu/Linux target image for pi-agent-browser-native platform smoke.
2
- # Build with npm run smoke:platform:ubuntu-image so the agent-browser version
3
- # comes from scripts/agent-browser-capability-baseline.mjs. The Go toolchain
2
+ # Build with npm run smoke:platform:ubuntu-image to check the latest stock
3
+ # agent-browser against the audited capability baseline. The Go toolchain
4
4
  # matches the version scripts/prepare-quality-checker.mjs selects for the
5
5
  # corrected quality checker that platform-target builds.
6
6
 
@@ -15,7 +15,10 @@ COPY --from=go /usr/local/go /usr/local/go
15
15
  RUN ln -s /usr/local/go/bin/go /usr/local/go/bin/gofmt /usr/local/bin/ \
16
16
  && apt-get update \
17
17
  && apt-get install -y --no-install-recommends ca-certificates chromium fonts-liberation git libvulkan1 mesa-vulkan-drivers openssh-client tar xvfb \
18
- && npm install -g "agent-browser@${AGENT_BROWSER_VERSION}" \
18
+ && npm install -g agent-browser --allow-scripts=agent-browser \
19
+ && actual="$(agent-browser --version)" \
20
+ && printf 'Installed %s; audited baseline agent-browser %s\n' "$actual" "$AGENT_BROWSER_VERSION" \
21
+ && test "$actual" = "agent-browser $AGENT_BROWSER_VERSION" \
19
22
  && groupadd -g 1002 circleci \
20
23
  && useradd -m -u 1001 -g 1002 -s /bin/bash circleci \
21
24
  && mkdir -p /work/crabbox /home/circleci/.cache \
@@ -1,6 +1,8 @@
1
1
  param(
2
2
  [Parameter(Mandatory=$true)][string]$PackageName,
3
- [Parameter(Mandatory=$true)][string]$NodeValidationVersion
3
+ [Parameter(Mandatory=$true)][string]$NodeValidationVersion,
4
+ [string]$CandidatePath = "",
5
+ [string]$CandidateSha256 = ""
4
6
  )
5
7
 
6
8
  $ErrorActionPreference = "Continue"
@@ -40,6 +42,17 @@ $PackTarball = ($PackOutput | Select-Object -Last 1)
40
42
  $PackFile = if ($PackTarball) { Join-Path $PackDir $PackTarball } else { "" }
41
43
  if (Test-Path $PackStderr) { Get-Content -Raw $PackStderr }
42
44
  Write-Output "PLATFORM_NPM_PACK_EXIT=$PackExit"
45
+ $CandidateExit = 0
46
+ if ($CandidatePath) {
47
+ $PackFile = Join-Path $PackDir "release-candidate.tgz"
48
+ $CandidateBefore = ""
49
+ try {
50
+ Copy-Item -LiteralPath (Join-Path $SourceRoot $CandidatePath) -Destination $PackFile -ErrorAction Stop
51
+ $CandidateBefore = (Get-FileHash -LiteralPath $PackFile -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant()
52
+ } catch { $CandidateExit = 1 }
53
+ Write-Output "PLATFORM_CANDIDATE_SHA256_BEFORE=$CandidateBefore"
54
+ if ($CandidateBefore -ne $CandidateSha256) { $CandidateExit = 1 }
55
+ }
43
56
  Write-Output "PLATFORM_PACKED_TARBALL=$PackFile"
44
57
 
45
58
  $PiCli = Join-Path $SourceRoot "node_modules/.bin/pi.cmd"
@@ -49,7 +62,7 @@ Write-Output "PLATFORM_PI_CLI=$PiCli"
49
62
 
50
63
  $PackedNodeInstallStdout = Join-Path $PackDir "packed-node-install.stdout.txt"
51
64
  $PackedNodeInstallStderr = Join-Path $PackDir "packed-node-install.stderr.txt"
52
- if ($PackFile -and (Test-Path $PackFile)) {
65
+ if ($CandidateExit -eq 0 -and $PackFile -and (Test-Path $PackFile)) {
53
66
  Push-Location $PiProject
54
67
  & npm init -y >$PackedNodeInstallStdout 2>$PackedNodeInstallStderr
55
68
  if ($LASTEXITCODE -eq 0) {
@@ -94,7 +107,14 @@ Write-Output "PLATFORM_PI_LIST_EXIT=$PiListExit"
94
107
  Write-Section "PI_LIST_STDOUT" $PiListStdout
95
108
  Write-Section "PI_LIST_STDERR" $PiListStderr
96
109
 
97
- if ($NodeVersionExit -ne 0 -or $NpmCiExit -ne 0 -or $VerifyExit -ne 0 -or $PackExit -ne 0 -or $PackedNodeInstallExit -ne 0 -or $PiInstallExit -ne 0 -or $PiListExit -ne 0) {
110
+ if ($CandidatePath) {
111
+ $CandidateAfter = ""
112
+ try { $CandidateAfter = (Get-FileHash -LiteralPath $PackFile -Algorithm SHA256 -ErrorAction Stop).Hash.ToLowerInvariant() } catch { $CandidateExit = 1 }
113
+ Write-Output "PLATFORM_CANDIDATE_SHA256_AFTER=$CandidateAfter"
114
+ if ($CandidateAfter -ne $CandidateSha256) { $CandidateExit = 1 }
115
+ }
116
+
117
+ if ($NodeVersionExit -ne 0 -or $NpmCiExit -ne 0 -or $VerifyExit -ne 0 -or $PackExit -ne 0 -or $PackedNodeInstallExit -ne 0 -or $PiInstallExit -ne 0 -or $PiListExit -ne 0 -or $CandidateExit -ne 0) {
98
118
  Write-Output "PLATFORM_BUILD_FAILED"
99
119
  exit 1
100
120
  }
@@ -28,7 +28,7 @@ export function commonChecks(result, secretViolations, stopResult) {
28
28
  ];
29
29
  }
30
30
 
31
- export function platformBuildChecks(config, result) {
31
+ export function platformBuildChecks(config, result, candidate) {
32
32
  const stdout = result.stdout;
33
33
  const listOutput = section(stdout, "PI_LIST_STDOUT");
34
34
  const nodeVersion = marker(stdout, "PLATFORM_NODE_VERSION");
@@ -48,6 +48,16 @@ export function platformBuildChecks(config, result) {
48
48
  /PLATFORM_NPM_PACK_EXIT=0/.test(stdout) &&
49
49
  marker(stdout, "PLATFORM_PACKED_TARBALL").length > 0,
50
50
  },
51
+ ...(candidate
52
+ ? [
53
+ {
54
+ id: "release-candidate-sha256",
55
+ fn: () =>
56
+ marker(stdout, "PLATFORM_CANDIDATE_SHA256_BEFORE") === candidate.sha256 &&
57
+ marker(stdout, "PLATFORM_CANDIDATE_SHA256_AFTER") === candidate.sha256,
58
+ },
59
+ ]
60
+ : []),
51
61
  { id: "packed-node-install", fn: () => /PLATFORM_PACKED_NODE_INSTALL_EXIT=0/.test(stdout) },
52
62
  { id: "pi-install-local-package", fn: () => /PLATFORM_PI_INSTALL_EXIT=0/.test(stdout) },
53
63
  {
@@ -32,6 +32,7 @@ function suiteDefinition({ config, targetName, suiteName, leaseSession }) {
32
32
  targetName,
33
33
  config.packageName,
34
34
  config.nodeValidationVersion,
35
+ leaseSession?.candidate,
35
36
  ),
36
37
  timeout: 1_500_000,
37
38
  metadata: { suiteName, modelCalls: 0 },
@@ -48,7 +49,7 @@ function suiteDefinition({ config, targetName, suiteName, leaseSession }) {
48
49
  extract: (suiteDir, result, secretValues) => {
49
50
  writePlatformExtracts(suiteDir, result.stdout, secretValues);
50
51
  },
51
- checks: (result) => platformBuildChecks(config, result),
52
+ checks: (result) => platformBuildChecks(config, result, leaseSession?.candidate),
52
53
  };
53
54
  }
54
55
  if (suiteName === "browser-dogfood-smoke") {
@@ -150,7 +151,7 @@ export async function runTargetSuite({
150
151
  return { ok: assertions.ok, suiteDir, assertions };
151
152
  }
152
153
 
153
- export async function runTargetSuites(config, targetName, suiteNames) {
154
+ export async function runTargetSuites(config, targetName, suiteNames, candidate) {
154
155
  const slug = `${config.packageName}-${targetName}`;
155
156
  const runId = makeRunId();
156
157
  const lease = await warmupLease(targetName, slug, config);
@@ -172,7 +173,7 @@ export async function runTargetSuites(config, targetName, suiteNames) {
172
173
  config,
173
174
  targetName,
174
175
  suiteName,
175
- leaseSession: { ...lease, dependenciesReady, sync },
176
+ leaseSession: { ...lease, dependenciesReady, sync, candidate },
176
177
  runId,
177
178
  });
178
179
  results.push(result);
@@ -5,8 +5,10 @@
5
5
  * Scope: Maintainer release verification only; target command rendering and artifact assertions live under scripts/platform-smoke/.
6
6
  */
7
7
 
8
+ import { createHash } from "node:crypto";
9
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
8
10
  import { createRequire } from "node:module";
9
- import { dirname, resolve } from "node:path";
11
+ import { basename, dirname, join, resolve } from "node:path";
10
12
  import { fileURLToPath, pathToFileURL } from "node:url";
11
13
 
12
14
  const __filename = fileURLToPath(import.meta.url);
@@ -28,17 +30,17 @@ function printHelp() {
28
30
  console.log(`Usage: node scripts/platform-smoke.mjs <command> [options]
29
31
 
30
32
  Commands:
31
- doctor Validate Crabbox and platform prerequisites
32
- run --target <names> Run one or more comma-separated targets concurrently
33
- run --suite <name> Run one suite on all or specified targets
33
+ doctor [--target <names>] Validate prerequisites for selected targets (required by default)
34
+ run [--target <names>] Run required or explicitly selected targets concurrently
35
+ run --suite <name> Run one suite on required or specified targets
34
36
 
35
37
  Package scripts:
36
38
  check:platform-smoke Syntax-check harness scripts and run cheap harness invariant tests
37
39
  smoke:platform:ubuntu-image Build the local Ubuntu image used by default
38
- smoke:platform:all Runs doctor first, then the full macOS/Ubuntu/Windows matrix
40
+ smoke:platform:all Runs doctor first, then required macOS/Ubuntu targets (not optional Windows)
39
41
 
40
42
  Targets:
41
- macos, ubuntu, windows-native
43
+ macos, ubuntu (required); windows-native (explicit best-effort)
42
44
 
43
45
  Suites:
44
46
  platform-build npm ci, npm run verify -- platform-target, npm pack, packed pi install --approve, pi list --approve
@@ -109,34 +111,75 @@ function validateNames(kind, names, allowed) {
109
111
  }
110
112
  }
111
113
 
112
- async function runTargets(args) {
113
- const { runTargetSuites } = await import("./platform-smoke/targets.mjs");
114
+ function selectedTargets(args) {
114
115
  const targets = args.target
115
116
  ? args.target
116
117
  .split(",")
117
118
  .map((name) => name.trim())
118
119
  .filter(Boolean)
119
120
  : config.requiredTargets;
121
+ validateNames("target", targets, [...config.requiredTargets, ...(config.optionalTargets ?? [])]);
122
+ if (targets.length === 0) {
123
+ throw new Error("Select at least one target");
124
+ }
125
+ return targets;
126
+ }
127
+
128
+ async function runTargets(args) {
129
+ const { runTargetSuites } = await import("./platform-smoke/targets.mjs");
130
+ const targets = selectedTargets(args);
120
131
  const suites = args.suite ? [args.suite] : config.requiredSuites;
121
- validateNames("target", targets, config.requiredTargets);
122
132
  validateNames("suite", suites, config.requiredSuites);
123
- const results = await Promise.all(
124
- targets.map(async (targetName) => {
125
- console.log(`\n=== Target: ${targetName} ===`);
126
- return { targetName, result: await runTargetSuites(config, targetName, suites) };
127
- }),
128
- );
129
- const failed = results.filter(({ result }) => !result.ok);
130
- if (failed.length > 0) {
131
- console.error(
132
- `\nPlatform smoke failed for ${failed.map(({ targetName }) => targetName).join(", ")}. See ${config.artifactRoot}.`,
133
+ let stagedCandidate;
134
+ try {
135
+ const candidatePath = process.env.PI_AGENT_BROWSER_RELEASE_TARBALL;
136
+ if (candidatePath !== undefined && suites.includes("platform-build")) {
137
+ if (candidatePath.trim().length === 0) {
138
+ throw new Error("PI_AGENT_BROWSER_RELEASE_TARBALL must name a candidate tarball");
139
+ }
140
+ const bytes = await readFile(resolve(candidatePath));
141
+ const directory = await mkdtemp(resolve(process.cwd(), ".npm-release-candidate-"));
142
+ stagedCandidate = { directory };
143
+ await writeFile(join(directory, "candidate"), bytes, { flag: "wx", mode: 0o600 });
144
+ stagedCandidate.path = `${basename(directory)}/candidate`;
145
+ stagedCandidate.sha256 = createHash("sha256").update(bytes).digest("hex");
146
+ }
147
+ const settled = await Promise.allSettled(
148
+ targets.map(async (targetName) => {
149
+ console.log(`\n=== Target: ${targetName} ===`);
150
+ return {
151
+ targetName,
152
+ result: await runTargetSuites(config, targetName, suites, stagedCandidate),
153
+ };
154
+ }),
155
+ );
156
+ const errors = settled.filter((result) => result.status === "rejected");
157
+ if (errors.length > 0) {
158
+ const messages = errors.map((result) =>
159
+ result.reason instanceof Error ? result.reason.message : String(result.reason),
160
+ );
161
+ throw new AggregateError(
162
+ errors.map((result) => result.reason),
163
+ `Platform smoke could not complete: ${messages.join("; ")}`,
164
+ );
165
+ }
166
+ const results = settled.map((result) => result.value);
167
+ const failed = results.filter(({ result }) => !result.ok);
168
+ if (failed.length > 0) {
169
+ console.error(
170
+ `\nPlatform smoke failed for ${failed.map(({ targetName }) => targetName).join(", ")}. See ${config.artifactRoot}.`,
171
+ );
172
+ return 1;
173
+ }
174
+ console.log(
175
+ `\nPlatform smoke passed for ${results.map(({ targetName }) => targetName).join(", ")}.`,
133
176
  );
134
- return 1;
177
+ return 0;
178
+ } finally {
179
+ if (stagedCandidate) {
180
+ await rm(stagedCandidate.directory, { recursive: true, force: true });
181
+ }
135
182
  }
136
- console.log(
137
- `\nPlatform smoke passed for ${results.map(({ targetName }) => targetName).join(", ")}.`,
138
- );
139
- return 0;
140
183
  }
141
184
 
142
185
  export async function main(argv = process.argv.slice(2)) {
@@ -151,7 +194,7 @@ export async function main(argv = process.argv.slice(2)) {
151
194
 
152
195
  if (args.command === "doctor") {
153
196
  const { runDoctor } = await import("./platform-smoke/doctor.mjs");
154
- await runDoctor(config);
197
+ await runDoctor({ ...config, requiredTargets: selectedTargets(args) });
155
198
  return process.exitCode ?? 0;
156
199
  }
157
200