upstream-radar 0.40.0 → 0.42.0
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 +72 -241
- package/README.zh-CN.md +89 -0
- package/dist/src/cli.js +11 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/dsh-compatibility-ir.d.ts +99 -0
- package/dist/src/dsh-compatibility-ir.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-ir.js +408 -0
- package/dist/src/dsh-compatibility-ir.js.map +1 -0
- package/dist/src/dsh-compatibility-issues.d.ts +46 -0
- package/dist/src/dsh-compatibility-issues.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-issues.js +223 -0
- package/dist/src/dsh-compatibility-issues.js.map +1 -0
- package/dist/src/dsh-compatibility-ledger.d.ts +152 -0
- package/dist/src/dsh-compatibility-ledger.d.ts.map +1 -0
- package/dist/src/dsh-compatibility-ledger.js +678 -0
- package/dist/src/dsh-compatibility-ledger.js.map +1 -0
- package/dist/src/dsh-directory-feed.d.ts +108 -0
- package/dist/src/dsh-directory-feed.d.ts.map +1 -0
- package/dist/src/dsh-directory-feed.js +267 -0
- package/dist/src/dsh-directory-feed.js.map +1 -0
- package/dist/src/dsh-install-observation.d.ts +89 -1
- package/dist/src/dsh-install-observation.d.ts.map +1 -1
- package/dist/src/dsh-install-observation.js +658 -19
- package/dist/src/dsh-install-observation.js.map +1 -1
- package/dist/src/dsh-install-plan.d.ts +16 -6
- package/dist/src/dsh-install-plan.d.ts.map +1 -1
- package/dist/src/dsh-install-plan.js +273 -47
- package/dist/src/dsh-install-plan.js.map +1 -1
- package/dist/src/dsh-plugin.d.ts.map +1 -1
- package/dist/src/dsh-plugin.js +6 -2
- package/dist/src/dsh-plugin.js.map +1 -1
- package/dist/src/dsh-runtime.d.ts +6 -0
- package/dist/src/dsh-runtime.d.ts.map +1 -1
- package/dist/src/dsh-runtime.js +38 -0
- package/dist/src/dsh-runtime.js.map +1 -1
- package/dist/src/index.d.ts +8 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/installed-graph.d.ts.map +1 -1
- package/dist/src/installed-graph.js +105 -26
- package/dist/src/installed-graph.js.map +1 -1
- package/dist/src/radar-types.d.ts +15 -0
- package/dist/src/radar-types.d.ts.map +1 -1
- package/dist/src/semver.d.ts.map +1 -1
- package/dist/src/semver.js +7 -1
- package/dist/src/semver.js.map +1 -1
- package/dist/src/tar.d.ts.map +1 -1
- package/dist/src/tar.js +28 -13
- package/dist/src/tar.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.zh-CN.md +25 -23
- package/package.json +3 -1
- package/schemas/dsh-compatibility-ir.schema.json +96 -0
- package/schemas/dsh-install-observation.schema.json +141 -2
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { constants } from 'node:fs';
|
|
4
|
-
import { lstat, mkdir, mkdtemp, open, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { lstat, mkdir, mkdtemp, open, readdir, realpath, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
|
-
import { basename, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { parsePnpmLockGraph } from './graph.js';
|
|
9
|
+
import { discoverDshRuntimeHostNodeModulesDirectory, discoverDshRuntimePackage, discoverDshRuntimePackageDirectory, } from './dsh-runtime.js';
|
|
10
|
+
import { parseInstalledNodeModulesGraph } from './installed-graph.js';
|
|
7
11
|
import { parseNpmSpec } from './npm.js';
|
|
8
12
|
import { satisfiesSemverRange } from './semver.js';
|
|
9
13
|
import { parseNpmTarball } from './tar.js';
|
|
@@ -12,6 +16,7 @@ export const DSH_INSTALL_OBSERVATION_SCHEMA = 'upstream-radar.dsh-install-observ
|
|
|
12
16
|
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
13
17
|
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
14
18
|
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
19
|
+
const MAX_ARTIFACT_UNPACKED_BYTES = 192 * 1024 * 1024;
|
|
15
20
|
const MAX_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
16
21
|
const MAX_REPORT_DETAIL_BYTES = 4 * 1024;
|
|
17
22
|
const MAX_TRACE_BYTES = 16 * 1024 * 1024;
|
|
@@ -20,8 +25,19 @@ const MAX_TRACE_EVENTS = 512;
|
|
|
20
25
|
const MAX_SNAPSHOT_ENTRIES = 25_000;
|
|
21
26
|
const MAX_DIFF_PATHS = 512;
|
|
22
27
|
const MAX_ALLOWED_BUILDS = 16;
|
|
28
|
+
const MAX_PROFILE_LOCKFILE_BYTES = 16 * 1024 * 1024;
|
|
29
|
+
const MAX_PROFILE_GRAPH_GAPS = 32;
|
|
30
|
+
const MAX_PLUGIN_PEERS = 64;
|
|
31
|
+
const MAX_STATIC_PEER_SCAN_BYTES = 8 * 1024 * 1024;
|
|
32
|
+
const MAX_RUNTIME_DISCOVERY_DIRECTORIES = 12_000;
|
|
33
|
+
const MAX_RUNTIME_DISCOVERY_DEPTH = 16;
|
|
23
34
|
const PROFILE = 'headless';
|
|
24
35
|
const LIFECYCLE_SCRIPTS = ['preinstall', 'install', 'postinstall', 'prepare'];
|
|
36
|
+
const PROFILE_LOCKFILE_CANDIDATES = [
|
|
37
|
+
['pnpm-lock.yaml'],
|
|
38
|
+
['node_modules', '.pnpm', 'lock.yaml'],
|
|
39
|
+
];
|
|
40
|
+
const SYNTHETIC_PROFILE_GRAPH_ROOT = { name: 'dsh-profile-headless', version: '0.0.0' };
|
|
25
41
|
function normalizeAllowedBuilds(values) {
|
|
26
42
|
if (values === undefined)
|
|
27
43
|
return [];
|
|
@@ -37,6 +53,127 @@ function normalizeAllowedBuilds(values) {
|
|
|
37
53
|
}
|
|
38
54
|
return [...names].sort();
|
|
39
55
|
}
|
|
56
|
+
function isNpmPackageName(value) {
|
|
57
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/.test(value);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* pnpm 10+ deliberately stops before running dependency lifecycle scripts
|
|
61
|
+
* unless each package is approved. Keep that policy gate distinct from a
|
|
62
|
+
* plugin install defect, but only when pnpm supplied a bounded, exact list.
|
|
63
|
+
*/
|
|
64
|
+
function requiredDependencyBuilds(output, artifactName) {
|
|
65
|
+
const normalized = output.replace(/\r\n?/g, '\n');
|
|
66
|
+
const marker = '[ERR_PNPM_IGNORED_BUILDS]';
|
|
67
|
+
const markerIndex = normalized.indexOf(marker);
|
|
68
|
+
if (markerIndex < 0)
|
|
69
|
+
return [];
|
|
70
|
+
const tail = normalized.slice(markerIndex, markerIndex + MAX_COMMAND_OUTPUT_BYTES);
|
|
71
|
+
const prefix = 'Ignored build scripts:';
|
|
72
|
+
const prefixIndex = tail.indexOf(prefix);
|
|
73
|
+
if (prefixIndex < marker.length || prefixIndex > 256)
|
|
74
|
+
return [];
|
|
75
|
+
let list = tail.slice(prefixIndex + prefix.length);
|
|
76
|
+
const terminator = list.search(/\n\s*\n|\n\s*Run\s+["']pnpm approve-builds["']/);
|
|
77
|
+
if (terminator >= 0)
|
|
78
|
+
list = list.slice(0, terminator);
|
|
79
|
+
const coordinates = list.split(',').map(value => value.trim()).filter(Boolean);
|
|
80
|
+
if (coordinates.length === 0 || coordinates.length > MAX_ALLOWED_BUILDS)
|
|
81
|
+
return [];
|
|
82
|
+
const names = new Set();
|
|
83
|
+
for (const coordinate of coordinates) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = parseNpmSpec(coordinate);
|
|
86
|
+
if (!EXACT_VERSION.test(parsed.version) || !isNpmPackageName(parsed.name))
|
|
87
|
+
return [];
|
|
88
|
+
names.add(parsed.name);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// When the exact reviewed tarball itself has a lifecycle script, pnpm
|
|
92
|
+
// prints its profile-relative file: coordinate alongside registry
|
|
93
|
+
// dependencies. Accept only the already-established artifact identity;
|
|
94
|
+
// an arbitrary local coordinate must not become an approval suggestion.
|
|
95
|
+
const prefix = `${artifactName}@file:`;
|
|
96
|
+
const localPath = coordinate.startsWith(prefix) ? coordinate.slice(prefix.length) : '';
|
|
97
|
+
if (localPath === '' || localPath.length > 4_096 || /[\u0000-\u0020\u007f]/.test(localPath))
|
|
98
|
+
return [];
|
|
99
|
+
names.add(artifactName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...names].sort();
|
|
103
|
+
}
|
|
104
|
+
function staticPeerUsage(entries, requirements) {
|
|
105
|
+
const state = new Map(requirements.map(requirement => [requirement.name, 'no-literal-reference-observed']));
|
|
106
|
+
let scanned = 0;
|
|
107
|
+
let incomplete = false;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entry.type !== 'file' || entry.contents === undefined
|
|
110
|
+
|| !/\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(entry.path))
|
|
111
|
+
continue;
|
|
112
|
+
if (scanned + entry.contents.length > MAX_STATIC_PEER_SCAN_BYTES) {
|
|
113
|
+
incomplete = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
scanned += entry.contents.length;
|
|
117
|
+
const text = entry.contents.toString('utf8');
|
|
118
|
+
const declarationFile = /\.d\.(?:[cm]?ts)$/i.test(entry.path);
|
|
119
|
+
for (const requirement of requirements) {
|
|
120
|
+
if (state.get(requirement.name) === 'runtime-import-observed')
|
|
121
|
+
continue;
|
|
122
|
+
const escaped = requirement.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
123
|
+
const literal = `['"]${escaped}(?:/[^'"]*)?['"]`;
|
|
124
|
+
// Stay within one statement line. A wider expression can start at an
|
|
125
|
+
// unrelated runtime import and accidentally consume a later `import
|
|
126
|
+
// type`, which would turn declaration-only evidence into a false runtime
|
|
127
|
+
// claim. Missing a heavily formatted import is safer than that claim.
|
|
128
|
+
const typeOnly = new RegExp(`(?:^|[;\\n])[\\t ]*import[\\t ]+type\\b[^;\\n]{0,1024}?\\bfrom[\\t ]*${literal}`);
|
|
129
|
+
const runtime = new RegExp([
|
|
130
|
+
`(?:^|[;\\n])[\\t ]*import[\\t ]+(?!type\\b)(?:[^;\\n]{0,1024}?[\\t ]+from[\\t ]+)?${literal}`,
|
|
131
|
+
`(?:^|[;\\n])[\\t ]*export[\\t ]+(?!type\\b)[^;\\n]{0,1024}?[\\t ]+from[\\t ]+${literal}`,
|
|
132
|
+
`\\b(?:require|import)\\s*\\(\\s*${literal}`,
|
|
133
|
+
].join('|'));
|
|
134
|
+
const literalReference = new RegExp(literal);
|
|
135
|
+
if (!declarationFile && runtime.test(text)) {
|
|
136
|
+
state.set(requirement.name, 'runtime-import-observed');
|
|
137
|
+
}
|
|
138
|
+
else if (declarationFile ? literalReference.test(text) : typeOnly.test(text)) {
|
|
139
|
+
state.set(requirement.name, 'type-only-reference-observed');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return requirements.map(requirement => {
|
|
144
|
+
const observed = state.get(requirement.name) ?? 'no-literal-reference-observed';
|
|
145
|
+
return observed === 'no-literal-reference-observed' && incomplete ? 'scan-incomplete' : observed;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function requiredPeerDependencies(manifest, entries) {
|
|
149
|
+
const peers = typeof manifest.peerDependencies === 'object' && manifest.peerDependencies !== null && !Array.isArray(manifest.peerDependencies)
|
|
150
|
+
? manifest.peerDependencies
|
|
151
|
+
: {};
|
|
152
|
+
const metadata = typeof manifest.peerDependenciesMeta === 'object' && manifest.peerDependenciesMeta !== null && !Array.isArray(manifest.peerDependenciesMeta)
|
|
153
|
+
? manifest.peerDependenciesMeta
|
|
154
|
+
: {};
|
|
155
|
+
const requirements = [];
|
|
156
|
+
for (const [name, rawRange] of Object.entries(peers).sort(([left], [right]) => left.localeCompare(right))) {
|
|
157
|
+
if (!isNpmPackageName(name))
|
|
158
|
+
throw new Error(`packed artifact has an invalid peer dependency name: ${name}`);
|
|
159
|
+
if (typeof rawRange !== 'string' || rawRange.trim() === '' || rawRange.length > 512) {
|
|
160
|
+
throw new Error(`packed artifact has an invalid peer dependency range for ${name}`);
|
|
161
|
+
}
|
|
162
|
+
const peerMetadata = metadata[name];
|
|
163
|
+
const optional = typeof peerMetadata === 'object' && peerMetadata !== null && !Array.isArray(peerMetadata)
|
|
164
|
+
&& peerMetadata.optional === true;
|
|
165
|
+
if (!optional)
|
|
166
|
+
requirements.push({ name, required: bounded(rawRange.trim(), 512) });
|
|
167
|
+
}
|
|
168
|
+
if (requirements.length > MAX_PLUGIN_PEERS) {
|
|
169
|
+
throw new Error(`packed artifact declares more than ${MAX_PLUGIN_PEERS} required peer dependencies`);
|
|
170
|
+
}
|
|
171
|
+
const usages = staticPeerUsage(entries, requirements);
|
|
172
|
+
return requirements.map((requirement, index) => ({
|
|
173
|
+
...requirement,
|
|
174
|
+
staticUsage: usages[index] ?? 'scan-incomplete',
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
40
177
|
function bounded(value, maximum = MAX_REPORT_DETAIL_BYTES) {
|
|
41
178
|
const normalized = value.replace(/[\u0000-\u001f\u007f-\u009f]/g, character => (`\\u${character.codePointAt(0)?.toString(16).padStart(4, '0') ?? '0000'}`));
|
|
42
179
|
return normalized.length <= maximum ? normalized : `${normalized.slice(0, maximum - 1)}…`;
|
|
@@ -391,18 +528,30 @@ async function readRegularFileNoFollow(path, maximum) {
|
|
|
391
528
|
async function parsePackedArtifact(result, artifactDirectory, expectedName, expectedVersion) {
|
|
392
529
|
if (commandStage(result).status !== 'passed')
|
|
393
530
|
throw new Error(commandStage(result).detail ?? 'npm pack failed');
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
531
|
+
const stdout = result.stdout.trim();
|
|
532
|
+
let filename;
|
|
533
|
+
let integrity;
|
|
534
|
+
if (stdout.startsWith('[')) {
|
|
535
|
+
let output;
|
|
536
|
+
try {
|
|
537
|
+
output = JSON.parse(stdout);
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
throw new Error('npm pack did not return valid JSON');
|
|
541
|
+
}
|
|
542
|
+
if (!Array.isArray(output) || output.length !== 1 || typeof output[0] !== 'object' || output[0] === null) {
|
|
543
|
+
throw new Error('npm pack returned an unexpected result');
|
|
544
|
+
}
|
|
545
|
+
const item = output[0];
|
|
546
|
+
filename = item.filename;
|
|
547
|
+
integrity = typeof item.integrity === 'string' ? bounded(item.integrity, 1_024) : undefined;
|
|
400
548
|
}
|
|
401
|
-
|
|
402
|
-
|
|
549
|
+
else {
|
|
550
|
+
const lines = stdout.split(/\r?\n/);
|
|
551
|
+
if (lines.length !== 1)
|
|
552
|
+
throw new Error('npm pack returned an unexpected result');
|
|
553
|
+
filename = lines[0];
|
|
403
554
|
}
|
|
404
|
-
const item = output[0];
|
|
405
|
-
const filename = item.filename;
|
|
406
555
|
if (typeof filename !== 'string' || filename === '' || filename !== basename(filename) || !filename.endsWith('.tgz')) {
|
|
407
556
|
throw new Error('npm pack returned an unsafe artifact filename');
|
|
408
557
|
}
|
|
@@ -410,7 +559,10 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
|
|
|
410
559
|
if (!path.startsWith(`${resolve(artifactDirectory)}${sep}`))
|
|
411
560
|
throw new Error('npm pack artifact escaped its directory');
|
|
412
561
|
const compressed = await readRegularFileNoFollow(path, MAX_ARTIFACT_BYTES);
|
|
413
|
-
const parsed = parseNpmTarball(compressed
|
|
562
|
+
const parsed = parseNpmTarball(compressed, {
|
|
563
|
+
maxFileBytes: MAX_ARTIFACT_BYTES,
|
|
564
|
+
maxUnpackedBytes: MAX_ARTIFACT_UNPACKED_BYTES,
|
|
565
|
+
});
|
|
414
566
|
const manifestEntry = parsed.entries.find(entry => entry.path === 'package.json' && entry.type === 'file');
|
|
415
567
|
if (manifestEntry?.contents === undefined)
|
|
416
568
|
throw new Error('packed artifact has no package.json');
|
|
@@ -455,7 +607,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
|
|
|
455
607
|
throw new Error('packed artifact Node engine requirement exceeds 512 characters');
|
|
456
608
|
}
|
|
457
609
|
const nodeEngine = rawNodeEngine === undefined || rawNodeEngine === '' ? undefined : bounded(rawNodeEngine, 512);
|
|
458
|
-
const
|
|
610
|
+
const peerRequirements = requiredPeerDependencies(manifest, parsed.entries);
|
|
459
611
|
return {
|
|
460
612
|
path,
|
|
461
613
|
filename,
|
|
@@ -465,6 +617,7 @@ async function parsePackedArtifact(result, artifactDirectory, expectedName, expe
|
|
|
465
617
|
bundlePatch,
|
|
466
618
|
...(nodeEngine === undefined ? {} : { nodeEngine }),
|
|
467
619
|
lifecycleScripts,
|
|
620
|
+
requiredPeerDependencies: peerRequirements,
|
|
468
621
|
};
|
|
469
622
|
}
|
|
470
623
|
async function snapshotTree(root) {
|
|
@@ -602,16 +755,472 @@ async function registeredBundle(dshHome, packageName) {
|
|
|
602
755
|
return false;
|
|
603
756
|
}
|
|
604
757
|
}
|
|
758
|
+
function isNotFound(error) {
|
|
759
|
+
return typeof error === 'object' && error !== null
|
|
760
|
+
&& 'code' in error
|
|
761
|
+
&& error.code === 'ENOENT';
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* DSH versions have emitted both a project lockfile and pnpm's virtual-store
|
|
765
|
+
* lockfile. Read only those fixed descendants, refuse every symlink in their
|
|
766
|
+
* path, and never retain the lockfile contents outside the disposable runner.
|
|
767
|
+
*/
|
|
768
|
+
async function readProfileLockfile(dshHome) {
|
|
769
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
770
|
+
try {
|
|
771
|
+
const profileMetadata = await lstat(profileDirectory);
|
|
772
|
+
if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink())
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
for (const segments of PROFILE_LOCKFILE_CANDIDATES) {
|
|
779
|
+
let current = profileDirectory;
|
|
780
|
+
try {
|
|
781
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
782
|
+
const segment = segments[index];
|
|
783
|
+
current = join(current, segment);
|
|
784
|
+
const metadata = await lstat(current);
|
|
785
|
+
if (metadata.isSymbolicLink())
|
|
786
|
+
return undefined;
|
|
787
|
+
if (index < segments.length - 1 && !metadata.isDirectory())
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
return await readRegularFileNoFollow(current, MAX_PROFILE_LOCKFILE_BYTES);
|
|
791
|
+
}
|
|
792
|
+
catch (error) {
|
|
793
|
+
if (isNotFound(error))
|
|
794
|
+
continue;
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return undefined;
|
|
799
|
+
}
|
|
800
|
+
function profileGraphRoot(manifest) {
|
|
801
|
+
return typeof manifest.name === 'string' && manifest.name.length > 0
|
|
802
|
+
&& typeof manifest.version === 'string' && EXACT_VERSION.test(manifest.version)
|
|
803
|
+
? { name: manifest.name, version: manifest.version }
|
|
804
|
+
: SYNTHETIC_PROFILE_GRAPH_ROOT;
|
|
805
|
+
}
|
|
806
|
+
function graphGaps(graph) {
|
|
807
|
+
if (graph.unresolved === undefined || graph.unresolved.length === 0)
|
|
808
|
+
return undefined;
|
|
809
|
+
return graph.unresolved.slice(0, MAX_PROFILE_GRAPH_GAPS).map(gap => ({
|
|
810
|
+
from: bounded(gap.from, 512),
|
|
811
|
+
name: bounded(gap.name, 214),
|
|
812
|
+
spec: bounded(gap.spec, 512),
|
|
813
|
+
kind: gap.kind,
|
|
814
|
+
}));
|
|
815
|
+
}
|
|
816
|
+
function pluginPeerContractEvidence(contracts) {
|
|
817
|
+
const entries = [...(contracts ?? [])].sort((left, right) => left.name.localeCompare(right.name));
|
|
818
|
+
const issues = entries
|
|
819
|
+
.filter((entry) => entry.status !== 'satisfied')
|
|
820
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
821
|
+
return {
|
|
822
|
+
declared: entries.length,
|
|
823
|
+
satisfied: entries.filter(entry => entry.status === 'satisfied').length,
|
|
824
|
+
mismatched: entries.filter(entry => entry.status === 'mismatched').length,
|
|
825
|
+
indeterminate: entries.filter(entry => entry.status === 'indeterminate').length,
|
|
826
|
+
missing: entries.filter(entry => entry.status === 'missing').length,
|
|
827
|
+
relations: entries.map(entry => ({
|
|
828
|
+
name: bounded(entry.name, 214),
|
|
829
|
+
required: bounded(entry.required, 512),
|
|
830
|
+
status: entry.status,
|
|
831
|
+
staticUsage: entry.staticUsage,
|
|
832
|
+
...(entry.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(entry.resolvedVersion, 256) }),
|
|
833
|
+
})),
|
|
834
|
+
...(issues.length === 0 ? {} : {
|
|
835
|
+
issues: issues.slice(0, MAX_PROFILE_GRAPH_GAPS).map(issue => ({
|
|
836
|
+
name: bounded(issue.name, 214),
|
|
837
|
+
required: bounded(issue.required, 512),
|
|
838
|
+
status: issue.status,
|
|
839
|
+
staticUsage: issue.staticUsage,
|
|
840
|
+
...(issue.resolvedVersion === undefined ? {} : { resolvedVersion: bounded(issue.resolvedVersion, 256) }),
|
|
841
|
+
})),
|
|
842
|
+
}),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function graphGapPartitions(graph) {
|
|
846
|
+
const required = [];
|
|
847
|
+
const optional = [];
|
|
848
|
+
for (const gap of graph.unresolved ?? []) {
|
|
849
|
+
if (gap.kind === 'optional')
|
|
850
|
+
optional.push(gap);
|
|
851
|
+
else
|
|
852
|
+
required.push(gap);
|
|
853
|
+
}
|
|
854
|
+
return { required, optional };
|
|
855
|
+
}
|
|
856
|
+
function isLexicallyInside(root, candidate) {
|
|
857
|
+
const child = relative(resolve(root), resolve(candidate));
|
|
858
|
+
return child === '' || (!child.startsWith(`..${sep}`) && child !== '..');
|
|
859
|
+
}
|
|
860
|
+
const PROFILE_PEER_RESOLUTION_SCHEMA = 'upstream-radar.profile-peer-resolution/v1alpha1';
|
|
861
|
+
function indeterminatePeerContracts(requirements) {
|
|
862
|
+
return requirements.map(requirement => ({ ...requirement, status: 'indeterminate' }));
|
|
863
|
+
}
|
|
864
|
+
async function peerVersionFromResolvedModule(url, expectedName, sandboxRoot) {
|
|
865
|
+
let resolvedModule;
|
|
866
|
+
try {
|
|
867
|
+
const parsed = new URL(url);
|
|
868
|
+
if (parsed.protocol !== 'file:')
|
|
869
|
+
return undefined;
|
|
870
|
+
resolvedModule = await realpath(fileURLToPath(parsed));
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
return undefined;
|
|
874
|
+
}
|
|
875
|
+
let sandboxReal;
|
|
876
|
+
try {
|
|
877
|
+
sandboxReal = await realpath(sandboxRoot);
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
return undefined;
|
|
881
|
+
}
|
|
882
|
+
if (!isLexicallyInside(sandboxReal, resolvedModule))
|
|
883
|
+
return undefined;
|
|
884
|
+
let cursor = dirname(resolvedModule);
|
|
885
|
+
for (let depth = 0; depth < 32 && isLexicallyInside(sandboxReal, cursor); depth += 1) {
|
|
886
|
+
try {
|
|
887
|
+
const manifest = JSON.parse((await readRegularFileNoFollow(join(cursor, 'package.json'), 1 * 1024 * 1024)).toString('utf8'));
|
|
888
|
+
if (typeof manifest === 'object' && manifest !== null && !Array.isArray(manifest)) {
|
|
889
|
+
const item = manifest;
|
|
890
|
+
if (item.name === expectedName && typeof item.version === 'string' && EXACT_VERSION.test(item.version))
|
|
891
|
+
return item.version;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
// Continue toward the package root; a module may live in a nested directory.
|
|
896
|
+
}
|
|
897
|
+
const parent = dirname(cursor);
|
|
898
|
+
if (parent === cursor)
|
|
899
|
+
break;
|
|
900
|
+
cursor = parent;
|
|
901
|
+
}
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
async function readProfilePeerContracts(path, requirements, sandboxRoot) {
|
|
905
|
+
if (requirements.length === 0)
|
|
906
|
+
return [];
|
|
907
|
+
let records;
|
|
908
|
+
try {
|
|
909
|
+
const raw = JSON.parse((await readRegularFileNoFollow(path, 64 * 1024)).toString('utf8'));
|
|
910
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
911
|
+
return indeterminatePeerContracts(requirements);
|
|
912
|
+
const item = raw;
|
|
913
|
+
if (item.schema !== PROFILE_PEER_RESOLUTION_SCHEMA || !Array.isArray(item.peers) || item.peers.length !== requirements.length) {
|
|
914
|
+
return indeterminatePeerContracts(requirements);
|
|
915
|
+
}
|
|
916
|
+
records = item.peers.map((value) => {
|
|
917
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
918
|
+
throw new Error('invalid peer resolver record');
|
|
919
|
+
const peer = value;
|
|
920
|
+
if (typeof peer.name !== 'string' || (peer.status !== 'resolved' && peer.status !== 'missing')) {
|
|
921
|
+
throw new Error('invalid peer resolver record');
|
|
922
|
+
}
|
|
923
|
+
if (peer.status === 'resolved' && (typeof peer.url !== 'string' || peer.url.length === 0 || peer.url.length > 4_096)) {
|
|
924
|
+
throw new Error('resolved peer has no bounded URL');
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
name: peer.name,
|
|
928
|
+
status: peer.status,
|
|
929
|
+
...(typeof peer.url === 'string' ? { url: peer.url } : {}),
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
return indeterminatePeerContracts(requirements);
|
|
935
|
+
}
|
|
936
|
+
const byName = new Map(records.map(record => [record.name, record]));
|
|
937
|
+
if (byName.size !== requirements.length || requirements.some(requirement => !byName.has(requirement.name))) {
|
|
938
|
+
return indeterminatePeerContracts(requirements);
|
|
939
|
+
}
|
|
940
|
+
const contracts = [];
|
|
941
|
+
for (const requirement of requirements) {
|
|
942
|
+
const record = byName.get(requirement.name);
|
|
943
|
+
if (record === undefined || record.status === 'missing') {
|
|
944
|
+
contracts.push({ ...requirement, status: 'missing' });
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const resolvedVersion = record.url === undefined
|
|
948
|
+
? undefined
|
|
949
|
+
: await peerVersionFromResolvedModule(record.url, requirement.name, sandboxRoot);
|
|
950
|
+
if (resolvedVersion === undefined) {
|
|
951
|
+
contracts.push({ ...requirement, status: 'indeterminate' });
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const evaluation = satisfiesSemverRange(resolvedVersion, requirement.required);
|
|
955
|
+
contracts.push({
|
|
956
|
+
...requirement,
|
|
957
|
+
status: evaluation === true ? 'satisfied' : evaluation === false ? 'mismatched' : 'indeterminate',
|
|
958
|
+
resolvedVersion,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
return contracts;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Resolve direct peer contracts from the profile anchor, then boot the actual
|
|
965
|
+
* composed DSH profile. A DSH bundle does not have to export its package root:
|
|
966
|
+
* its patch may load one or more package subpaths (or a command adapter).
|
|
967
|
+
* Importing the root here would therefore test a contract DSH never declared.
|
|
968
|
+
*/
|
|
969
|
+
async function writeProfileLoadProbe(dshHome, dshVersion, pnpmCommand, peerRequirements) {
|
|
970
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
971
|
+
const profileMetadata = await lstat(profileDirectory);
|
|
972
|
+
if (!profileMetadata.isDirectory() || profileMetadata.isSymbolicLink()) {
|
|
973
|
+
throw new Error('the DSH profile directory is not a regular directory for the load probe');
|
|
974
|
+
}
|
|
975
|
+
const [homeReal, profileReal] = await Promise.all([realpath(dshHome), realpath(profileDirectory)]);
|
|
976
|
+
if (!isLexicallyInside(homeReal, profileReal)) {
|
|
977
|
+
throw new Error('the DSH profile directory escaped the controlled DSH home');
|
|
978
|
+
}
|
|
979
|
+
const probePath = join(profileDirectory, '.upstream-radar-load-probe.mjs');
|
|
980
|
+
const peerResolutionPath = join(profileDirectory, '.upstream-radar-peer-resolution.json');
|
|
981
|
+
const dshBootArgs = dshArgs(dshVersion, ['--profile', PROFILE, '--help']);
|
|
982
|
+
const contents = [
|
|
983
|
+
"import { spawn } from 'node:child_process'",
|
|
984
|
+
"import { writeFile } from 'node:fs/promises'",
|
|
985
|
+
`const peerRequirements = ${JSON.stringify(peerRequirements)}`,
|
|
986
|
+
'const peers = []',
|
|
987
|
+
'for (const peer of peerRequirements) {',
|
|
988
|
+
' try { peers.push({ name: peer.name, status: \'resolved\', url: import.meta.resolve(peer.name) }) }',
|
|
989
|
+
' catch { peers.push({ name: peer.name, status: \'missing\' }) }',
|
|
990
|
+
'}',
|
|
991
|
+
`await writeFile(${JSON.stringify(peerResolutionPath)}, JSON.stringify({ schema: ${JSON.stringify(PROFILE_PEER_RESOLUTION_SCHEMA)}, peers }), { encoding: 'utf8', mode: 0o600, flag: 'wx' })`,
|
|
992
|
+
`const child = spawn(${JSON.stringify(pnpmCommand)}, ${JSON.stringify(dshBootArgs)}, { cwd: process.cwd(), env: process.env, stdio: 'inherit' })`,
|
|
993
|
+
"const exitCode = await new Promise(resolve => {",
|
|
994
|
+
" child.once('error', error => { console.error(error.message); resolve(1) })",
|
|
995
|
+
" child.once('close', code => resolve(code ?? 1))",
|
|
996
|
+
'})',
|
|
997
|
+
'process.exitCode = exitCode',
|
|
998
|
+
'',
|
|
999
|
+
].join('\n');
|
|
1000
|
+
// Never overwrite a path a target package may have planted in the profile.
|
|
1001
|
+
await writeFile(probePath, contents, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
1002
|
+
return { path: probePath, peerResolutionPath, profileDirectory };
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* `pnpm dlx` owns the DSH host dependency plane, rather than a plugin-created
|
|
1006
|
+
* symlink in the profile. Discover the exact cached DSH package by manifest,
|
|
1007
|
+
* with bounded directory traversal and no symlink following.
|
|
1008
|
+
*/
|
|
1009
|
+
async function discoverExactDshRuntime(cacheHome, dshVersion) {
|
|
1010
|
+
const root = resolve(cacheHome, 'pnpm', 'dlx');
|
|
1011
|
+
const rootReal = await realpath(root);
|
|
1012
|
+
const queue = [{ path: root, depth: 0 }];
|
|
1013
|
+
const candidates = new Set();
|
|
1014
|
+
let visited = 0;
|
|
1015
|
+
while (queue.length > 0) {
|
|
1016
|
+
const current = queue.shift();
|
|
1017
|
+
if (current === undefined)
|
|
1018
|
+
break;
|
|
1019
|
+
if (visited >= MAX_RUNTIME_DISCOVERY_DIRECTORIES) {
|
|
1020
|
+
throw new Error(`DSH runtime discovery exceeds ${MAX_RUNTIME_DISCOVERY_DIRECTORIES} directories`);
|
|
1021
|
+
}
|
|
1022
|
+
visited += 1;
|
|
1023
|
+
let entries;
|
|
1024
|
+
try {
|
|
1025
|
+
entries = await readdir(current.path, { withFileTypes: true });
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
if (isNotFound(error))
|
|
1029
|
+
continue;
|
|
1030
|
+
throw error;
|
|
1031
|
+
}
|
|
1032
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
1033
|
+
const child = join(current.path, entry.name);
|
|
1034
|
+
if (entry.isDirectory()) {
|
|
1035
|
+
if (current.depth < MAX_RUNTIME_DISCOVERY_DEPTH)
|
|
1036
|
+
queue.push({ path: child, depth: current.depth + 1 });
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
if (!entry.isFile() || entry.name !== 'package.json')
|
|
1040
|
+
continue;
|
|
1041
|
+
let manifest;
|
|
1042
|
+
try {
|
|
1043
|
+
manifest = JSON.parse((await readRegularFileNoFollow(child, 1 * 1024 * 1024)).toString('utf8'));
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof manifest !== 'object' || manifest === null || Array.isArray(manifest))
|
|
1049
|
+
continue;
|
|
1050
|
+
const item = manifest;
|
|
1051
|
+
if (item.name === '@deepseek-ai/dsh' && item.version === dshVersion)
|
|
1052
|
+
candidates.add(child);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (candidates.size === 0)
|
|
1056
|
+
throw new Error(`exact @deepseek-ai/dsh@${dshVersion} was not found in the controlled pnpm dlx cache`);
|
|
1057
|
+
if (candidates.size > 1)
|
|
1058
|
+
throw new Error(`multiple exact @deepseek-ai/dsh@${dshVersion} packages were found in the controlled pnpm dlx cache`);
|
|
1059
|
+
const manifestPath = [...candidates][0];
|
|
1060
|
+
const packageDirectory = discoverDshRuntimePackageDirectory(manifestPath);
|
|
1061
|
+
const packageCoordinate = discoverDshRuntimePackage(manifestPath);
|
|
1062
|
+
const nodeModulesDirectory = discoverDshRuntimeHostNodeModulesDirectory(manifestPath);
|
|
1063
|
+
if (packageDirectory === undefined || packageCoordinate === undefined || nodeModulesDirectory === undefined) {
|
|
1064
|
+
throw new Error('the exact DSH package did not expose a usable dependency plane');
|
|
1065
|
+
}
|
|
1066
|
+
if (!isLexicallyInside(rootReal, packageDirectory) || !isLexicallyInside(rootReal, nodeModulesDirectory)) {
|
|
1067
|
+
throw new Error('the exact DSH dependency plane escaped the controlled pnpm dlx cache');
|
|
1068
|
+
}
|
|
1069
|
+
if (packageCoordinate.name !== '@deepseek-ai/dsh' || packageCoordinate.version !== dshVersion) {
|
|
1070
|
+
throw new Error('the discovered DSH package does not match the requested exact version');
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
packageDirectory,
|
|
1074
|
+
nodeModulesDirectory,
|
|
1075
|
+
package: { ecosystem: 'npm', name: '@deepseek-ai/dsh', version: packageCoordinate.version },
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function profileResolutionEvidence(dshHome) {
|
|
1079
|
+
const profileDirectory = join(dshHome, 'profiles', PROFILE);
|
|
1080
|
+
const lockfile = await readProfileLockfile(dshHome);
|
|
1081
|
+
if (lockfile === undefined)
|
|
1082
|
+
return {};
|
|
1083
|
+
const profileLockfile = {
|
|
1084
|
+
sha256: createHash('sha256').update(lockfile).digest('hex'),
|
|
1085
|
+
bytes: lockfile.length,
|
|
1086
|
+
};
|
|
1087
|
+
try {
|
|
1088
|
+
const manifestBuffer = await readRegularFileNoFollow(join(profileDirectory, 'package.json'), 4 * 1024 * 1024);
|
|
1089
|
+
const manifest = JSON.parse(manifestBuffer.toString('utf8'));
|
|
1090
|
+
const graph = parsePnpmLockGraph(lockfile.toString('utf8'), profileGraphRoot(manifest));
|
|
1091
|
+
const gaps = graphGaps(graph);
|
|
1092
|
+
return {
|
|
1093
|
+
profileLockfile: {
|
|
1094
|
+
...profileLockfile,
|
|
1095
|
+
...(graph.digest === undefined ? {} : { graphDigest: graph.digest }),
|
|
1096
|
+
nodes: graph.nodes.length,
|
|
1097
|
+
edges: graph.edges.length,
|
|
1098
|
+
unresolved: graph.unresolved?.length ?? 0,
|
|
1099
|
+
...(gaps === undefined ? {} : { unresolvedDependencies: gaps }),
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
return { profileLockfile };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
async function runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
|
|
1108
|
+
try {
|
|
1109
|
+
const dshRuntime = await discoverExactDshRuntime(cacheHome, dshVersion);
|
|
1110
|
+
const graph = await parseInstalledNodeModulesGraph(join(dshHome, 'profiles', PROFILE), rootPackage, {
|
|
1111
|
+
hostNodeModulesDirectory: dshRuntime.nodeModulesDirectory,
|
|
1112
|
+
hostRuntimeSource: 'dsh-process',
|
|
1113
|
+
hostRuntimePackage: dshRuntime.package,
|
|
1114
|
+
hostRuntimePackageDirectory: dshRuntime.packageDirectory,
|
|
1115
|
+
});
|
|
1116
|
+
if (graph.digest === undefined)
|
|
1117
|
+
return {};
|
|
1118
|
+
const gaps = graphGapPartitions(graph);
|
|
1119
|
+
const contracts = profileResolvedPeerContracts ?? graph.rootPeerContracts?.map(contract => ({
|
|
1120
|
+
...contract,
|
|
1121
|
+
staticUsage: 'scan-incomplete',
|
|
1122
|
+
}));
|
|
1123
|
+
const concretelyResolvedRootPeers = new Set((contracts ?? [])
|
|
1124
|
+
.filter(contract => contract.status === 'satisfied' || contract.status === 'mismatched')
|
|
1125
|
+
.map(contract => contract.name));
|
|
1126
|
+
const effectiveRequiredGaps = gaps.required.filter(gap => !(gap.from === graph.rootNodeId
|
|
1127
|
+
&& gap.kind === 'peer'
|
|
1128
|
+
&& concretelyResolvedRootPeers.has(gap.name)));
|
|
1129
|
+
const requiredGaps = graphGaps({ unresolved: effectiveRequiredGaps });
|
|
1130
|
+
const optionalGaps = graphGaps({ unresolved: gaps.optional });
|
|
1131
|
+
return {
|
|
1132
|
+
runtimeGraph: {
|
|
1133
|
+
digest: graph.digest,
|
|
1134
|
+
nodes: graph.nodes.length,
|
|
1135
|
+
edges: graph.edges.length,
|
|
1136
|
+
unresolved: effectiveRequiredGaps.length,
|
|
1137
|
+
...(requiredGaps === undefined ? {} : { unresolvedDependencies: requiredGaps }),
|
|
1138
|
+
...(gaps.optional.length === 0 ? {} : {
|
|
1139
|
+
optionalUnavailable: gaps.optional.length,
|
|
1140
|
+
...(optionalGaps === undefined ? {} : { optionalUnavailableDependencies: optionalGaps }),
|
|
1141
|
+
}),
|
|
1142
|
+
pluginPeerContracts: pluginPeerContractEvidence(contracts),
|
|
1143
|
+
...(graph.hostRuntime === undefined ? {} : {
|
|
1144
|
+
hostRuntime: {
|
|
1145
|
+
source: graph.hostRuntime.source,
|
|
1146
|
+
resolvedNodes: graph.hostRuntime.resolvedNodes,
|
|
1147
|
+
...(graph.hostRuntime.package === undefined ? {} : { dshVersion: graph.hostRuntime.package.version }),
|
|
1148
|
+
},
|
|
1149
|
+
}),
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
catch (error) {
|
|
1154
|
+
return {
|
|
1155
|
+
runtimeGraphError: bounded(error instanceof Error ? error.message : String(error), 512),
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
async function resolutionEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts) {
|
|
1160
|
+
const [profile, runtime] = await Promise.all([
|
|
1161
|
+
profileResolutionEvidence(dshHome),
|
|
1162
|
+
runtimeGraphEvidence(dshHome, rootPackage, dshVersion, cacheHome, profileResolvedPeerContracts),
|
|
1163
|
+
]);
|
|
1164
|
+
return { ...profile, ...runtime };
|
|
1165
|
+
}
|
|
605
1166
|
function finishReport(report, result, reason) {
|
|
606
1167
|
report.completedAt = new Date().toISOString();
|
|
607
1168
|
report.result = result;
|
|
608
1169
|
report.reason = reason;
|
|
609
1170
|
return report;
|
|
610
1171
|
}
|
|
1172
|
+
/**
|
|
1173
|
+
* A load success is necessary but not enough for the compatibility claim. The
|
|
1174
|
+
* final verdict also requires a complete required-edge graph and a direct
|
|
1175
|
+
* plugin peer contract that the observer could actually evaluate.
|
|
1176
|
+
*/
|
|
1177
|
+
function finalCompatibilityConclusion(resolution) {
|
|
1178
|
+
const graph = resolution.runtimeGraph;
|
|
1179
|
+
if (graph === undefined) {
|
|
1180
|
+
return {
|
|
1181
|
+
result: 'unknown',
|
|
1182
|
+
reason: resolution.runtimeGraphError === undefined
|
|
1183
|
+
? 'the exact artifact installed and loaded, but the effective DSH runtime graph was not established'
|
|
1184
|
+
: `the exact artifact installed and loaded, but the effective DSH runtime graph could not be established: ${resolution.runtimeGraphError}`,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
const contracts = graph.pluginPeerContracts;
|
|
1188
|
+
const firstIssue = contracts.issues?.[0];
|
|
1189
|
+
if (contracts.mismatched > 0 || contracts.missing > 0) {
|
|
1190
|
+
const detail = firstIssue === undefined
|
|
1191
|
+
? `${contracts.mismatched + contracts.missing} required plugin peer contract(s) do not match the DSH runtime`
|
|
1192
|
+
: firstIssue.status === 'missing'
|
|
1193
|
+
? `${firstIssue.name}@${firstIssue.required} was not resolved by the DSH runtime (${firstIssue.staticUsage})`
|
|
1194
|
+
: `${firstIssue.name}@${firstIssue.resolvedVersion ?? 'unknown'} does not satisfy ${firstIssue.required} (${firstIssue.staticUsage})`;
|
|
1195
|
+
return {
|
|
1196
|
+
result: 'peer-contract-incompatible',
|
|
1197
|
+
reason: `the exact artifact installed and loaded, but ${detail}`,
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
if (graph.unresolved > 0) {
|
|
1201
|
+
return {
|
|
1202
|
+
result: 'unknown',
|
|
1203
|
+
reason: `the exact artifact installed and loaded, but the effective DSH runtime graph has ${graph.unresolved} required unresolved edge(s)`,
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
if (contracts.indeterminate > 0) {
|
|
1207
|
+
return {
|
|
1208
|
+
result: 'unknown',
|
|
1209
|
+
reason: `the exact artifact installed and loaded, but ${contracts.indeterminate} required plugin peer range(s) could not be evaluated safely`,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
result: 'compatible',
|
|
1214
|
+
reason: 'the exact artifact installed, registered, loaded, and satisfied its direct peer contracts under the requested DSH version',
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
611
1217
|
export async function observeDshPluginInstall(options) {
|
|
612
1218
|
const spec = parseNpmSpec(options.packageSpec);
|
|
613
1219
|
if (!EXACT_VERSION.test(options.dshVersion))
|
|
614
1220
|
throw new Error('DSH version must be an exact semantic version');
|
|
1221
|
+
if (options.caseId !== undefined && !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(options.caseId)) {
|
|
1222
|
+
throw new Error('DSH install observation caseId must be a short lowercase label');
|
|
1223
|
+
}
|
|
615
1224
|
const allowedBuilds = normalizeAllowedBuilds(options.allowedBuilds);
|
|
616
1225
|
if (!options.allowExecution)
|
|
617
1226
|
throw new Error('DSH install observation requires explicit execution consent');
|
|
@@ -637,6 +1246,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
637
1246
|
scope: 'install-and-load-behavior',
|
|
638
1247
|
startedAt,
|
|
639
1248
|
completedAt: startedAt,
|
|
1249
|
+
...(options.caseId === undefined ? {} : { caseId: options.caseId }),
|
|
640
1250
|
dshVersion: options.dshVersion,
|
|
641
1251
|
runtime: {
|
|
642
1252
|
platform: process.platform,
|
|
@@ -660,6 +1270,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
660
1270
|
},
|
|
661
1271
|
observations: { install: emptyTraceObservation(), load: emptyTraceObservation() },
|
|
662
1272
|
filesystem: { install: emptyFilesystemDiff(), load: emptyFilesystemDiff() },
|
|
1273
|
+
resolution: {},
|
|
663
1274
|
result: 'unknown',
|
|
664
1275
|
reason: 'observation did not complete',
|
|
665
1276
|
boundary: {
|
|
@@ -670,6 +1281,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
670
1281
|
pluginCodeMayExecuteDuringLoad: true,
|
|
671
1282
|
inheritedHostSecrets: false,
|
|
672
1283
|
approvedDependencyBuilds: allowedBuilds,
|
|
1284
|
+
requiredDependencyBuilds: [],
|
|
673
1285
|
note: 'Radar scrubs the child environment and records Linux system-call evidence, but the caller provides and must verify the disposable isolation boundary. Same-container traces are best-effort evidence, not a malicious-code safety certificate.',
|
|
674
1286
|
},
|
|
675
1287
|
};
|
|
@@ -719,7 +1331,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
719
1331
|
const artifactResult = await runSafely(runner, {
|
|
720
1332
|
phase: 'artifact',
|
|
721
1333
|
command: npmCommand,
|
|
722
|
-
args: ['pack', `${spec.name}@${spec.version}`, '--ignore-scripts', '--
|
|
1334
|
+
args: ['pack', `${spec.name}@${spec.version}`, '--ignore-scripts', '--pack-destination', '.', '--silent'],
|
|
723
1335
|
cwd: artifactDirectory,
|
|
724
1336
|
env: noScriptsEnvironment,
|
|
725
1337
|
timeoutMs,
|
|
@@ -788,6 +1400,7 @@ export async function observeDshPluginInstall(options) {
|
|
|
788
1400
|
const afterInstall = await snapshotSandbox(sandboxRoot, environment);
|
|
789
1401
|
report.observations.install = await readTrace(installTracePath, sandboxRoot);
|
|
790
1402
|
report.filesystem.install = diffSnapshots(beforeInstall, afterInstall);
|
|
1403
|
+
report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME);
|
|
791
1404
|
report.stages.install = commandStage(installResult);
|
|
792
1405
|
if (installResult.timedOut || installResult.outputExceeded || installResult.launchError !== undefined) {
|
|
793
1406
|
return finishReport(report, 'unknown', 'the plugin install did not produce a bounded command result');
|
|
@@ -801,6 +1414,11 @@ export async function observeDshPluginInstall(options) {
|
|
|
801
1414
|
return finishReport(report, 'unknown', 'the install command ran without readable trace evidence');
|
|
802
1415
|
}
|
|
803
1416
|
if (installResult.code !== 0) {
|
|
1417
|
+
const requiredBuilds = requiredDependencyBuilds(`${installResult.stderr}\n${installResult.stdout}`, spec.name);
|
|
1418
|
+
if (requiredBuilds.length > 0) {
|
|
1419
|
+
report.boundary.requiredDependencyBuilds = requiredBuilds;
|
|
1420
|
+
return finishReport(report, 'build-approval-required', `the DSH plugin install requires explicit approval for dependency builds: ${requiredBuilds.join(', ')}`);
|
|
1421
|
+
}
|
|
804
1422
|
return finishReport(report, 'install-failed', 'the traced DSH plugin install command failed');
|
|
805
1423
|
}
|
|
806
1424
|
const registered = await registeredBundle(environment.DSH_HOME, spec.name);
|
|
@@ -809,12 +1427,13 @@ export async function observeDshPluginInstall(options) {
|
|
|
809
1427
|
: { status: 'failed', detail: `the DSH profile did not register ${spec.name}` };
|
|
810
1428
|
if (!registered)
|
|
811
1429
|
return finishReport(report, 'install-failed', 'DSH accepted the install command but did not register the plugin bundle');
|
|
1430
|
+
const loadProbe = await writeProfileLoadProbe(environment.DSH_HOME, options.dshVersion, pnpmCommand, artifact.requiredPeerDependencies);
|
|
812
1431
|
const loadTracePath = join(traceDirectory, 'load.strace');
|
|
813
1432
|
const loadResult = await runSafely(runner, {
|
|
814
1433
|
phase: 'load',
|
|
815
|
-
command:
|
|
816
|
-
args:
|
|
817
|
-
cwd:
|
|
1434
|
+
command: process.execPath,
|
|
1435
|
+
args: [loadProbe.path],
|
|
1436
|
+
cwd: loadProbe.profileDirectory,
|
|
818
1437
|
env: scriptsEnvironment,
|
|
819
1438
|
timeoutMs,
|
|
820
1439
|
sandboxRoot,
|
|
@@ -823,6 +1442,10 @@ export async function observeDshPluginInstall(options) {
|
|
|
823
1442
|
const afterLoad = await snapshotSandbox(sandboxRoot, environment);
|
|
824
1443
|
report.observations.load = await readTrace(loadTracePath, sandboxRoot);
|
|
825
1444
|
report.filesystem.load = diffSnapshots(afterInstall, afterLoad);
|
|
1445
|
+
const profilePeerContracts = await readProfilePeerContracts(loadProbe.peerResolutionPath, artifact.requiredPeerDependencies, sandboxRoot);
|
|
1446
|
+
// Loading may trigger one final DSH profile reconciliation. Preserve the
|
|
1447
|
+
// final resolved graph rather than only the state immediately after add.
|
|
1448
|
+
report.resolution = await resolutionEvidence(environment.DSH_HOME, spec, options.dshVersion, environment.XDG_CACHE_HOME, profilePeerContracts);
|
|
826
1449
|
report.stages.load = commandStage(loadResult);
|
|
827
1450
|
if (loadResult.timedOut || loadResult.outputExceeded || loadResult.launchError !== undefined) {
|
|
828
1451
|
return finishReport(report, 'unknown', 'the plugin load did not produce a bounded command result');
|
|
@@ -833,7 +1456,8 @@ export async function observeDshPluginInstall(options) {
|
|
|
833
1456
|
}
|
|
834
1457
|
if (loadResult.code !== 0)
|
|
835
1458
|
return finishReport(report, 'load-failed', 'the traced DSH profile load command failed');
|
|
836
|
-
|
|
1459
|
+
const conclusion = finalCompatibilityConclusion(report.resolution);
|
|
1460
|
+
return finishReport(report, conclusion.result, conclusion.reason);
|
|
837
1461
|
}
|
|
838
1462
|
catch (error) {
|
|
839
1463
|
const detail = bounded(error instanceof Error ? error.message : String(error));
|
|
@@ -854,23 +1478,38 @@ export function renderDshInstallObservation(report) {
|
|
|
854
1478
|
const lifecycle = report.artifact.lifecycleScripts.length === 0 ? 'none' : report.artifact.lifecycleScripts.join(', ');
|
|
855
1479
|
const lines = [
|
|
856
1480
|
'DSH isolated install observation',
|
|
1481
|
+
...(report.caseId === undefined ? [] : [`Case: ${report.caseId}`]),
|
|
857
1482
|
`Artifact: ${report.artifact.name}@${report.artifact.version}${report.artifact.sha256 === undefined ? '' : ` (sha256:${report.artifact.sha256.slice(0, 12)}…)`}`,
|
|
858
1483
|
`DSH: ${report.dshVersion}`,
|
|
859
1484
|
`Runtime: Node ${report.runtime.nodeVersion} (${report.runtime.platform}/${report.runtime.architecture}), pnpm ${report.runtime.packageManager.version ?? 'unknown'}`,
|
|
860
1485
|
`Plugin Node requirement: ${report.artifact.nodeEngine ?? 'not declared'}`,
|
|
861
1486
|
`Isolation claim: ${report.boundary.isolationProviderClaim} (provided externally; not verified by Radar)`,
|
|
862
1487
|
`Approved dependency builds: ${report.boundary.approvedDependencyBuilds.length === 0 ? 'none' : report.boundary.approvedDependencyBuilds.join(', ')}`,
|
|
1488
|
+
`Additional dependency builds required: ${report.boundary.requiredDependencyBuilds.length === 0 ? 'none' : report.boundary.requiredDependencyBuilds.join(', ')}`,
|
|
863
1489
|
'',
|
|
864
1490
|
`Result: ${report.result.toUpperCase()} — ${report.reason}`,
|
|
865
1491
|
`Lifecycle scripts declared: ${lifecycle}`,
|
|
866
1492
|
`Install evidence: ${install.processes.length} process, ${install.network.length} network, ${install.fileWrites.length} file-write event(s); trace ${install.coverage.status}`,
|
|
867
1493
|
`Load evidence: ${load.processes.length} process, ${load.network.length} network, ${load.fileWrites.length} file-write event(s); trace ${load.coverage.status}`,
|
|
868
1494
|
`Final filesystem delta: install +${report.filesystem.install.totals.created} ~${report.filesystem.install.totals.modified} -${report.filesystem.install.totals.deleted}; load +${report.filesystem.load.totals.created} ~${report.filesystem.load.totals.modified} -${report.filesystem.load.totals.deleted}`,
|
|
1495
|
+
`Resolved profile graph: ${report.resolution.profileLockfile?.graphDigest ?? 'not established'}${report.resolution.profileLockfile === undefined ? '' : ` (lock sha256:${report.resolution.profileLockfile.sha256.slice(0, 12)}…)`}`,
|
|
1496
|
+
`Effective DSH runtime graph: ${report.resolution.runtimeGraph?.digest ?? 'not established'}${report.resolution.runtimeGraph === undefined ? '' : ` (${report.resolution.runtimeGraph.nodes} nodes, ${report.resolution.runtimeGraph.edges} edges, ${report.resolution.runtimeGraph.unresolved} required unresolved${report.resolution.runtimeGraph.optionalUnavailable === undefined ? '' : `, ${report.resolution.runtimeGraph.optionalUnavailable} optional unavailable`})`}`,
|
|
1497
|
+
`Plugin peer contracts: ${report.resolution.runtimeGraph === undefined
|
|
1498
|
+
? 'not established'
|
|
1499
|
+
: `${report.resolution.runtimeGraph.pluginPeerContracts.satisfied}/${report.resolution.runtimeGraph.pluginPeerContracts.declared} satisfied; ${report.resolution.runtimeGraph.pluginPeerContracts.mismatched} mismatched, ${report.resolution.runtimeGraph.pluginPeerContracts.missing} missing, ${report.resolution.runtimeGraph.pluginPeerContracts.indeterminate} indeterminate`}`,
|
|
1500
|
+
`Effective graph collector: ${report.resolution.runtimeGraphError ?? 'captured'}`,
|
|
869
1501
|
'',
|
|
870
1502
|
];
|
|
871
1503
|
for (const [name, stage] of Object.entries(report.stages)) {
|
|
872
1504
|
lines.push(` ${name}: ${stage.status}${stage.detail === undefined ? '' : ` (${stage.detail})`}`);
|
|
873
1505
|
}
|
|
1506
|
+
const peerIssues = report.resolution.runtimeGraph?.pluginPeerContracts.issues ?? [];
|
|
1507
|
+
if (peerIssues.length > 0) {
|
|
1508
|
+
lines.push('', 'Direct peer-contract findings:');
|
|
1509
|
+
for (const issue of peerIssues) {
|
|
1510
|
+
lines.push(` ${issue.name}: ${issue.status}; requires ${issue.required}${issue.resolvedVersion === undefined ? '' : `, resolved ${issue.resolvedVersion}`}; static use ${issue.staticUsage}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
874
1513
|
lines.push('', report.boundary.note);
|
|
875
1514
|
return `${lines.join('\n')}\n`;
|
|
876
1515
|
}
|