| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import { spawnSync } from 'node:child_process'; |
| import { createHash } from 'node:crypto'; |
| import { |
| cpSync, |
| existsSync, |
| mkdirSync, |
| mkdtempSync, |
| readFileSync, |
| readdirSync, |
| rmSync, |
| statSync, |
| writeFileSync, |
| } from 'node:fs'; |
| import { createServer } from 'node:net'; |
| import { tmpdir, userInfo } from 'node:os'; |
| import { isAbsolute, join, resolve } from 'node:path'; |
| import { fileURLToPath, pathToFileURL } from 'node:url'; |
| import { npmSpawnOptions } from './npm-spawn.mjs'; |
| |
| const PROCESS_TIMEOUT_MS = 90_000; |
| const MAX_OUTPUT_BYTES = 16 * 1024 * 1024; |
| const MAX_TARBALL_BYTES = 64 * 1024 * 1024; |
| const fixturePath = fileURLToPath( |
| new URL('./released-cli-state-root-fixture.mjs', import.meta.url), |
| ); |
| const qualificationPath = fileURLToPath(import.meta.url); |
| const SUDO_BWRAP_ENV = 'MAKA_QUALIFICATION_BWRAP_USE_SUDO'; |
| |
| export function parseQualificationArgs(argv) { |
| const allowedNames = new Set([ |
| '--source', |
| '--target', |
| '--source-sha256', |
| '--target-sha256', |
| '--target-workspace', |
| '--expect-epoch-relation', |
| ]); |
| const values = new Map(); |
| for (let index = 0; index < argv.length; index += 2) { |
| const name = argv[index]; |
| const value = argv[index + 1]; |
| if (!name?.startsWith('--') || value === undefined) { |
| throw new Error('Qualification arguments must be --name value pairs'); |
| } |
| if (!allowedNames.has(name)) throw new Error(`Unknown qualification argument: ${name}`); |
| if (values.has(name)) throw new Error(`Duplicate qualification argument: ${name}`); |
| values.set(name, value); |
| } |
| const source = requireAbsolutePath(values, '--source'); |
| const sourceSha256 = requireSha256(values, '--source-sha256'); |
| const expectedEpochRelation = values.get('--expect-epoch-relation') ?? 'any'; |
| if (!['same', 'different', 'any'].includes(expectedEpochRelation)) { |
| throw new Error('Expected epoch relation must be same, different, or any'); |
| } |
| // A pull request qualifies the built workspace rather than a packaged |
| // artifact: what decides whether durable state still opens is the compiled |
| // storage and Runtime Host code, and packaging only moves it. The release |
| // lanes keep naming an exact tarball, which is why identity stays required |
| // there rather than optional everywhere. |
| if (values.has('--target-workspace')) { |
| if (values.has('--target') || values.has('--target-sha256')) { |
| throw new Error('A workspace target cannot also name a tarball target'); |
| } |
| return { |
| source, |
| sourceSha256, |
| targetWorkspace: requireAbsolutePath(values, '--target-workspace'), |
| expectedEpochRelation, |
| }; |
| } |
| const target = requireAbsolutePath(values, '--target'); |
| const targetSha256 = requireSha256(values, '--target-sha256'); |
| return { source, target, sourceSha256, targetSha256, expectedEpochRelation }; |
| } |
| |
| export function assertExpectedEpochRelation(sourceEpoch, targetEpoch, expected) { |
| if (!Number.isSafeInteger(sourceEpoch) || !Number.isSafeInteger(targetEpoch)) { |
| throw new Error('Release compatibility epochs must be safe integers'); |
| } |
| const actual = sourceEpoch === targetEpoch ? 'same' : 'different'; |
| if (expected !== 'any' && expected !== actual) { |
| throw new Error(`Expected ${expected} compatibility epochs, found ${actual}`); |
| } |
| return actual; |
| } |
| |
| export function sha256File(path) { |
| return createHash('sha256').update(readFileSync(path)).digest('hex'); |
| } |
| |
| export function qualificationSandboxInvocation({ args, account, useSudo }) { |
| if (!useSudo) return { command: 'bwrap', args }; |
| const separator = args.indexOf('--'); |
| if (separator === -1) throw new Error('Qualification sandbox command is missing'); |
| return { |
| command: 'sudo', |
| args: [ |
| '--non-interactive', |
| '--', |
| '/usr/bin/bwrap', |
| ...args.slice(0, separator), |
| '--cap-add', |
| 'CAP_SETUID', |
| '--cap-add', |
| 'CAP_SETGID', |
| '--cap-add', |
| 'CAP_SETPCAP', |
| '--', |
| '/usr/bin/setpriv', |
| '--regid', |
| String(account.gid), |
| '--reuid', |
| String(account.uid), |
| '--clear-groups', |
| '--inh-caps=-all', |
| '--ambient-caps=-all', |
| '--bounding-set=-all', |
| '--no-new-privs', |
| ...args.slice(separator + 1), |
| ], |
| }; |
| } |
| |
| export async function qualifyReleasedCliStateRoot(input) { |
| if (process.platform !== 'linux') { |
| throw new Error('Released State Root qualification currently requires Linux'); |
| } |
| assertCommandAvailable('bwrap'); |
| assertCommandAvailable('timeout'); |
| const useSudo = parseSudoBwrapEnvironment(process.env[SUDO_BWRAP_ENV]); |
| if (useSudo) { |
| assertCommandAvailable('sudo'); |
| assertCommandAvailable('/usr/bin/setpriv'); |
| } |
| assertTarballDigest(input.source, input.sourceSha256, 'source'); |
| if (!input.targetWorkspace) { |
| assertTarballDigest(input.target, input.targetSha256, 'target'); |
| } |
| const scope = mkdtempSync(join(tmpdir(), 'maka-released-state-root-')); |
| try { |
| const sandbox = prepareSandbox(scope); |
| const source = installArtifact({ |
| role: 'source', |
| tarball: input.source, |
| scope, |
| sandbox, |
| }); |
| const target = input.targetWorkspace |
| ? workspaceArtifact({ repoRoot: input.targetWorkspace, scope, sandbox }) |
| : installArtifact({ |
| role: 'target', |
| tarball: input.target, |
| scope, |
| sandbox, |
| }); |
| const epochRelation = assertExpectedEpochRelation( |
| source.compatibilityEpoch, |
| target.compatibilityEpoch, |
| input.expectedEpochRelation, |
| ); |
| const innerInputPath = join(scope, 'qualification-input.json'); |
| writeFileSync( |
| innerInputPath, |
| `${JSON.stringify({ |
| scope, |
| source, |
| target, |
| sourceSha256: input.sourceSha256, |
| targetSha256: input.targetSha256, |
| epochRelation, |
| })}\n`, |
| { mode: 0o600 }, |
| ); |
| return runQualificationSandbox({ innerInputPath, sandbox, scope, useSudo }); |
| } finally { |
| rmSync(scope, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 }); |
| } |
| } |
| |
| // The State Root is not the whole durable surface. The Runtime Host access file |
| // and the rest of the control records live in the account-local control |
| // namespace, and the Host opens them before the Kernel starts — so a golden |
| // copy that captured only the State Root could not restore, or observe, the |
| // state that decides whether the Host starts at all. The control path mirrors |
| // resolveRootControlNamespace in @maka/storage; this harness runs on Linux |
| // only, so it names the Linux location rather than importing across the |
| // installed artifacts it is here to compare. |
| export function durableStateLocations(scope) { |
| return [ |
| { live: join(scope, 'state-root'), golden: join(scope, 'golden-root') }, |
| { |
| live: join(userInfo().homedir, '.cache', 'maka', 'runtime-hosts'), |
| golden: join(scope, 'golden-control'), |
| }, |
| ]; |
| } |
| |
| async function qualifyInstalledArtifacts(input) { |
| const { scope, source, target } = input; |
| const locations = durableStateLocations(scope); |
| const rootPath = locations[0].live; |
| mkdirSync(rootPath, { recursive: true }); |
| const seeded = runFixture({ action: 'seed', artifact: source, rootPath, scope }); |
| assertFacts(seeded); |
| captureGolden(locations); |
| |
| const sourceReady = await runInstalledRuntimeHost({ artifact: source, rootPath, scope }); |
| const sourceFacts = runFixture({ action: 'inspect', artifact: source, rootPath, scope }); |
| assertSameFacts(seeded, sourceFacts, 'source self-reopen'); |
| |
| restoreGolden(locations); |
| const writerFence = await proveWriterFence({ source, target, rootPath, scope }); |
| |
| restoreGolden(locations); |
| const targetReady = await runInstalledRuntimeHost({ artifact: target, rootPath, scope }); |
| const targetFacts = runFixture({ action: 'inspect', artifact: target, rootPath, scope }); |
| assertSameFacts(seeded, targetFacts, 'target transition'); |
| |
| return { |
| schemaVersion: 1, |
| source: artifactEvidence(source, input.sourceSha256), |
| target: artifactEvidence(target, input.targetSha256), |
| epochRelation: input.epochRelation, |
| facts: seeded, |
| checks: { |
| sourceSelfReopen: { kind: 'passed', host: sourceReady }, |
| concurrentWriterFence: writerFence, |
| targetTransition: { kind: 'passed', host: targetReady }, |
| }, |
| claims: { |
| rollback: 'not_claimed', |
| downgrade: 'unsupported', |
| externalNpmReconciliation: 'not_qualified_by_this_harness', |
| }, |
| }; |
| } |
| |
| function requireAbsolutePath(values, name) { |
| const value = values.get(name); |
| if (!value || !isAbsolute(value)) throw new Error(`${name} must be an absolute path`); |
| return resolve(value); |
| } |
| |
| function requireSha256(values, name) { |
| const value = values.get(name); |
| if (!value || !/^[a-f0-9]{64}$/u.test(value)) { |
| throw new Error(`${name} must be a lowercase SHA-256`); |
| } |
| return value; |
| } |
| |
| function assertTarballDigest(path, expected, role) { |
| const stat = statSync(path); |
| if (!stat.isFile() || stat.size > MAX_TARBALL_BYTES) { |
| throw new Error(`The ${role} release tarball exceeds the qualification boundary`); |
| } |
| const actual = sha256File(path); |
| if (actual !== expected) { |
| throw new Error(`The ${role} release tarball SHA-256 does not match`); |
| } |
| } |
| |
| function assertCommandAvailable(command) { |
| const result = spawnSync(command, ['--version'], { encoding: 'utf8' }); |
| if (result.error?.code === 'ENOENT') throw new Error(`${command} is required`); |
| if (result.status !== 0) throw new Error(`${command} is unavailable`); |
| } |
| |
| function prepareSandbox(scope) { |
| const account = userInfo(); |
| const home = join(scope, 'home'); |
| const temp = join(scope, 'tmp'); |
| const etc = join(scope, 'etc'); |
| for (const path of [home, temp, etc]) mkdirSync(path, { recursive: true }); |
| const passwd = join(etc, 'passwd'); |
| const group = join(etc, 'group'); |
| writeFileSync( |
| passwd, |
| `maka-qualification:x:${account.uid}:${account.gid}:Maka qualification:${home}:/bin/sh\n`, |
| ); |
| writeFileSync(group, `maka-qualification:x:${account.gid}:\n`); |
| return { |
| account, |
| home, |
| temp, |
| passwd, |
| group, |
| environment: { |
| ...process.env, |
| HOME: home, |
| XDG_CACHE_HOME: join(home, '.cache'), |
| XDG_CONFIG_HOME: join(home, '.config'), |
| XDG_DATA_HOME: join(home, '.local/share'), |
| TMPDIR: temp, |
| }, |
| }; |
| } |
| |
| function parseSudoBwrapEnvironment(value) { |
| if (value === undefined || value === '') return false; |
| if (value === '1') return true; |
| throw new Error(`${SUDO_BWRAP_ENV} must be 1 when set`); |
| } |
| |
| function installArtifact({ role, tarball, scope, sandbox }) { |
| const prefix = join(scope, `${role}-prefix`); |
| const cache = join(scope, `${role}-npm-cache`); |
| const result = spawnSync( |
| 'npm', |
| [ |
| 'install', |
| '--global', |
| '--prefix', |
| prefix, |
| '--cache', |
| cache, |
| '--offline', |
| '--no-audit', |
| '--no-fund', |
| tarball, |
| ], |
| npmSpawnOptions({ |
| cwd: scope, |
| env: { |
| ...sandbox.environment, |
| npm_config_registry: 'http://127.0.0.1:9/', |
| }, |
| encoding: 'utf8', |
| }), |
| ); |
| if (result.status !== 0) { |
| throw new Error(`Unable to install the ${role} release tarball: ${result.stderr}`); |
| } |
| const packageRoot = join(prefix, 'lib/node_modules/maka-agent'); |
| const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')); |
| const protocolPath = join(packageRoot, 'node_modules/@maka/runtime-host/dist/protocol/index.js'); |
| const protocol = readFileSync(protocolPath, 'utf8'); |
| const epoch = protocol.match(/RUNTIME_HOST_COMPATIBILITY_EPOCH\s*=\s*(\d+)/u)?.[1]; |
| if (!epoch) throw new Error(`The ${role} release has no compatibility epoch`); |
| const cliPath = join(packageRoot, 'dist/cli.js'); |
| const versionResult = spawnSync(process.execPath, [cliPath, '--version'], { |
| cwd: scope, |
| env: sandbox.environment, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS, |
| }); |
| if (versionResult.status !== 0) { |
| throw new Error(`The ${role} CLI version check failed: ${versionResult.stderr}`); |
| } |
| const version = versionResult.stdout.trim(); |
| if (version !== manifest.version) { |
| throw new Error(`The ${role} CLI reports ${version}; expected ${manifest.version}`); |
| } |
| return { role, prefix, packageRoot, cliPath, version, compatibilityEpoch: Number(epoch) }; |
| } |
| |
| // The workspace already carries the compiled packages the fixture loads: npm |
| // workspaces link node_modules/@maka/* at the repository root, which is the |
| // same shape an installed tarball presents. Nothing is installed, so the |
| // packaging chain in front of this check is skipped entirely. |
| function workspaceArtifact({ repoRoot, scope, sandbox }) { |
| const cliPath = join(repoRoot, 'packages/cli/dist/cli.js'); |
| const protocolPath = join(repoRoot, 'node_modules/@maka/runtime-host/dist/protocol/index.js'); |
| for (const required of [cliPath, protocolPath]) { |
| if (!existsSync(required)) { |
| throw new Error(`The workspace target is not built: ${required} is missing`); |
| } |
| } |
| const epoch = readFileSync(protocolPath, 'utf8').match( |
| /RUNTIME_HOST_COMPATIBILITY_EPOCH\s*=\s*(\d+)/u, |
| )?.[1]; |
| if (!epoch) throw new Error('The workspace target has no compatibility epoch'); |
| const versionResult = spawnSync(process.execPath, [cliPath, '--version'], { |
| cwd: scope, |
| env: sandbox.environment, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS, |
| }); |
| if (versionResult.status !== 0) { |
| throw new Error(`The workspace CLI version check failed: ${versionResult.stderr}`); |
| } |
| return { |
| role: 'target', |
| kind: 'workspace', |
| packageRoot: repoRoot, |
| cliPath, |
| version: versionResult.stdout.trim(), |
| compatibilityEpoch: Number(epoch), |
| }; |
| } |
| |
| export function qualificationSandboxArgs({ innerInputPath, sandbox, scope }) { |
| return [ |
| '--die-with-parent', |
| '--ro-bind', |
| '/', |
| '/', |
| '--tmpfs', |
| '/tmp', |
| '--chmod', |
| '1777', |
| '/tmp', |
| '--bind', |
| scope, |
| scope, |
| '--ro-bind', |
| sandbox.passwd, |
| '/etc/passwd', |
| '--ro-bind', |
| sandbox.group, |
| '/etc/group', |
| '--setenv', |
| 'HOME', |
| sandbox.home, |
| '--setenv', |
| 'XDG_CACHE_HOME', |
| sandbox.environment.XDG_CACHE_HOME, |
| '--setenv', |
| 'XDG_CONFIG_HOME', |
| sandbox.environment.XDG_CONFIG_HOME, |
| '--setenv', |
| 'XDG_DATA_HOME', |
| sandbox.environment.XDG_DATA_HOME, |
| '--setenv', |
| 'TMPDIR', |
| sandbox.temp, |
| '--chdir', |
| scope, |
| '--', |
| process.execPath, |
| qualificationPath, |
| '--inner', |
| innerInputPath, |
| ]; |
| } |
| |
| function runQualificationSandbox({ innerInputPath, sandbox, scope, useSudo }) { |
| const invocation = qualificationSandboxInvocation({ |
| args: qualificationSandboxArgs({ innerInputPath, sandbox, scope }), |
| account: sandbox.account, |
| useSudo, |
| }); |
| const result = spawnSync(invocation.command, invocation.args, { |
| cwd: scope, |
| env: sandbox.environment, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS * 4, |
| }); |
| if (result.status !== 0) { |
| throw new Error(`Qualification sandbox failed: ${result.stderr || result.stdout}`); |
| } |
| return parseLastJsonLine(result.stdout); |
| } |
| |
| function runFixture({ action, artifact, rootPath, scope }) { |
| const result = spawnSync( |
| process.execPath, |
| [fixturePath, '--action', action, '--package-root', artifact.packageRoot, '--root', rootPath], |
| { |
| cwd: scope, |
| env: process.env, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS, |
| }, |
| ); |
| if (result.status !== 0) { |
| throw new Error(`Released fixture failed: ${result.stderr || result.stdout}`); |
| } |
| return parseLastJsonLine(result.stdout); |
| } |
| |
| async function proveWriterFence({ source, target, rootPath, scope }) { |
| const result = spawnSync( |
| process.execPath, |
| [ |
| fixturePath, |
| '--action', |
| 'fence', |
| '--package-root', |
| source.packageRoot, |
| '--root', |
| rootPath, |
| '--target-package-root', |
| target.packageRoot, |
| ], |
| { |
| cwd: scope, |
| env: process.env, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS, |
| }, |
| ); |
| if (result.status !== 0) { |
| throw new Error(`Released writer-fence fixture failed: ${result.stderr || result.stdout}`); |
| } |
| const attempted = parseLastJsonLine(result.stdout); |
| if (attempted.kind !== 'writer_fenced') { |
| throw new Error('The target release acquired a concurrently held State Root writer'); |
| } |
| return { kind: 'passed', rootId: attempted.rootId }; |
| } |
| |
| async function runInstalledRuntimeHost({ artifact, rootPath, scope }) { |
| const configPath = join(scope, `${artifact.role}-runtime-host-service.json`); |
| writeFileSync( |
| configPath, |
| `${JSON.stringify({ |
| schemaVersion: 2, |
| rootPath, |
| projectDirectoryRoots: [{ label: 'qualification', path: scope }], |
| websocket: { |
| host: '127.0.0.1', |
| port: await allocateLoopbackPort(), |
| path: '/runtime-host', |
| }, |
| launch: { nodePath: process.execPath, cliPath: artifact.cliPath }, |
| })}\n`, |
| { mode: 0o600 }, |
| ); |
| const result = spawnSync( |
| 'timeout', |
| [ |
| '--signal=INT', |
| '--kill-after=15s', |
| '--preserve-status', |
| '10s', |
| process.execPath, |
| artifact.cliPath, |
| 'runtime-host', |
| 'serve', |
| '--managed-service-config', |
| configPath, |
| '--json', |
| ], |
| { |
| cwd: scope, |
| env: process.env, |
| encoding: 'utf8', |
| maxBuffer: MAX_OUTPUT_BYTES, |
| timeout: PROCESS_TIMEOUT_MS, |
| }, |
| ); |
| if (result.status !== 0) { |
| throw new Error(`Released Runtime Host failed: ${result.stderr || result.stdout}`); |
| } |
| const ready = findJsonLine(result.stdout, (value) => value.event === 'runtime_host_ready'); |
| if (!ready?.rootId || !ready.hostEpoch) { |
| throw new Error(`The released Runtime Host did not publish Ready: ${result.stderr}`); |
| } |
| return { rootId: ready.rootId, hostEpoch: ready.hostEpoch }; |
| } |
| |
| function parseLastJsonLine(output) { |
| const lines = output.trim().split(/\r?\n/u); |
| for (let index = lines.length - 1; index >= 0; index -= 1) { |
| try { |
| return JSON.parse(lines[index]); |
| } catch { |
| // Earlier lines may contain runtime diagnostics. |
| } |
| } |
| throw new Error('Released fixture produced no JSON evidence'); |
| } |
| |
| function findJsonLine(output, predicate) { |
| for (const line of output.split(/\r?\n/u)) { |
| if (!line.trim().startsWith('{')) continue; |
| try { |
| const value = JSON.parse(line); |
| if (predicate(value)) return value; |
| } catch { |
| // Non-JSON diagnostics remain outside the evidence channel. |
| } |
| } |
| return undefined; |
| } |
| |
| function assertFacts(value) { |
| if ( |
| value.kind !== 'facts' || |
| !value.rootId || |
| !value.session?.id || |
| !value.session?.message?.id || |
| !value.scheduledTask?.id || |
| !value.access?.credentialId |
| ) { |
| throw new Error('Released fixture evidence is incomplete'); |
| } |
| } |
| |
| function assertSameFacts(expected, actual, stage) { |
| assertFacts(actual); |
| if (JSON.stringify(actual) !== JSON.stringify(expected)) { |
| throw new Error(`Durable facts changed during ${stage}`); |
| } |
| } |
| |
| function captureGolden(locations) { |
| for (const { live, golden } of locations) { |
| rmSync(golden, { recursive: true, force: true }); |
| if (!existsSync(live)) continue; |
| cpSync(live, golden, { recursive: true, force: true }); |
| } |
| } |
| |
| function restoreGolden(locations) { |
| for (const { live, golden } of locations) { |
| if (existsSync(live)) { |
| for (const entry of readdirSync(live)) { |
| rmSync(join(live, entry), { recursive: true, force: true }); |
| } |
| } |
| if (!existsSync(golden)) continue; |
| mkdirSync(live, { recursive: true }); |
| for (const entry of readdirSync(golden)) { |
| cpSync(join(golden, entry), join(live, entry), { |
| recursive: true, |
| force: true, |
| }); |
| } |
| } |
| } |
| |
| function artifactEvidence(artifact, sha256) { |
| // A workspace target has no published identity to pin, and saying so keeps |
| // this report from reading as evidence about a release it never touched. |
| if (artifact.kind === 'workspace') { |
| return { |
| version: artifact.version, |
| compatibilityEpoch: artifact.compatibilityEpoch, |
| kind: 'workspace', |
| sha256: 'not_applicable', |
| }; |
| } |
| return { |
| version: artifact.version, |
| compatibilityEpoch: artifact.compatibilityEpoch, |
| sha256, |
| }; |
| } |
| |
| async function allocateLoopbackPort() { |
| const server = createServer(); |
| await new Promise((resolvePromise, reject) => { |
| server.once('error', reject); |
| server.listen(0, '127.0.0.1', resolvePromise); |
| }); |
| const address = server.address(); |
| await new Promise((resolvePromise, reject) => |
| server.close((error) => (error ? reject(error) : resolvePromise())), |
| ); |
| if (!address || typeof address === 'string') throw new Error('Unable to allocate a port'); |
| return address.port; |
| } |
| |
| if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { |
| try { |
| if (process.argv[2] === '--inner') { |
| const innerInputPath = process.argv[3]; |
| if (!innerInputPath || !isAbsolute(innerInputPath) || process.argv.length !== 4) { |
| throw new Error('Qualification sandbox requires one absolute input path'); |
| } |
| const report = await qualifyInstalledArtifacts( |
| JSON.parse(readFileSync(innerInputPath, 'utf8')), |
| ); |
| process.stdout.write(`${JSON.stringify(report)}\n`); |
| } else { |
| const report = await qualifyReleasedCliStateRoot( |
| parseQualificationArgs(process.argv.slice(2)), |
| ); |
| process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); |
| } |
| } catch (error) { |
| process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`); |
| process.exitCode = 1; |
| } |
| } |