diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5b46baa8..7259a8f28d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - A fleet message the broker cannot deliver to its worker is no longer reported back as handled, so it stays outstanding and can be redelivered. - Fleet deliveries the broker rejects are now logged with a reason and sequence number, so a worker that stops receiving messages can be diagnosed from the broker log. - PTY workers no longer exit when Claude Code's folder-trust dialog appears. Relay selects the affirmative option by its label, so both menu orderings work. +- A relayflow that declares agent `permissions` no longer fails local provisioning with `Failed to create workspace : HTTP 404`. ## [11.10.3] - 2026-09-05 diff --git a/package-lock.json b/package-lock.json index 8c46e52d83..2ae4e4c5c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agent-relay/monorepo", - "version": "11.5.2", + "version": "11.10.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agent-relay/monorepo", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "workspaces": [ "packages/*" @@ -4252,6 +4252,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=18" } @@ -4269,6 +4270,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -4286,6 +4288,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -4303,6 +4306,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=18" } @@ -4320,6 +4324,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -4337,6 +4342,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=18" } @@ -4354,6 +4360,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4371,6 +4378,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4388,6 +4396,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4405,6 +4414,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4422,6 +4432,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4439,6 +4450,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4456,6 +4468,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4473,6 +4486,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4490,6 +4504,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4507,6 +4522,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4524,6 +4540,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=18" } @@ -4541,6 +4558,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4558,6 +4576,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4575,6 +4594,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4592,6 +4612,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=18" } @@ -4609,6 +4630,7 @@ "os": [ "openharmony" ], + "peer": true, "engines": { "node": ">=18" } @@ -4626,6 +4648,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=18" } @@ -4643,6 +4666,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -4660,6 +4684,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -4677,6 +4702,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=18" } @@ -6202,10 +6228,10 @@ } }, "node_modules/@relayfile/core": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.8.10.tgz", - "integrity": "sha512-fq5607zeDCw7XIB/q1xtNnFqgak9p+MQoqZ4UmreAZ/Fj8ywYv9WYdH5I3/kQX2bMCgQh1KRUjt0FaxPLPJhHg==", - "license": "MIT", + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/core/-/core-0.10.54.tgz", + "integrity": "sha512-psKO/HvXklThe4eFb2lL29lD1RnMbILY8tnkyrD47/oZ309qb2w98PDlZohajgbeO9qwS9VhupoJmDeoDq0pnw==", + "license": "Apache-2.0", "engines": { "node": ">=18" } @@ -6224,6 +6250,58 @@ "node": ">=18" } }, + "node_modules/@relayfile/mount-darwin-arm64": { + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-arm64/-/mount-darwin-arm64-0.10.54.tgz", + "integrity": "sha512-D0UQOSipYdslvTtT4iFANmZFlotHkJ6Tpifq15IihaXR1ujy6k5DeeViF7C+KvIO7hbcjXs9LbVhGme0411y0w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@relayfile/mount-darwin-x64": { + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/mount-darwin-x64/-/mount-darwin-x64-0.10.54.tgz", + "integrity": "sha512-BQO87V5ex+JxSIsCP/aXcwIdknAEjxpOKKknGK93d2h8/iNbdPC8CXLcTbaGUngO5rVl2mKi8iJgM2xNVswuNg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@relayfile/mount-linux-arm64": { + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-arm64/-/mount-linux-arm64-0.10.54.tgz", + "integrity": "sha512-A7iMaLCjNxpG/dWrf+vs4CDZvAAZYjvd7Bxf3MWIp3NWBV/5cPfojOXgaK/eVcya5PG66x/tdNNTrKAY08ArZw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@relayfile/mount-linux-x64": { + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/mount-linux-x64/-/mount-linux-x64-0.10.54.tgz", + "integrity": "sha512-Om86gou2mcv23FnqmB+uyhyZGV3Mu9pqM6EEDG6iloU2Gsop6pciDwP8z74tRWyycmUDkPAaLDKXChGOEjIXzw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@relayfile/relay-helpers": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/@relayfile/relay-helpers/-/relay-helpers-0.4.6.tgz", @@ -6253,17 +6331,23 @@ } }, "node_modules/@relayfile/sdk": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.8.10.tgz", - "integrity": "sha512-16aFuXOv9vAdBz9uymY9QwdrhaahPGczrvw3U8NUp/9B6foyE2gb7sFPtXupKlY/M2hXYrKJ9RA7dubc5sHndw==", - "license": "MIT", + "version": "0.10.54", + "resolved": "https://registry.npmjs.org/@relayfile/sdk/-/sdk-0.10.54.tgz", + "integrity": "sha512-ygWa4te+4Hs9/CDEgXOZAJTeZeEA0JB3ZgxS29BEw0mzAKaYMCWlBMtfWeUx51Ut0h1jJt01ikGYPkWcZ4tjiw==", + "license": "Apache-2.0", "dependencies": { - "@relayfile/core": "0.8.10", + "@relayfile/core": "0.10.54", "ignore": "^7.0.5", "tar": "^7.5.10" }, "engines": { "node": ">=18" + }, + "optionalDependencies": { + "@relayfile/mount-darwin-arm64": "0.10.54", + "@relayfile/mount-darwin-x64": "0.10.54", + "@relayfile/mount-linux-arm64": "0.10.54", + "@relayfile/mount-linux-x64": "0.10.54" } }, "node_modules/@relayflows/browser-primitive": { @@ -8531,6 +8615,7 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, "optional": true, "engines": { "node": ">=10.0.0" @@ -8885,6 +8970,7 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, "hasInstallScript": true, "optional": true, "dependencies": { @@ -11318,6 +11404,7 @@ "version": "2.27.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", + "dev": true, "license": "MIT", "optional": true }, @@ -13506,14 +13593,14 @@ }, "packages/brand": { "name": "@agent-relay/brand", - "version": "11.5.2", + "version": "11.10.3", "engines": { "node": ">=22.0.0" } }, "packages/broker-darwin-arm64": { "name": "@agent-relay/broker-darwin-arm64", - "version": "11.5.2", + "version": "11.10.3", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -13521,7 +13608,7 @@ }, "packages/broker-darwin-x64": { "name": "@agent-relay/broker-darwin-x64", - "version": "11.5.2", + "version": "11.10.3", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -13529,7 +13616,7 @@ }, "packages/broker-linux-arm64": { "name": "@agent-relay/broker-linux-arm64", - "version": "11.5.2", + "version": "11.10.3", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -13537,7 +13624,7 @@ }, "packages/broker-linux-x64": { "name": "@agent-relay/broker-linux-x64", - "version": "11.5.2", + "version": "11.10.3", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -13545,7 +13632,7 @@ }, "packages/broker-win32-x64": { "name": "@agent-relay/broker-win32-x64", - "version": "11.5.2", + "version": "11.10.3", "license": "MIT", "engines": { "node": ">=22.0.0" @@ -13553,16 +13640,17 @@ }, "packages/cli": { "name": "agent-relay", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { - "@agent-relay/cloud": "11.5.2", - "@agent-relay/config": "11.5.2", - "@agent-relay/fleet": "11.5.2", - "@agent-relay/harness-driver": "11.5.2", - "@agent-relay/harnesses": "11.5.2", - "@agent-relay/sdk": "11.5.2", - "@agent-relay/utils": "11.5.2", + "@agent-relay/cloud": "11.10.3", + "@agent-relay/config": "11.10.3", + "@agent-relay/fleet": "11.10.3", + "@agent-relay/harness-driver": "11.10.3", + "@agent-relay/harnesses": "11.10.3", + "@agent-relay/sdk": "11.10.3", + "@agent-relay/session": "11.10.3", + "@agent-relay/utils": "11.10.3", "@modelcontextprotocol/sdk": "^1.23.0", "@relayfile/client": "^0.10.27", "@relayflows/cli": "1.0.1", @@ -13590,9 +13678,9 @@ }, "packages/cloud": { "name": "@agent-relay/cloud", - "version": "11.5.2", + "version": "11.10.3", "dependencies": { - "@agent-relay/config": "11.5.2", + "@agent-relay/config": "11.10.3", "@aws-sdk/client-s3": "3.1020.0", "ignore": "^7.0.5", "tar": "^7.5.21" @@ -13611,7 +13699,7 @@ }, "packages/config": { "name": "@agent-relay/config", - "version": "11.5.2", + "version": "11.10.3", "dependencies": { "zod": "^4.4.3" }, @@ -13626,11 +13714,11 @@ }, "packages/evals": { "name": "@agent-relay/evals", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "11.5.2", - "@agent-relay/integration-prompts": "11.5.2" + "@agent-relay/harness-driver": "11.10.3", + "@agent-relay/integration-prompts": "11.10.3" }, "engines": { "node": ">=22.0.0" @@ -13638,11 +13726,11 @@ }, "packages/fleet": { "name": "@agent-relay/fleet", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "11.5.2", - "@agent-relay/harnesses": "11.5.2", + "@agent-relay/harness-driver": "11.10.3", + "@agent-relay/harnesses": "11.10.3", "@relaycast/sdk": "^8.0.7", "ws": "^8.18.3", "zod": "^4.4.3" @@ -13656,10 +13744,10 @@ }, "packages/harness-driver": { "name": "@agent-relay/harness-driver", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { - "@agent-relay/sdk": "11.5.2", + "@agent-relay/sdk": "11.10.3", "ws": "^8.18.3", "zod": "^4.4.3" }, @@ -13667,20 +13755,20 @@ "node": ">=22.0.0" }, "optionalDependencies": { - "@agent-relay/broker-darwin-arm64": "11.5.2", - "@agent-relay/broker-darwin-x64": "11.5.2", - "@agent-relay/broker-linux-arm64": "11.5.2", - "@agent-relay/broker-linux-x64": "11.5.2", - "@agent-relay/broker-win32-x64": "11.5.2" + "@agent-relay/broker-darwin-arm64": "11.10.3", + "@agent-relay/broker-darwin-x64": "11.10.3", + "@agent-relay/broker-linux-arm64": "11.10.3", + "@agent-relay/broker-linux-x64": "11.10.3", + "@agent-relay/broker-win32-x64": "11.10.3" } }, "packages/harnesses": { "name": "@agent-relay/harnesses", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { - "@agent-relay/harness-driver": "11.5.2", - "@agent-relay/sdk": "11.5.2", + "@agent-relay/harness-driver": "11.10.3", + "@agent-relay/sdk": "11.10.3", "@ai-sdk/harness": "1.0.34", "@ai-sdk/harness-claude-code": "1.0.35", "@ai-sdk/harness-codex": "1.0.40", @@ -13694,7 +13782,7 @@ }, "packages/integration-prompts": { "name": "@agent-relay/integration-prompts", - "version": "11.5.2", + "version": "11.10.3", "license": "Apache-2.0", "engines": { "node": ">=22.0.0" @@ -13702,9 +13790,9 @@ }, "packages/policy": { "name": "@agent-relay/policy", - "version": "11.5.2", + "version": "11.10.3", "dependencies": { - "@agent-relay/config": "11.5.2" + "@agent-relay/config": "11.10.3" }, "devDependencies": { "@types/node": "^22.19.3", @@ -13716,7 +13804,7 @@ }, "packages/sdk": { "name": "@agent-relay/sdk", - "version": "11.5.2", + "version": "11.10.3", "dependencies": { "@relaycast/sdk": "^8.0.7", "@relaycast/types": "^8.0.7", @@ -13731,7 +13819,7 @@ }, "packages/session": { "name": "@agent-relay/session", - "version": "11.5.5", + "version": "11.10.3", "license": "Apache-2.0", "dependencies": { "@relaycast/sdk": "^8.0.7" @@ -13747,9 +13835,9 @@ }, "packages/utils": { "name": "@agent-relay/utils", - "version": "11.5.2", + "version": "11.10.3", "dependencies": { - "@agent-relay/config": "11.5.2", + "@agent-relay/config": "11.10.3", "compare-versions": "^6.1.1" }, "devDependencies": { diff --git a/package.json b/package.json index 37890950d5..1562043d89 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,9 @@ }, "packageManager": "npm@10.5.1", "overrides": { + "@relayflows/core": { + "@relayfile/sdk": "^0.10.47" + }, "flatted": "^3.4.2", "axios": "^1.18.1", "fast-uri": "^3.1.4", diff --git a/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/case.json b/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/case.json new file mode 100644 index 0000000000..d8ce695407 --- /dev/null +++ b/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/case.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "id": "1702-relayfile-sdk-workspace-404", + "kind": "bugfix", + "title": "createWorkspaceIfNeeded treats a 404 on the workspace collection route as fatal", + "runner": { + "command": ["node", "tests/relayflows/cases/1702-relayfile-sdk-workspace-404/run.mjs"] + }, + "requirements": [], + "timeoutSeconds": 900, + "expected": { + "base": { + "outcome": "bug", + "signature": "relayfile_sdk_404_fatal" + }, + "head": { + "outcome": "fixed", + "signature": "relayfile_sdk_404_treated_as_noop" + } + } +} diff --git a/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/run.mjs b/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/run.mjs new file mode 100644 index 0000000000..ed4e65b7b9 --- /dev/null +++ b/tests/relayflows/cases/1702-relayfile-sdk-workspace-404/run.mjs @@ -0,0 +1,171 @@ +import { spawnSync } from 'node:child_process'; +import { createServer } from 'node:http'; +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const CASE_ID = '1702-relayfile-sdk-workspace-404'; +const INSTALL_TIMEOUT_MS = 10 * 60 * 1000; +// createWorkspaceIfNeeded() takes no signal/timeout option of its own — it is +// an opaque call into the target checkout's installed SDK, hitting a +// same-process mock server that answers instantly, so it should never +// legitimately take anywhere near this long. Racing it against a timeout +// keeps a stalled or unexpectedly-retrying call from burning the case's +// entire 900s budget before failing closed with a clear diagnostic. +const WORKSPACE_CALL_TIMEOUT_MS = 15_000; + +const targetDir = requiredDirectory('RELAY_PR_PROOF_TARGET_DIR'); +const harnessDir = requiredDirectory('RELAY_PR_PROOF_HARNESS_DIR'); +const resultPath = requiredValue('RELAY_PR_PROOF_RESULT_PATH'); +const arm = requiredValue('RELAY_PR_PROOF_ARM'); + +if (arm !== 'base' && arm !== 'head') { + throw new Error(`RELAY_PR_PROOF_ARM must be base or head, received ${JSON.stringify(arm)}.`); +} + +const expectedSha = + arm === 'base' ? process.env.RELAY_PR_PROOF_BASE_SHA : process.env.RELAY_PR_PROOF_HEAD_SHA; +if (!expectedSha) throw new Error(`Missing expected ${arm} SHA.`); +const targetSha = run( + 'git', + ['-C', targetDir, 'rev-parse', 'HEAD'], + targetDir, + 'git rev-parse' +).stdout.trim(); +if (targetSha !== expectedSha) { + throw new Error(`Target checkout ${targetSha} does not match exact ${arm} SHA ${expectedSha}.`); +} + +const runnerPath = fileURLToPath(import.meta.url); +if (!isWithin(harnessDir, runnerPath)) { + throw new Error('The RelayFlow runner must execute from the exact-head harness checkout.'); +} + +// The bug (relay#1702) is that @relayflows/core pins @relayfile/sdk to a +// version whose createWorkspaceIfNeeded() treats a 404 on the deprecated +// bare `POST /v1/workspaces` collection route as fatal. relayfile-cloud's +// real router never registers that route — only `/v1/workspaces/:id/...`, +// since workspaces are Durable Objects created implicitly by ID — so this +// is a guaranteed failure, not a hypothetical one. Reproducing it faithfully +// only requires the target checkout's OWN resolved @relayfile/sdk exercising +// the exact route shape the real service returns; standing up the real +// relayfile-cloud service is unnecessary and would make the case dependent +// on production availability instead of on the code actually under test. +const server = createServer((req, res) => { + const isBareWorkspacesCollectionPost = + req.method === 'POST' && new URL(req.url, 'http://127.0.0.1').pathname === '/v1/workspaces'; + if (isBareWorkspacesCollectionPost) { + res.writeHead(404, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ code: 'not_found', message: 'Route not found' })); + return; + } + // Any ID-scoped route (the only kind relayfile-cloud actually registers) + // succeeds, matching production. + res.writeHead(204); + res.end(); +}); + +let details; +let outcome; +let signature; + +try { + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + resolve(); + }); + }); + const address = server.address(); + const baseUrl = `http://127.0.0.1:${address.port}`; + + // Fresh, exact-lockfile install of the target checkout so the resolved + // @relayfile/sdk version is whatever that checkout's package.json and + // package-lock.json actually pin — the real thing under test, not an + // assumption about it. + await rm(path.join(targetDir, 'node_modules'), { recursive: true, force: true }); + run('npm', ['ci'], targetDir, 'npm ci'); + + const sdkEntryPath = path.join(targetDir, 'node_modules/@relayfile/sdk/dist/workspace-seeder.js'); + const sdkPackagePath = path.join(targetDir, 'node_modules/@relayfile/sdk/package.json'); + const { default: sdkPackage } = await import(pathToFileURL(sdkPackagePath).href, { + with: { type: 'json' }, + }); + const { createWorkspaceIfNeeded } = await import(pathToFileURL(sdkEntryPath).href); + + let thrown = null; + try { + await withTimeout( + createWorkspaceIfNeeded(baseUrl, 'proof-token', 'proof-workspace'), + WORKSPACE_CALL_TIMEOUT_MS, + 'createWorkspaceIfNeeded' + ); + } catch (error) { + thrown = error; + } + + if (thrown === null) { + outcome = 'fixed'; + signature = 'relayfile_sdk_404_treated_as_noop'; + details = `@relayfile/sdk@${sdkPackage.version}: createWorkspaceIfNeeded() returned normally against a mock relayfile-cloud that 404s the bare collection route, matching the real service.`; + } else if (/HTTP 404/.test(thrown.message ?? '')) { + outcome = 'bug'; + signature = 'relayfile_sdk_404_fatal'; + details = `@relayfile/sdk@${sdkPackage.version}: createWorkspaceIfNeeded() threw on the mock relayfile-cloud's 404 for the bare collection route: ${thrown.message.slice(0, 500)}`; + } else { + throw thrown; + } +} finally { + await new Promise((resolve) => server.close(resolve)); +} + +await mkdir(path.dirname(resultPath), { recursive: true }); +await writeFile( + resultPath, + `${JSON.stringify({ version: 1, caseId: CASE_ID, arm, outcome, signature, details })}\n` +); + +function requiredValue(name) { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`Missing required environment variable ${name}.`); + return value; +} + +function requiredDirectory(name) { + return path.resolve(requiredValue(name)); +} + +function isWithin(directory, candidate) { + const relative = path.relative(directory, candidate); + return ( + relative === '' || + (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)) + ); +} + +async function withTimeout(promise, timeoutMs, label) { + let timer; + const timeout = new Promise((_resolve, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs); + }); + try { + return await Promise.race([promise, timeout]); + } finally { + clearTimeout(timer); + } +} + +function run(command, args, cwd, label) { + const completed = spawnSync(command, args, { cwd, encoding: 'utf8', timeout: INSTALL_TIMEOUT_MS }); + if (completed.error) throw new Error(`${label} could not start: ${completed.error.message}`); + if (completed.status !== 0) { + throw new Error( + `${label} failed with ${ + completed.signal ? `signal ${completed.signal}` : `exit code ${completed.status ?? 'unknown'}` + }: ${(completed.stderr ?? '').slice(-2000)}` + ); + } + return completed; +}