@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.49
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.
- package/README.md +183 -15
- package/bin/dsh-live-qualification-authority.js +15 -0
- package/bin/dsh-local-release-adapter.js +328 -8
- package/bin/dsh-systemd-host-attestor.js +359 -9
- package/bin/dsh-systemd-host-authority.js +12 -0
- package/lib/adoption-coordinator.d.ts +3 -1
- package/lib/adoption-coordinator.d.ts.map +1 -1
- package/lib/adoption-coordinator.js +24 -9
- package/lib/adoption-coordinator.js.map +1 -1
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +33 -14
- package/lib/cli.js.map +1 -1
- package/lib/creation-capability-feedback.d.ts +23 -0
- package/lib/creation-capability-feedback.d.ts.map +1 -0
- package/lib/creation-capability-feedback.js +132 -0
- package/lib/creation-capability-feedback.js.map +1 -0
- package/lib/creation-capability-journal.d.ts +66 -0
- package/lib/creation-capability-journal.d.ts.map +1 -0
- package/lib/creation-capability-journal.js +751 -0
- package/lib/creation-capability-journal.js.map +1 -0
- package/lib/creation-capability-runtime.d.ts +63 -0
- package/lib/creation-capability-runtime.d.ts.map +1 -0
- package/lib/creation-capability-runtime.js +564 -0
- package/lib/creation-capability-runtime.js.map +1 -0
- package/lib/creation-capability-source.d.ts +37 -0
- package/lib/creation-capability-source.d.ts.map +1 -0
- package/lib/creation-capability-source.js +328 -0
- package/lib/creation-capability-source.js.map +1 -0
- package/lib/creation-capability-types.d.ts +252 -0
- package/lib/creation-capability-types.d.ts.map +1 -0
- package/lib/creation-capability-types.js +2 -0
- package/lib/creation-capability-types.js.map +1 -0
- package/lib/deployment-readiness.d.ts +16 -0
- package/lib/deployment-readiness.d.ts.map +1 -1
- package/lib/deployment-readiness.js +48 -1
- package/lib/deployment-readiness.js.map +1 -1
- package/lib/effect-blocked-replay.js +2 -2
- package/lib/effect-blocked-replay.js.map +1 -1
- package/lib/foreground-deployment-runtime.d.ts +3 -0
- package/lib/foreground-deployment-runtime.d.ts.map +1 -1
- package/lib/foreground-deployment-runtime.js +55 -13
- package/lib/foreground-deployment-runtime.js.map +1 -1
- package/lib/foreground-deployment.d.ts.map +1 -1
- package/lib/foreground-deployment.js +5 -0
- package/lib/foreground-deployment.js.map +1 -1
- package/lib/host-attestor.d.ts +2 -0
- package/lib/host-attestor.d.ts.map +1 -1
- package/lib/host-attestor.js +8 -1
- package/lib/host-attestor.js.map +1 -1
- package/lib/host-inputs.d.ts +13 -0
- package/lib/host-inputs.d.ts.map +1 -0
- package/lib/host-inputs.js +162 -0
- package/lib/host-inputs.js.map +1 -0
- package/lib/host-maintenance.d.ts +61 -0
- package/lib/host-maintenance.d.ts.map +1 -0
- package/lib/host-maintenance.js +158 -0
- package/lib/host-maintenance.js.map +1 -0
- package/lib/host-readiness.d.ts +5 -0
- package/lib/host-readiness.d.ts.map +1 -0
- package/lib/host-readiness.js +22 -0
- package/lib/host-readiness.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/live-qualification-authority.d.ts +32 -0
- package/lib/live-qualification-authority.d.ts.map +1 -0
- package/lib/live-qualification-authority.js +280 -0
- package/lib/live-qualification-authority.js.map +1 -0
- package/lib/live-qualification-client.d.ts +4 -0
- package/lib/live-qualification-client.d.ts.map +1 -0
- package/lib/live-qualification-client.js +22 -0
- package/lib/live-qualification-client.js.map +1 -0
- package/lib/live-qualification-runtime.d.ts +78 -0
- package/lib/live-qualification-runtime.d.ts.map +1 -0
- package/lib/live-qualification-runtime.js +367 -0
- package/lib/live-qualification-runtime.js.map +1 -0
- package/lib/live-qualification.d.ts +73 -0
- package/lib/live-qualification.d.ts.map +1 -0
- package/lib/live-qualification.js +164 -0
- package/lib/live-qualification.js.map +1 -0
- package/lib/owner-task-gaps.d.ts +2 -0
- package/lib/owner-task-gaps.d.ts.map +1 -1
- package/lib/owner-task-gaps.js +9 -1
- package/lib/owner-task-gaps.js.map +1 -1
- package/lib/runtime-epoch.d.ts +69 -0
- package/lib/runtime-epoch.d.ts.map +1 -0
- package/lib/runtime-epoch.js +139 -0
- package/lib/runtime-epoch.js.map +1 -0
- package/lib/runtime-observer.d.ts.map +1 -1
- package/lib/runtime-observer.js +42 -6
- package/lib/runtime-observer.js.map +1 -1
- package/lib/service.d.ts +247 -4
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1332 -27
- package/lib/service.js.map +1 -1
- package/lib/source-adoption-authority.d.ts +3 -0
- package/lib/source-adoption-authority.d.ts.map +1 -1
- package/lib/source-adoption-authority.js +13 -3
- package/lib/source-adoption-authority.js.map +1 -1
- package/lib/source-adoption-runner.d.ts +3 -0
- package/lib/source-adoption-runner.d.ts.map +1 -1
- package/lib/source-adoption-runner.js +16 -4
- package/lib/source-adoption-runner.js.map +1 -1
- package/lib/source-baseline.d.ts +37 -0
- package/lib/source-baseline.d.ts.map +1 -0
- package/lib/source-baseline.js +250 -0
- package/lib/source-baseline.js.map +1 -0
- package/lib/source-build.d.ts +17 -1
- package/lib/source-build.d.ts.map +1 -1
- package/lib/source-build.js +501 -7
- package/lib/source-build.js.map +1 -1
- package/lib/source-context.d.ts +18 -0
- package/lib/source-context.d.ts.map +1 -1
- package/lib/source-context.js +85 -11
- package/lib/source-context.js.map +1 -1
- package/lib/source-creation.d.ts +106 -0
- package/lib/source-creation.d.ts.map +1 -0
- package/lib/source-creation.js +664 -0
- package/lib/source-creation.js.map +1 -0
- package/lib/source-job-types.d.ts +19 -0
- package/lib/source-job-types.d.ts.map +1 -1
- package/lib/source-jobs.d.ts +30 -3
- package/lib/source-jobs.d.ts.map +1 -1
- package/lib/source-jobs.js +382 -40
- package/lib/source-jobs.js.map +1 -1
- package/lib/source-maintenance.d.ts +66 -0
- package/lib/source-maintenance.d.ts.map +1 -0
- package/lib/source-maintenance.js +177 -0
- package/lib/source-maintenance.js.map +1 -0
- package/lib/source-revision.d.ts +57 -0
- package/lib/source-revision.d.ts.map +1 -0
- package/lib/source-revision.js +175 -0
- package/lib/source-revision.js.map +1 -0
- package/lib/source-workspace.d.ts +19 -14
- package/lib/source-workspace.d.ts.map +1 -1
- package/lib/source-workspace.js +90 -17
- package/lib/source-workspace.js.map +1 -1
- package/lib/sqlite.d.ts +13 -3
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +362 -1
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +258 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2211 -95
- package/lib/store.js.map +1 -1
- package/lib/systemd-host-authority.d.ts +68 -0
- package/lib/systemd-host-authority.d.ts.map +1 -0
- package/lib/systemd-host-authority.js +424 -0
- package/lib/systemd-host-authority.js.map +1 -0
- package/lib/task-observation-evidence.d.ts +32 -0
- package/lib/task-observation-evidence.d.ts.map +1 -0
- package/lib/task-observation-evidence.js +62 -0
- package/lib/task-observation-evidence.js.map +1 -0
- package/lib/task-observation-runtime.d.ts +2 -3
- package/lib/task-observation-runtime.d.ts.map +1 -1
- package/lib/task-observation-runtime.js +7 -39
- package/lib/task-observation-runtime.js.map +1 -1
- package/lib/task-observation-store.js +1 -1
- package/lib/trust.d.ts.map +1 -1
- package/lib/trust.js +0 -0
- package/lib/trust.js.map +1 -1
- package/lib/types.d.ts +43 -6
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +21 -14
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
existsSync,
|
|
26
26
|
fsyncSync,
|
|
27
27
|
lstatSync,
|
|
28
|
+
mkdtempSync,
|
|
28
29
|
mkdirSync,
|
|
29
30
|
openSync,
|
|
30
31
|
readFileSync,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
readdirSync,
|
|
34
35
|
readSync,
|
|
35
36
|
renameSync,
|
|
37
|
+
rmdirSync,
|
|
36
38
|
rmSync,
|
|
37
39
|
fstatSync,
|
|
38
40
|
statSync,
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
} from 'node:fs'
|
|
44
46
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
45
47
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
48
|
+
import { gunzipSync, gzipSync } from 'node:zlib'
|
|
46
49
|
|
|
47
50
|
export const LOCAL_RELEASE_ADAPTER_VERSION = 'dsh-local-release-adapter-1'
|
|
48
51
|
const PHASES = new Set(['pr', 'review', 'merge', 'build', 'sign', 'publish', 'registry-verify', 'catalog-admission'])
|
|
@@ -411,7 +414,8 @@ function catalogConfig(value, npm = false) {
|
|
|
411
414
|
}
|
|
412
415
|
function buildConfig(value) {
|
|
413
416
|
const item = object(value, 'build config')
|
|
414
|
-
|
|
417
|
+
const expected = ['sandboxExecutable', 'tarExecutable', 'nodeExecutable', 'pnpmExecutable', 'pnpmRoot', 'storeRoot']
|
|
418
|
+
exactKeys(item, item.cacheRoot === undefined ? expected : [...expected, 'cacheRoot'], 'build config')
|
|
415
419
|
const sandboxExecutable = inspectExecutable(item.sandboxExecutable, 'build sandbox')
|
|
416
420
|
const tarExecutable = inspectExecutable(item.tarExecutable, 'tar')
|
|
417
421
|
if (sandboxExecutable.path !== '/usr/bin/bwrap') fail('local build adapter requires the pinned Linux bubblewrap sandbox')
|
|
@@ -419,6 +423,7 @@ function buildConfig(value) {
|
|
|
419
423
|
const pnpmExecutable = inspectExecutable(item.pnpmExecutable, 'pnpm executable')
|
|
420
424
|
const pnpmRoot = pinnedDirectory(item.pnpmRoot, 'pnpm root')
|
|
421
425
|
const storeRoot = pinnedDirectory(item.storeRoot, 'pnpm store')
|
|
426
|
+
const cacheRoot = item.cacheRoot === undefined ? undefined : pinnedDirectory(item.cacheRoot, 'pnpm cache')
|
|
422
427
|
if (nodeExecutable.path !== join(pnpmRoot.path, 'node')) fail('Node executable must be the pinned pnpm root node entrypoint')
|
|
423
428
|
if (pnpmExecutable.path !== join(pnpmRoot.path, 'pnpm')) fail('pnpm executable must be the pinned pnpm root entrypoint')
|
|
424
429
|
let pnpmManifest
|
|
@@ -428,14 +433,40 @@ function buildConfig(value) {
|
|
|
428
433
|
const storeVersion = Number.parseInt(pnpmVersion.split('.')[0], 10)
|
|
429
434
|
const projectsPath = join(storeRoot.path, `v${storeVersion}`, 'projects')
|
|
430
435
|
if (!existsSync(projectsPath) || !lstatSync(projectsPath).isDirectory()) fail('pnpm store project registry is unavailable')
|
|
431
|
-
return { sandboxExecutable, tarExecutable, nodeExecutable, pnpmExecutable, pnpmRoot, storeRoot, storeVersion }
|
|
436
|
+
return { sandboxExecutable, tarExecutable, nodeExecutable, pnpmExecutable, pnpmRoot, storeRoot, cacheRoot, storeVersion }
|
|
432
437
|
}
|
|
433
438
|
function verifyBuildPins(build) {
|
|
434
|
-
|
|
439
|
+
const pinned = [[build.pnpmRoot, 'pnpm root'], [build.storeRoot, 'pnpm store']]
|
|
440
|
+
if (build.cacheRoot !== undefined) pinned.push([build.cacheRoot, 'pnpm cache'])
|
|
441
|
+
for (const [item, label] of pinned) {
|
|
435
442
|
if (directoryInventory(item.path, label).sha256 !== item.sha256) fail(`${label} changed after configuration validation`)
|
|
436
443
|
}
|
|
437
444
|
}
|
|
438
445
|
|
|
446
|
+
export function inspectLocalReleaseBuildEnvironment(value) {
|
|
447
|
+
const input = object(value, 'local release build environment')
|
|
448
|
+
exactKeys(input, input.cacheRoot === undefined ? ['pnpmRoot', 'storeRoot'] : ['pnpmRoot', 'storeRoot', 'cacheRoot'],
|
|
449
|
+
'local release build environment')
|
|
450
|
+
const executable = (path, label) => {
|
|
451
|
+
const pin = { path, sha256: sha256Bytes(stableBytes(path, label)) }
|
|
452
|
+
return inspectExecutable(pin, label)
|
|
453
|
+
}
|
|
454
|
+
const pnpmRoot = directoryInventory(input.pnpmRoot, 'pnpm root')
|
|
455
|
+
const storeRoot = directoryInventory(input.storeRoot, 'pnpm store')
|
|
456
|
+
const cacheRoot = input.cacheRoot === undefined ? undefined : directoryInventory(input.cacheRoot, 'pnpm cache')
|
|
457
|
+
const pins = {
|
|
458
|
+
sandboxExecutable: executable('/usr/bin/bwrap', 'build sandbox'),
|
|
459
|
+
tarExecutable: executable('/usr/bin/tar', 'tar'),
|
|
460
|
+
nodeExecutable: executable(join(pnpmRoot.path, 'node'), 'build Node executable'),
|
|
461
|
+
pnpmExecutable: executable(join(pnpmRoot.path, 'pnpm'), 'pnpm executable'),
|
|
462
|
+
pnpmRoot: { path: pnpmRoot.path, sha256: pnpmRoot.sha256 },
|
|
463
|
+
storeRoot: { path: storeRoot.path, sha256: storeRoot.sha256 },
|
|
464
|
+
...(cacheRoot === undefined ? {} : { cacheRoot: { path: cacheRoot.path, sha256: cacheRoot.sha256 } }),
|
|
465
|
+
}
|
|
466
|
+
buildConfig(pins)
|
|
467
|
+
return pins
|
|
468
|
+
}
|
|
469
|
+
|
|
439
470
|
function command(executable, args, cwd, extraEnvironment = {}, input, inherited = [], hooks = {}) {
|
|
440
471
|
if (process.platform !== 'linux' || !existsSync('/proc/self/fd')) fail('descriptor-pinned commands require Linux /proc/self/fd')
|
|
441
472
|
const pinned = openPinnedFile(executable, `pinned ${basename(executable.path)} executable`, MAX_ARTIFACT_BYTES, true)
|
|
@@ -488,14 +519,17 @@ function openSandboxContext(build, workspace, output, runRoot) {
|
|
|
488
519
|
const storeProjects = join(runRoot, 'store-projects')
|
|
489
520
|
if (!existsSync(storeProjects)) mkdirSync(storeProjects, { mode: 0o700 })
|
|
490
521
|
const toolchainSnapshot = join(runRoot, 'toolchain-snapshot'); const storeSnapshot = join(runRoot, 'store-snapshot')
|
|
522
|
+
const cacheSnapshot = build.cacheRoot === undefined ? undefined : join(runRoot, 'cache-snapshot')
|
|
491
523
|
if (!existsSync(toolchainSnapshot)) copyPinnedTree(build.pnpmRoot, toolchainSnapshot, 'pnpm root')
|
|
492
524
|
if (!existsSync(storeSnapshot)) copyPinnedTree(build.storeRoot, storeSnapshot, 'pnpm store')
|
|
525
|
+
if (cacheSnapshot !== undefined && !existsSync(cacheSnapshot)) copyPinnedTree(build.cacheRoot, cacheSnapshot, 'pnpm cache')
|
|
493
526
|
const pnpmRoot = openPinnedDirectory({ path: toolchainSnapshot, sha256: build.pnpmRoot.sha256 }, 'pnpm snapshot')
|
|
494
527
|
const storeRoot = openPinnedDirectory({ path: storeSnapshot, sha256: build.storeRoot.sha256 }, 'pnpm store snapshot')
|
|
495
528
|
const workspaceRoot = openPinnedDirectory({ path: workspace }, 'build workspace')
|
|
496
529
|
const outputRoot = openPinnedDirectory({ path: output }, 'build output')
|
|
497
530
|
const storeProjectsRoot = openPinnedDirectory({ path: storeProjects }, 'pnpm project registry')
|
|
498
531
|
const mounts = [pnpmRoot, storeRoot, workspaceRoot, outputRoot, storeProjectsRoot]
|
|
532
|
+
if (cacheSnapshot !== undefined) mounts.push(openPinnedDirectory({ path: cacheSnapshot, sha256: build.cacheRoot.sha256 }, 'pnpm cache snapshot'))
|
|
499
533
|
return { mounts, runRoot }
|
|
500
534
|
}
|
|
501
535
|
function closeSandboxContext(context) { for (const item of context.mounts) closePinnedDirectory(item) }
|
|
@@ -505,15 +539,92 @@ function sandboxCommand(build, args, context, hooks = {}) {
|
|
|
505
539
|
const mountFd = index => String(3 + index)
|
|
506
540
|
const sandboxArgs = ['--unshare-all', '--die-with-parent', '--new-session', '--clearenv',
|
|
507
541
|
'--ro-bind', '/usr', '/usr', '--ro-bind', '/lib', '/lib', '--ro-bind', '/lib64', '/lib64',
|
|
542
|
+
'--symlink', 'usr/bin', '/bin',
|
|
508
543
|
'--ro-bind-fd', mountFd(0), '/toolchain', '--ro-bind-fd', mountFd(1), '/store',
|
|
544
|
+
...(build.cacheRoot === undefined ? [] : ['--ro-bind-fd', mountFd(5), '/cache']),
|
|
509
545
|
'--bind-fd', mountFd(4), `/store/v${build.storeVersion}/projects`, '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp', '--dir', '/home',
|
|
510
546
|
'--bind-fd', mountFd(2), '/workspace', '--bind-fd', mountFd(3), '/output',
|
|
511
547
|
'--chdir', '/workspace', '--setenv', 'HOME', '/home', '--setenv', 'TMPDIR', '/tmp', '--setenv', 'SOURCE_DATE_EPOCH', '0',
|
|
512
|
-
'--setenv', 'PATH', '/toolchain:/usr/bin:/bin',
|
|
548
|
+
'--setenv', 'PATH', '/toolchain:/usr/bin:/bin',
|
|
549
|
+
...(build.cacheRoot === undefined ? [] : ['--setenv', 'pnpm_config_cache_dir', '/cache']),
|
|
550
|
+
'/toolchain/pnpm', ...args]
|
|
513
551
|
command(build.sandboxExecutable, sandboxArgs, runRoot, {}, undefined, mounts.map(item => item.descriptor),
|
|
514
552
|
{ beforeSpawn: hooks.beforeSandboxSpawn, afterSpawn: hooks.afterSandboxSpawn, afterFinally: hooks.afterSandboxFinally })
|
|
515
553
|
for (const item of mounts) verifyPinnedDirectory(item)
|
|
516
554
|
}
|
|
555
|
+
|
|
556
|
+
/** Build one frozen local cohort with the release adapter's pinned, offline
|
|
557
|
+
* sandbox. The caller supplies private staging directories, never a live Home.
|
|
558
|
+
* All candidate code sees is a copy without Git metadata or installed modules. */
|
|
559
|
+
export function buildLocalUpdateCohort(value) {
|
|
560
|
+
const input = object(value, 'local update cohort')
|
|
561
|
+
exactKeys(input, ['build', 'workspace', 'output', 'packagePaths'], 'local update cohort')
|
|
562
|
+
const build = buildConfig(input.build)
|
|
563
|
+
const workspace = canonicalPath(input.workspace, 'local update workspace')
|
|
564
|
+
const output = canonicalPath(input.output, 'local update output')
|
|
565
|
+
if (workspace === output || dirname(workspace) !== dirname(output)) fail('local update workspace and output must be private siblings')
|
|
566
|
+
privateDirectory(dirname(workspace), 'local update staging parent')
|
|
567
|
+
privateDirectory(workspace, 'local update workspace')
|
|
568
|
+
privateDirectory(output, 'local update output')
|
|
569
|
+
if (readdirSync(output).length !== 0) fail('local update output must be empty')
|
|
570
|
+
const outputIdentity = statSync(output, { bigint: true })
|
|
571
|
+
if (!Array.isArray(input.packagePaths) || input.packagePaths.length < 1 || input.packagePaths.length > 128) {
|
|
572
|
+
fail('local update package paths exceed their bound')
|
|
573
|
+
}
|
|
574
|
+
const paths = input.packagePaths.map(path => text(path, 'local update package path', /^(?:plugins|packages)\/[a-z0-9][a-z0-9-]{0,63}$/u))
|
|
575
|
+
const slugs = paths.map(path => basename(path))
|
|
576
|
+
if (new Set(paths).size !== paths.length || new Set(slugs).size !== slugs.length) fail('local update package paths are ambiguous')
|
|
577
|
+
verifyBuildPins(build)
|
|
578
|
+
const runRoot = mkdtempSync(join(dirname(workspace), '.local-cohort-build-'))
|
|
579
|
+
try {
|
|
580
|
+
chmodSync(runRoot, 0o700)
|
|
581
|
+
const isolatedWorkspace = join(runRoot, 'workspace')
|
|
582
|
+
copyBuildWorkspace(workspace, isolatedWorkspace)
|
|
583
|
+
normalizeBuildBinModes(isolatedWorkspace)
|
|
584
|
+
const sourceInputs = snapshotBuildInputs(isolatedWorkspace)
|
|
585
|
+
for (const path of paths) {
|
|
586
|
+
const packageRoot = join(isolatedWorkspace, path)
|
|
587
|
+
if (!existsSync(packageRoot) || !lstatSync(packageRoot).isDirectory()
|
|
588
|
+
|| realpathSync(packageRoot) !== resolve(packageRoot)) fail(`local update package is unavailable: ${path}`)
|
|
589
|
+
}
|
|
590
|
+
const packed = join(runRoot, 'packed'); mkdirSync(packed, { mode: 0o700 })
|
|
591
|
+
for (const slug of slugs) mkdirSync(join(packed, slug), { mode: 0o700 })
|
|
592
|
+
const sandbox = openSandboxContext(build, isolatedWorkspace, packed, runRoot)
|
|
593
|
+
try {
|
|
594
|
+
sandboxCommand(build, ['install', '--offline', '--frozen-lockfile', '--frozen-store', '--ignore-scripts',
|
|
595
|
+
'--package-import-method=copy', '--store-dir=/store'], sandbox)
|
|
596
|
+
sandboxCommand(build, ['--workspace-root', '--if-present', 'run', 'build'], sandbox)
|
|
597
|
+
for (const [index, path] of paths.entries()) {
|
|
598
|
+
const packageRoot = join(isolatedWorkspace, path)
|
|
599
|
+
if (!lstatSync(packageRoot).isDirectory() || realpathSync(packageRoot) !== resolve(packageRoot)) {
|
|
600
|
+
fail(`local update package changed before pack: ${path}`)
|
|
601
|
+
}
|
|
602
|
+
sandboxCommand(build, ['--dir', `/workspace/${path}`, 'pack', '--pack-destination', `/output/${slugs[index]}`], sandbox)
|
|
603
|
+
}
|
|
604
|
+
} finally { closeSandboxContext(sandbox) }
|
|
605
|
+
verifyBuildInputs(isolatedWorkspace, sourceInputs)
|
|
606
|
+
verifyBuildPins(build)
|
|
607
|
+
if (readdirSync(packed).sort().join('\0') !== [...slugs].sort().join('\0')) fail('local update pack output has unexpected directories')
|
|
608
|
+
let totalBytes = 0
|
|
609
|
+
for (const slug of slugs) {
|
|
610
|
+
const directory = join(packed, slug)
|
|
611
|
+
privateDirectory(directory, `local update ${slug} output`)
|
|
612
|
+
const names = readdirSync(directory)
|
|
613
|
+
if (names.length !== 1 || !names[0].endsWith('.tgz')) fail(`local update pack output is ambiguous: ${slug}`)
|
|
614
|
+
const tarball = safeRegularFile(join(directory, names[0]), `local update ${slug} tarball`)
|
|
615
|
+
totalBytes += lstatSync(tarball).size
|
|
616
|
+
if (totalBytes > 2_147_483_648) fail('local update cohort output exceeds its bound')
|
|
617
|
+
}
|
|
618
|
+
// Output is a private, empty preparation directory; make the complete
|
|
619
|
+
// validated set visible in one rename, or leave it absent on failure.
|
|
620
|
+
const currentOutput = lstatSync(output, { bigint: true })
|
|
621
|
+
if (!currentOutput.isDirectory() || currentOutput.dev !== outputIdentity.dev
|
|
622
|
+
|| currentOutput.ino !== outputIdentity.ino || readdirSync(output).length !== 0) fail('local update output changed during build')
|
|
623
|
+
rmdirSync(output)
|
|
624
|
+
renameSync(packed, output)
|
|
625
|
+
fsyncDirectory(dirname(output))
|
|
626
|
+
} finally { rmSync(runRoot, { recursive: true, force: true }) }
|
|
627
|
+
}
|
|
517
628
|
function git(gitValue, args, cwd, environment = {}, hooks = {}) {
|
|
518
629
|
return command(gitValue.executable, [...SAFE_GIT_CONFIG, ...args], cwd,
|
|
519
630
|
{ GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null', ...environment }, undefined, [],
|
|
@@ -964,6 +1075,92 @@ function copyBuildWorkspace(source, destination) {
|
|
|
964
1075
|
}
|
|
965
1076
|
inspect(destination)
|
|
966
1077
|
}
|
|
1078
|
+
function normalizeBuildBinModes(root) {
|
|
1079
|
+
for (const collection of ['plugins', 'packages']) {
|
|
1080
|
+
const collectionRoot = join(root, collection)
|
|
1081
|
+
if (!existsSync(collectionRoot)) continue
|
|
1082
|
+
for (const name of readdirSync(collectionRoot)) {
|
|
1083
|
+
const packageRoot = join(collectionRoot, name)
|
|
1084
|
+
if (!lstatSync(packageRoot).isDirectory()) continue
|
|
1085
|
+
const manifestPath = join(packageRoot, 'package.json')
|
|
1086
|
+
if (!existsSync(manifestPath)) continue
|
|
1087
|
+
const manifestFile = lstatSync(manifestPath)
|
|
1088
|
+
if (!manifestFile.isFile() || manifestFile.size > MAX_INPUT_BYTES) fail('local update package manifest is unsafe')
|
|
1089
|
+
let manifest
|
|
1090
|
+
try { manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) }
|
|
1091
|
+
catch { fail('local update package manifest is invalid') }
|
|
1092
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) fail('local update package manifest is invalid')
|
|
1093
|
+
const bin = manifest.bin
|
|
1094
|
+
if (bin === undefined) continue
|
|
1095
|
+
const binPaths = typeof bin === 'string' ? [bin]
|
|
1096
|
+
: bin !== null && typeof bin === 'object' && !Array.isArray(bin) ? Object.values(bin) : null
|
|
1097
|
+
if (binPaths === null) fail('local update package bin is invalid')
|
|
1098
|
+
for (const binPath of binPaths) {
|
|
1099
|
+
if (typeof binPath !== 'string' || binPath.length === 0 || binPath.length > 2_000
|
|
1100
|
+
|| binPath.includes('\0') || binPath.includes('\\') || isAbsolute(binPath)
|
|
1101
|
+
|| binPath.split('/').includes('..')) fail('local update bin path is invalid')
|
|
1102
|
+
const path = resolve(packageRoot, binPath)
|
|
1103
|
+
const inside = relative(packageRoot, path)
|
|
1104
|
+
if (!inside || inside === '..' || inside.startsWith(`..${sep}`) || isAbsolute(inside)) fail('local update bin path escapes its package')
|
|
1105
|
+
const parts = inside.split(sep)
|
|
1106
|
+
let current = packageRoot
|
|
1107
|
+
let missing = false
|
|
1108
|
+
for (const [index, part] of parts.entries()) {
|
|
1109
|
+
current = join(current, part)
|
|
1110
|
+
let metadata
|
|
1111
|
+
try { metadata = lstatSync(current) }
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
if (error.code !== 'ENOENT') throw error
|
|
1114
|
+
missing = true
|
|
1115
|
+
break
|
|
1116
|
+
}
|
|
1117
|
+
if (metadata.isSymbolicLink() || (index === parts.length - 1 ? !metadata.isFile() : !metadata.isDirectory())) {
|
|
1118
|
+
fail('local update bin path is unsafe')
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (missing) continue
|
|
1122
|
+
if (realpathSync(path) !== path) fail('local update bin path is unsafe')
|
|
1123
|
+
chmodSync(path, 0o755)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function snapshotBuildInputs(root) {
|
|
1129
|
+
const directories = [], files = []
|
|
1130
|
+
let entries = 0, bytes = 0
|
|
1131
|
+
const visit = (path, relativePath = '') => {
|
|
1132
|
+
const metadata = lstatSync(path)
|
|
1133
|
+
if (++entries > 100_000) fail('local update source has too many entries')
|
|
1134
|
+
if (metadata.isDirectory() && !metadata.isSymbolicLink()) {
|
|
1135
|
+
directories.push({ path: relativePath, mode: metadata.mode & 0o777 })
|
|
1136
|
+
for (const name of readdirSync(path).sort()) visit(join(path, name), relativePath ? `${relativePath}/${name}` : name)
|
|
1137
|
+
} else if (metadata.isFile() && !metadata.isSymbolicLink()) {
|
|
1138
|
+
bytes += metadata.size
|
|
1139
|
+
if (metadata.size > MAX_ARTIFACT_BYTES || bytes > 4_294_967_296) fail('local update source exceeds its size bound')
|
|
1140
|
+
files.push({ path: relativePath, mode: metadata.mode & 0o777, size: metadata.size,
|
|
1141
|
+
sha256: sha256Bytes(readFileSync(path)) })
|
|
1142
|
+
} else fail('local update source contains an unsupported entry')
|
|
1143
|
+
}
|
|
1144
|
+
visit(root)
|
|
1145
|
+
return { directories, files }
|
|
1146
|
+
}
|
|
1147
|
+
function verifyBuildInputs(root, before) {
|
|
1148
|
+
for (const item of before.directories) {
|
|
1149
|
+
const path = item.path ? join(root, item.path) : root
|
|
1150
|
+
const metadata = lstatSync(path)
|
|
1151
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== item.mode) {
|
|
1152
|
+
fail(`local update source directory changed during build: ${item.path}`)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
for (const item of before.files) {
|
|
1156
|
+
const metadata = lstatSync(join(root, item.path))
|
|
1157
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size !== item.size
|
|
1158
|
+
|| (metadata.mode & 0o777) !== item.mode
|
|
1159
|
+
|| sha256Bytes(readFileSync(join(root, item.path))) !== item.sha256) {
|
|
1160
|
+
fail(`local update source file changed during build: ${item.path}`)
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
967
1164
|
function fileInventory(root) {
|
|
968
1165
|
const result = []
|
|
969
1166
|
const visit = (directory, prefix = '') => {
|
|
@@ -976,6 +1173,92 @@ function fileInventory(root) {
|
|
|
976
1173
|
}
|
|
977
1174
|
visit(root); return result
|
|
978
1175
|
}
|
|
1176
|
+
const TAR_BLOCK_BYTES = 512
|
|
1177
|
+
const DEPENDENCY_MAPS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']
|
|
1178
|
+
function tarOctal(header, start, length, label) {
|
|
1179
|
+
const field = header.subarray(start, start + length)
|
|
1180
|
+
if (field.some(byte => byte !== 0 && byte !== 32 && (byte < 48 || byte > 55))) {
|
|
1181
|
+
fail(`${label} uses unsupported binary or non-octal tar encoding`)
|
|
1182
|
+
}
|
|
1183
|
+
const octal = field.toString('ascii').replaceAll('\0', ' ').trim()
|
|
1184
|
+
if (!/^[0-7]+$/u.test(octal)) fail(`${label} is not an octal tar field`)
|
|
1185
|
+
const value = Number.parseInt(octal, 8)
|
|
1186
|
+
if (!Number.isSafeInteger(value)) fail(`${label} exceeds the safe tar size`)
|
|
1187
|
+
return value
|
|
1188
|
+
}
|
|
1189
|
+
function tarString(header, start, length) {
|
|
1190
|
+
const field = header.subarray(start, start + length)
|
|
1191
|
+
const terminator = field.indexOf(0)
|
|
1192
|
+
if (terminator !== -1 && field.subarray(terminator).some(byte => byte !== 0)) fail('tar pathname field has nonzero padding')
|
|
1193
|
+
return field.subarray(0, terminator === -1 ? field.length : terminator).toString('utf8')
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
export function normalizePackedDependencyOrder(packed) {
|
|
1197
|
+
if (!Buffer.isBuffer(packed) || packed.length < 1 || packed.length > MAX_ARTIFACT_BYTES) fail('packed archive size is invalid')
|
|
1198
|
+
let archive
|
|
1199
|
+
try { archive = gunzipSync(packed, { maxOutputLength: MAX_ARTIFACT_BYTES }) }
|
|
1200
|
+
catch { fail('packed archive is invalid or exceeds the extraction limit') }
|
|
1201
|
+
if (archive.length < TAR_BLOCK_BYTES * 3 || archive.length % TAR_BLOCK_BYTES !== 0) fail('packed tar block layout is invalid')
|
|
1202
|
+
let manifestOffset; let manifestSize; let offset = 0; let terminated = false
|
|
1203
|
+
while (offset < archive.length) {
|
|
1204
|
+
const header = archive.subarray(offset, offset + TAR_BLOCK_BYTES)
|
|
1205
|
+
if (header.every(byte => byte === 0)) {
|
|
1206
|
+
if (archive.length - offset < TAR_BLOCK_BYTES * 2 || archive.subarray(offset).some(byte => byte !== 0)) {
|
|
1207
|
+
fail('packed tar terminator is invalid')
|
|
1208
|
+
}
|
|
1209
|
+
terminated = true; break
|
|
1210
|
+
}
|
|
1211
|
+
const expectedChecksum = tarOctal(header, 148, 8, 'tar header checksum')
|
|
1212
|
+
let checksum = 0
|
|
1213
|
+
for (let index = 0; index < TAR_BLOCK_BYTES; index += 1) checksum += index >= 148 && index < 156 ? 32 : header[index]
|
|
1214
|
+
if (checksum !== expectedChecksum) fail('packed tar header checksum is invalid')
|
|
1215
|
+
const size = tarOctal(header, 124, 12, 'tar entry size')
|
|
1216
|
+
const type = header[156]
|
|
1217
|
+
if (type !== 0 && type !== 48 && type !== 53) fail('packed tar uses an unsupported entry representation')
|
|
1218
|
+
if (type === 53 && size !== 0) fail('packed tar directory has a payload')
|
|
1219
|
+
const name = tarString(header, 0, 100); const prefix = tarString(header, 345, 155)
|
|
1220
|
+
const entryPath = prefix === '' ? name : `${prefix}/${name}`
|
|
1221
|
+
const logicalPath = type === 53 && entryPath.endsWith('/') ? entryPath.slice(0, -1) : entryPath
|
|
1222
|
+
if ((logicalPath !== 'package' && !logicalPath.startsWith('package/'))
|
|
1223
|
+
|| logicalPath.includes('//') || logicalPath.split('/').some(part => part === '' || part === '.' || part === '..')) {
|
|
1224
|
+
fail('packed tar entry path is not canonical')
|
|
1225
|
+
}
|
|
1226
|
+
const dataOffset = offset + TAR_BLOCK_BYTES
|
|
1227
|
+
const nextOffset = dataOffset + Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES
|
|
1228
|
+
if (nextOffset > archive.length) fail('packed tar entry is truncated')
|
|
1229
|
+
if (entryPath === 'package/package.json') {
|
|
1230
|
+
if (manifestOffset !== undefined || prefix !== '' || (type !== 0 && type !== 48)) {
|
|
1231
|
+
fail('packed manifest has a duplicate or unsupported representation')
|
|
1232
|
+
}
|
|
1233
|
+
manifestOffset = dataOffset; manifestSize = size
|
|
1234
|
+
}
|
|
1235
|
+
offset = nextOffset
|
|
1236
|
+
}
|
|
1237
|
+
if (!terminated || manifestOffset === undefined || manifestSize === undefined) fail('packed tar has no regular package manifest')
|
|
1238
|
+
const sourceBytes = archive.subarray(manifestOffset, manifestOffset + manifestSize)
|
|
1239
|
+
const source = sourceBytes.toString('utf8')
|
|
1240
|
+
if (!Buffer.from(source, 'utf8').equals(sourceBytes)) fail('packed manifest is not valid UTF-8')
|
|
1241
|
+
let manifest
|
|
1242
|
+
try { manifest = JSON.parse(source) } catch { fail('packed manifest is not valid JSON') }
|
|
1243
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest)) fail('packed manifest must be an object')
|
|
1244
|
+
const lineEnding = source.endsWith('\n') ? '\n' : ''
|
|
1245
|
+
if (`${JSON.stringify(manifest, null, 2)}${lineEnding}` !== source) fail('packed manifest is not in the supported canonical JSON format')
|
|
1246
|
+
const normalized = { ...manifest }
|
|
1247
|
+
for (const name of DEPENDENCY_MAPS) {
|
|
1248
|
+
if (!Object.hasOwn(manifest, name)) continue
|
|
1249
|
+
const entries = manifest[name]
|
|
1250
|
+
if (typeof entries !== 'object' || entries === null || Array.isArray(entries)
|
|
1251
|
+
|| Object.values(entries).some(value => typeof value !== 'string')) fail(`packed manifest ${name} is invalid`)
|
|
1252
|
+
normalized[name] = Object.fromEntries(Object.entries(entries).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0))
|
|
1253
|
+
}
|
|
1254
|
+
const normalizedBytes = Buffer.from(`${JSON.stringify(normalized, null, 2)}${lineEnding}`)
|
|
1255
|
+
if (normalizedBytes.length !== manifestSize) fail('packed manifest normalization changed its tar entry size')
|
|
1256
|
+
const normalizedArchive = Buffer.from(archive)
|
|
1257
|
+
normalizedBytes.copy(normalizedArchive, manifestOffset)
|
|
1258
|
+
const result = gzipSync(normalizedArchive, { mtime: 0 })
|
|
1259
|
+
if (result.length > MAX_ARTIFACT_BYTES) fail('normalized packed archive exceeds the artifact limit')
|
|
1260
|
+
return result
|
|
1261
|
+
}
|
|
979
1262
|
function prepareBuildSource(request, context, gitValue, hooks) {
|
|
980
1263
|
const bundle = join(context.directory, 'source.bundle'); rmSync(bundle, { force: true })
|
|
981
1264
|
const temporaryRef = `refs/dsh-release/build/${sha256Bytes(request.operationId).slice(0, 32)}`
|
|
@@ -1012,15 +1295,18 @@ function runIsolatedBuild(request, context, runNumber, gitValue, build, packageP
|
|
|
1012
1295
|
try {
|
|
1013
1296
|
sandboxCommand(build, ['install', '--offline', '--frozen-lockfile', '--frozen-store', '--ignore-scripts',
|
|
1014
1297
|
'--package-import-method=copy', '--store-dir=/store'], sandbox, hooks)
|
|
1298
|
+
sandboxCommand(build, ['--workspace-root', '--if-present', 'run', 'build'], sandbox, hooks)
|
|
1015
1299
|
sandboxCommand(build, ['--dir', `/workspace/${packagePath}`, 'run', 'build'], sandbox, hooks)
|
|
1016
1300
|
sandboxCommand(build, ['--dir', `/workspace/${packagePath}`, 'pack', '--pack-destination', '/output'], sandbox, hooks)
|
|
1017
1301
|
} finally { closeSandboxContext(sandbox) }
|
|
1018
1302
|
const packed = readdirSync(output).filter(name => name.endsWith('.tgz'))
|
|
1019
1303
|
if (packed.length !== 1 || readdirSync(output).length !== 1) fail('pnpm pack must produce exactly one tarball')
|
|
1020
1304
|
const tarball = safeRegularFile(join(output, packed[0]), `isolated build ${runNumber} tarball`)
|
|
1021
|
-
const bytes = readBounded(tarball, `isolated build ${runNumber} tarball`)
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1305
|
+
const bytes = normalizePackedDependencyOrder(readBounded(tarball, `isolated build ${runNumber} tarball`))
|
|
1306
|
+
const normalizedTarball = join(runRoot, 'normalized-package.tgz'); writeSynced(normalizedTarball, bytes, 0o600)
|
|
1307
|
+
const unpacked = unpackPackage(build, normalizedTarball, join(runRoot, 'packed'), runRoot, hooks)
|
|
1308
|
+
return { runRoot, checkout, workspace, packageRoot: unpacked.packageRoot, inventory: unpacked.inventory,
|
|
1309
|
+
tarball: normalizedTarball, bytes, sha256: sha256Bytes(bytes) }
|
|
1024
1310
|
}
|
|
1025
1311
|
function temporaryBuildRef(request) { return `refs/dsh-release/build/${sha256Bytes(request.operationId).slice(0, 32)}` }
|
|
1026
1312
|
function buildPhase(request, config, context, hooks) {
|
|
@@ -1069,7 +1355,8 @@ function buildPhase(request, config, context, hooks) {
|
|
|
1069
1355
|
const sbomPath = join(outputs, 'sbom.cdx.json'); immutableCopy(sbomPath, Buffer.from(`${canonicalReleaseValue(sbom)}\n`), 0o600)
|
|
1070
1356
|
const provenance = { _type: 'https://in-toto.io/Statement/v1', subject: [{ name: packageName, digest: { sha256: tarballSha256 } }],
|
|
1071
1357
|
predicateType: 'https://slsa.dev/provenance/v1', predicate: { buildDefinition: { buildType: 'https://dsh-enhanced.dev/build/local-isolated/v1',
|
|
1072
|
-
externalParameters: { packagePath, packageVersion
|
|
1358
|
+
externalParameters: { packagePath, packageVersion, manifestNormalization: 'sort-direct-dependency-maps-v1' },
|
|
1359
|
+
resolvedDependencies: [{ uri: pathToFileURL(gitValue.remote).href, digest: { gitCommit: mergeCommit } }] },
|
|
1073
1360
|
runDetails: { builder: { id: `dsh-local-release-adapter:${config.authority}:${config.keyId}` }, metadata: { invocationId: request.operationId },
|
|
1074
1361
|
byproducts: [{ name: 'sbom', digest: { sha256: sha256Bytes(readFileSync(sbomPath)) } }] } } }
|
|
1075
1362
|
const provenancePath = join(outputs, 'provenance.intoto.jsonl'); immutableCopy(provenancePath, Buffer.from(`${canonicalReleaseValue(provenance)}\n`), 0o600)
|
|
@@ -1384,6 +1671,39 @@ function persistReviewReceipt(receipt, config) {
|
|
|
1384
1671
|
} else immutableJson(path, receipt, 0o600)
|
|
1385
1672
|
}
|
|
1386
1673
|
|
|
1674
|
+
/** Read-only installation preflight. No request, receipt, lock, subprocess or
|
|
1675
|
+
* release phase is created; return public identity only, never loaded keys. */
|
|
1676
|
+
export function inspectLocalReleaseAdapterConfiguration(environment, phase) {
|
|
1677
|
+
if (!PHASES.has(phase)) fail('adapter inspection phase is invalid')
|
|
1678
|
+
const config = loadConfig(environment, phase)
|
|
1679
|
+
if (config.phase !== phase) fail('adapter config differs from inspection phase')
|
|
1680
|
+
const publicKey = createPublicKey(config.privateKey)
|
|
1681
|
+
if (config.authorizationAuthority.publicKey.equals(publicKey)
|
|
1682
|
+
|| (config.authorizationAuthority.authority === config.authority && config.authorizationAuthority.keyId === config.keyId)) {
|
|
1683
|
+
fail('release authorization and adapter identities must be independent')
|
|
1684
|
+
}
|
|
1685
|
+
if (['pr', 'review', 'merge', 'build'].includes(phase)) {
|
|
1686
|
+
const git = gitConfig(config.git)
|
|
1687
|
+
if (phase === 'review' && (!git.reviewStore || !git.reviewDecisionRoot)) fail('review requires independent decision and receipt stores')
|
|
1688
|
+
if (phase === 'merge' && (!git.reviewStore || !git.reviewAuthority || git.reviewAuthority.publicKey.equals(publicKey))) {
|
|
1689
|
+
fail('merge requires an independent review identity and receipt store')
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (phase === 'build') buildConfig(config.build)
|
|
1693
|
+
if (['publish', 'registry-verify', 'catalog-admission'].includes(phase)) {
|
|
1694
|
+
const registry = registryConfig(config.registry)
|
|
1695
|
+
if (!registry.signer) fail('registry requires an artifact signer')
|
|
1696
|
+
if (phase === 'registry-verify' && !registry.downloadRoot) fail('registry verification requires a separate download root')
|
|
1697
|
+
if (registry.signer.publicKey.equals(publicKey)) fail('registry phase and artifact signer must be independent')
|
|
1698
|
+
}
|
|
1699
|
+
if (phase === 'catalog-admission') {
|
|
1700
|
+
const catalog = catalogConfig(config.catalog)
|
|
1701
|
+
const helper = openPinnedFile(catalog.helper, 'catalog helper', 1_048_576, false)
|
|
1702
|
+
try { privateFile(catalog.path, 'catalog file') } finally { closePinnedFile(helper) }
|
|
1703
|
+
}
|
|
1704
|
+
return { id: config.id, phase, executablePath: config.executablePath, authority: config.authority, keyId: config.keyId }
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1387
1707
|
export async function runLocalReleaseAdapter(argv = process.argv.slice(2), environment = process.env, hooks = {}) {
|
|
1388
1708
|
if (argv.length === 1 && argv[0] === '--version') { process.stdout.write(`${LOCAL_RELEASE_ADAPTER_VERSION}\n`); return }
|
|
1389
1709
|
if (argv.length === 1 && argv[0] === '--capabilities') {
|