dsh-plugin-inspector 0.1.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/LICENSE +21 -0
- package/README.md +271 -0
- package/lib/checks/input.js +10 -0
- package/lib/checks/tier-a.js +632 -0
- package/lib/checks/tier-b.js +411 -0
- package/lib/checks/tier-c.js +288 -0
- package/lib/cli.js +154 -0
- package/lib/cordis-yaml.js +393 -0
- package/lib/files.js +131 -0
- package/lib/index.js +22 -0
- package/lib/injection.js +90 -0
- package/lib/inspect.js +168 -0
- package/lib/knowledge.js +321 -0
- package/lib/manifest.js +143 -0
- package/lib/model.js +55 -0
- package/lib/publish.js +208 -0
- package/lib/report.js +182 -0
- package/lib/source.js +410 -0
- package/lib/types/checks/input.d.ts +42 -0
- package/lib/types/checks/tier-a.d.ts +24 -0
- package/lib/types/checks/tier-b.d.ts +23 -0
- package/lib/types/checks/tier-c.d.ts +37 -0
- package/lib/types/cli.d.ts +56 -0
- package/lib/types/cordis-yaml.d.ts +133 -0
- package/lib/types/files.d.ts +71 -0
- package/lib/types/index.d.ts +23 -0
- package/lib/types/injection.d.ts +41 -0
- package/lib/types/inspect.d.ts +31 -0
- package/lib/types/knowledge.d.ts +137 -0
- package/lib/types/manifest.d.ts +62 -0
- package/lib/types/model.d.ts +160 -0
- package/lib/types/publish.d.ts +55 -0
- package/lib/types/report.d.ts +27 -0
- package/lib/types/source.d.ts +71 -0
- package/package.json +61 -0
package/lib/inspect.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration: decode the package once, run the three tiers over the decoded
|
|
3
|
+
* form, apply the Tier C downgrade, and assemble the report.
|
|
4
|
+
*
|
|
5
|
+
* This module is the only place that reads the package and the only place that
|
|
6
|
+
* decides confidence. Nothing here — and nothing it calls — imports, requires,
|
|
7
|
+
* spawns, or evaluates anything from the analysed package. The only `Function`
|
|
8
|
+
* constructed anywhere in this tool is the parse-only compile in
|
|
9
|
+
* `cordis-yaml.ts`, and its result is discarded without being called.
|
|
10
|
+
* @module dsh-plugin-inspector/inspect
|
|
11
|
+
*/
|
|
12
|
+
import { EXPRESSION_CLASSES, PatchParseError, parsePatchDocument, } from "./cordis-yaml.js";
|
|
13
|
+
import { isCordisConfigFile, isModelVisibleText, isSourceFile, normalizePackagePath } from "./files.js";
|
|
14
|
+
import { HARNESS_REFERENCE } from "./knowledge.js";
|
|
15
|
+
import { parseManifest } from "./manifest.js";
|
|
16
|
+
import { SEVERITY_RANK, compareFindings, summarize, } from "./model.js";
|
|
17
|
+
import { loadSource } from "./source.js";
|
|
18
|
+
import { runTierA } from "./checks/tier-a.js";
|
|
19
|
+
import { runTierB } from "./checks/tier-b.js";
|
|
20
|
+
import { NON_DEGRADING_CHECKS, runTierC } from "./checks/tier-c.js";
|
|
21
|
+
/** This tool's own version, reported in the JSON document. */
|
|
22
|
+
export const TOOL_VERSION = '0.1.0';
|
|
23
|
+
/** This tool's package name, reported in the JSON document. */
|
|
24
|
+
export const TOOL_NAME = 'dsh-plugin-inspector';
|
|
25
|
+
/**
|
|
26
|
+
* Locate the one Cordis patch layer that actually mounts.
|
|
27
|
+
*
|
|
28
|
+
* `dsh.bundle.patch` is the whole of it. The launcher reads that key and no
|
|
29
|
+
* other (`packages/boot/app-boot/src/profile.ts`), so a file named
|
|
30
|
+
* `cordis.patch.yml` sitting anywhere else in the package is a document, an
|
|
31
|
+
* example, or a test fixture — not a layer. Treating those as mounted is how a
|
|
32
|
+
* tool comes to report `[critical] disables the core row "approval"` about a
|
|
33
|
+
* package that mounts nothing at all.
|
|
34
|
+
* @param source - the decoded package.
|
|
35
|
+
* @param declaredPatch - the `dsh.bundle.patch` value, already normalised.
|
|
36
|
+
* @returns the mounted layer's path, and every other cordis YAML file.
|
|
37
|
+
*/
|
|
38
|
+
function patchFiles(source, declaredPatch) {
|
|
39
|
+
const candidates = [...source.files.keys()].filter(isCordisConfigFile).sort();
|
|
40
|
+
const mounted = declaredPatch !== null && source.files.has(declaredPatch) ? declaredPatch : null;
|
|
41
|
+
return { mounted, others: candidates.filter(path => path !== mounted) };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Tally the `!!js` inventory by what each expression reaches. The inventory is
|
|
45
|
+
* a fact — a constant or a service read in a config value is what the shipped
|
|
46
|
+
* bundles are made of — and only the classes with reach become findings.
|
|
47
|
+
* @param patches - the parsed patch layers.
|
|
48
|
+
* @returns one count per classification.
|
|
49
|
+
*/
|
|
50
|
+
function tallyExpressions(patches) {
|
|
51
|
+
const tally = Object.fromEntries(EXPRESSION_CLASSES.map(name => [name, 0]));
|
|
52
|
+
for (const patch of patches) {
|
|
53
|
+
for (const site of patch.expressions)
|
|
54
|
+
tally[site.classification] += 1;
|
|
55
|
+
}
|
|
56
|
+
return tally;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Lower a Tier B confidence when Tier C fired. A Tier B positive stays true —
|
|
60
|
+
* the tool saw what it saw — but it is no longer a `high`-confidence reading of
|
|
61
|
+
* the package as a whole.
|
|
62
|
+
* @param confidence - the check's own confidence.
|
|
63
|
+
* @param degraded - whether any Tier C check fired.
|
|
64
|
+
* @returns the effective confidence.
|
|
65
|
+
*/
|
|
66
|
+
function downgrade(confidence, degraded) {
|
|
67
|
+
if (!degraded || confidence !== 'high')
|
|
68
|
+
return confidence;
|
|
69
|
+
return 'moderate';
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Inspect a plugin package.
|
|
73
|
+
* @param target - a plugin directory, or a `.tgz` / `.tar.gz` npm tarball.
|
|
74
|
+
* @returns the complete report.
|
|
75
|
+
* @throws SourceError or ManifestError when the target cannot be analysed at all.
|
|
76
|
+
*/
|
|
77
|
+
export async function inspect(target) {
|
|
78
|
+
const source = await loadSource(target);
|
|
79
|
+
const manifest = parseManifest(source.files.get('package.json') ?? '');
|
|
80
|
+
const declared = manifest.dsh.bundle?.patch;
|
|
81
|
+
const mountsAsBundle = declared !== undefined;
|
|
82
|
+
const declaredPatch = declared === undefined ? null : normalizePackagePath(declared);
|
|
83
|
+
const { mounted, others } = patchFiles(source, declaredPatch);
|
|
84
|
+
const patches = [];
|
|
85
|
+
const patchFailures = [];
|
|
86
|
+
if (mounted !== null) {
|
|
87
|
+
const text = source.files.get(mounted) ?? '';
|
|
88
|
+
try {
|
|
89
|
+
patches.push(parsePatchDocument(mounted, text));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!(error instanceof PatchParseError))
|
|
93
|
+
throw error;
|
|
94
|
+
patchFailures.push({ file: mounted, error });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const sourceFiles = [...source.files.keys()].filter(isSourceFile).sort();
|
|
98
|
+
const modelVisibleFiles = [...source.files.keys()].filter(isModelVisibleText).sort();
|
|
99
|
+
const input = {
|
|
100
|
+
source,
|
|
101
|
+
manifest,
|
|
102
|
+
mountsAsBundle,
|
|
103
|
+
patches,
|
|
104
|
+
patchFailures,
|
|
105
|
+
unmountedPatchFiles: others,
|
|
106
|
+
sourceFiles,
|
|
107
|
+
modelVisibleFiles,
|
|
108
|
+
};
|
|
109
|
+
const tierC = runTierC(input);
|
|
110
|
+
const unreadable = tierC.filter(finding => !NON_DEGRADING_CHECKS.has(finding.checkId));
|
|
111
|
+
const degraded = unreadable.length > 0;
|
|
112
|
+
const raw = [
|
|
113
|
+
...runTierA(input),
|
|
114
|
+
...runTierB(input).map(finding => ({ ...finding, confidence: downgrade(finding.confidence, degraded) })),
|
|
115
|
+
...tierC,
|
|
116
|
+
];
|
|
117
|
+
const findings = [...raw].sort(compareFindings);
|
|
118
|
+
const facts = {
|
|
119
|
+
packageName: manifest.name,
|
|
120
|
+
packageVersion: manifest.version,
|
|
121
|
+
license: manifest.license,
|
|
122
|
+
mountsAsBundle,
|
|
123
|
+
bundlePatchPath: declared ?? null,
|
|
124
|
+
shipsClientBundle: manifest.dsh.client !== undefined && manifest.exportPaths.includes('./client'),
|
|
125
|
+
profileBundles: manifest.dsh.profile?.bundles ?? [],
|
|
126
|
+
binNames: manifest.binNames,
|
|
127
|
+
insertedRows: patches.flatMap(patch => patch.inserts.map(row => ({
|
|
128
|
+
id: row.id ?? '(unnamed)',
|
|
129
|
+
...row.name === null ? {} : { name: row.name },
|
|
130
|
+
}))),
|
|
131
|
+
targetedRows: [...new Set(patches.flatMap(patch => patch.overrides.map(override => override.id)))].sort(),
|
|
132
|
+
jsExpressions: tallyExpressions(patches),
|
|
133
|
+
unmountedPatchFiles: others,
|
|
134
|
+
dependencies: Object.keys(manifest.dependencies).sort(),
|
|
135
|
+
peerDependencies: Object.keys(manifest.peerDependencies).sort(),
|
|
136
|
+
modelVisibleFiles,
|
|
137
|
+
filesRead: source.files.size,
|
|
138
|
+
bytesRead: source.bytesRead,
|
|
139
|
+
sourceFilesParsed: sourceFiles.length,
|
|
140
|
+
publishBasis: source.publishBasis,
|
|
141
|
+
unpublishedFiles: source.unpublishedFiles,
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
tool: { name: TOOL_NAME, version: TOOL_VERSION, harnessReference: HARNESS_REFERENCE },
|
|
146
|
+
target: { kind: source.kind, path: source.path },
|
|
147
|
+
facts,
|
|
148
|
+
analysis: {
|
|
149
|
+
integrity: degraded ? 'degraded' : 'complete',
|
|
150
|
+
negativesReliable: !degraded,
|
|
151
|
+
degradedBy: [...new Set(unreadable.map(finding => finding.checkId))].sort(),
|
|
152
|
+
filesSkipped: source.skipped,
|
|
153
|
+
},
|
|
154
|
+
summary: summarize(findings),
|
|
155
|
+
findings,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Whether a report should fail a CI gate.
|
|
160
|
+
* @param report - the report.
|
|
161
|
+
* @param threshold - the lowest severity that fails, or `none` to never fail.
|
|
162
|
+
* @returns true when at least one finding is at or above the threshold.
|
|
163
|
+
*/
|
|
164
|
+
export function exceedsThreshold(report, threshold) {
|
|
165
|
+
if (threshold === 'none')
|
|
166
|
+
return false;
|
|
167
|
+
return report.findings.some(finding => SEVERITY_RANK[finding.severity] >= SEVERITY_RANK[threshold]);
|
|
168
|
+
}
|
package/lib/knowledge.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ground truth read out of the DeepSeek Harness checkout: the core row
|
|
3
|
+
* inventory, the capability seam keys, the waterfall event set, and the
|
|
4
|
+
* capabilities the harness's own sandbox denies untrusted code.
|
|
5
|
+
*
|
|
6
|
+
* Every table here cites the harness file it was transcribed from. These are
|
|
7
|
+
* facts about a specific harness version, not opinions — when the harness
|
|
8
|
+
* changes, these tables are what needs updating, and keeping them in one
|
|
9
|
+
* module is what makes that a single reviewable diff.
|
|
10
|
+
* @module dsh-plugin-inspector/knowledge
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Harness version these tables were transcribed from — the version string in
|
|
14
|
+
* the checkout's own `packages/bundle/*/package.json`.
|
|
15
|
+
*/
|
|
16
|
+
export const HARNESS_REFERENCE = '0.1.0-rc.5';
|
|
17
|
+
/**
|
|
18
|
+
* The three profile bundles the harness ships, mapped to what each one is.
|
|
19
|
+
* A package that *is* one of these composes the core rows rather than modifying
|
|
20
|
+
* somebody else's: `@deepseek-ai/dsh-web-app` disabling two dozen rows the base
|
|
21
|
+
* layer inserted is the definition of a bundle, not an attack on one.
|
|
22
|
+
* Transcribed from `packages/bundle/{base,headless,web-app}/package.json`.
|
|
23
|
+
*/
|
|
24
|
+
export const HARNESS_BUNDLE_PACKAGES = new Map([
|
|
25
|
+
['@deepseek-ai/dsh-base', 'base'],
|
|
26
|
+
['@deepseek-ai/dsh-headless', 'headless'],
|
|
27
|
+
['@deepseek-ai/dsh-web-app', 'web-app'],
|
|
28
|
+
]);
|
|
29
|
+
/**
|
|
30
|
+
* Every row the shipped bundles define, mapped from row id to the module that
|
|
31
|
+
* implements it and the bundles that insert it. Transcribed from
|
|
32
|
+
* `packages/bundle/{base,headless,web-app}/cordis.patch.yml`.
|
|
33
|
+
*
|
|
34
|
+
* A patch row whose `id` is a key here is modifying core behavior rather than
|
|
35
|
+
* contributing its own. The name half matters because `applyEntryPatches`
|
|
36
|
+
* treats `name` on a non-insert patch as an assertion guard: on mismatch it
|
|
37
|
+
* warns and skips the whole patch, so a patch naming the wrong module silently
|
|
38
|
+
* does nothing at all. The bundle half matters because the three layers are not
|
|
39
|
+
* one profile: a `ui-*` row exists only where the web bundle is mounted, so a
|
|
40
|
+
* headless profile never had it to lose.
|
|
41
|
+
*/
|
|
42
|
+
export const CORE_ROWS = new Map([
|
|
43
|
+
['agent', { module: '@deepseek-ai/dsh-agent', bundles: ['base'] }],
|
|
44
|
+
['agent-default-model', { module: '@deepseek-ai/dsh-agent-default-model', bundles: ['base'] }],
|
|
45
|
+
['agent-instructions', { module: '@deepseek-ai/dsh-agent-instructions', bundles: ['base'] }],
|
|
46
|
+
['agent-loop', { module: '@deepseek-ai/dsh-agent-loop', bundles: ['base'] }],
|
|
47
|
+
['agent-presets', { module: '@deepseek-ai/dsh-agent-presets', bundles: ['web-app'] }],
|
|
48
|
+
['api-gateway', { module: '@deepseek-ai/dsh-host-apiproxy', bundles: ['web-app'] }],
|
|
49
|
+
['api-remotes', { module: '@deepseek-ai/dsh-api-remotes', bundles: ['web-app'] }],
|
|
50
|
+
['approval', { module: '@deepseek-ai/dsh-user-approval', bundles: ['base'] }],
|
|
51
|
+
['attachment-local', { module: '@deepseek-ai/dsh-attachment-local', bundles: ['base'] }],
|
|
52
|
+
['bash-sandbox', { module: '@deepseek-ai/dsh-bash-sandbox', bundles: ['base'] }],
|
|
53
|
+
['client-hmr', { module: '@deepseek-ai/dsh-client-hmr', bundles: ['web-app'] }],
|
|
54
|
+
['client-runtime', { module: '@deepseek-ai/dsh-client-runtime', bundles: ['web-app'] }],
|
|
55
|
+
['code-runtime', { module: '@deepseek-ai/dsh-code-runtime-worker-thread', bundles: ['headless', 'web-app'] }],
|
|
56
|
+
['command-compact', { module: '@deepseek-ai/dsh-command-compact', bundles: ['base'] }],
|
|
57
|
+
['command-feedback', { module: '@deepseek-ai/dsh-command-feedback', bundles: ['base'] }],
|
|
58
|
+
['command-goal', { module: '@deepseek-ai/dsh-command-goal', bundles: ['base'] }],
|
|
59
|
+
['commands', { module: '@deepseek-ai/dsh-commands', bundles: ['base'] }],
|
|
60
|
+
['compaction-basic', { module: '@deepseek-ai/dsh-compaction-basic', bundles: ['base'] }],
|
|
61
|
+
['connection', { module: '@deepseek-ai/dsh-client-connection', bundles: ['web-app'] }],
|
|
62
|
+
['cordis-client-runner', { module: '@deepseek-ai/dsh-cordis-client-runner', bundles: ['web-app'] }],
|
|
63
|
+
['cordis-host-runner', { module: '@deepseek-ai/dsh-cordis-host-runner', bundles: ['web-app'] }],
|
|
64
|
+
['credentials', { module: '@deepseek-ai/dsh-credentials-local', bundles: ['base'] }],
|
|
65
|
+
['directory-picker', { module: '@deepseek-ai/dsh-host-directory-picker-auto', bundles: ['web-app'] }],
|
|
66
|
+
['fs-observation-policy', { module: '@deepseek-ai/dsh-fs-observation-policy', bundles: ['base'] }],
|
|
67
|
+
['fs-sandbox', { module: '@deepseek-ai/dsh-fs-sandbox', bundles: ['base'] }],
|
|
68
|
+
['goal', { module: '@deepseek-ai/dsh-goal', bundles: ['base'] }],
|
|
69
|
+
['goal-round-driver', { module: '@deepseek-ai/dsh-goal-round-driver', bundles: ['base'] }],
|
|
70
|
+
['headless-runner', { module: '@deepseek-ai/dsh-headless', bundles: ['headless'] }],
|
|
71
|
+
['headless-startup', { module: '@deepseek-ai/dsh-headless/startup', bundles: ['headless'] }],
|
|
72
|
+
['hmr', { module: '@deepseek-ai/cordis-plugin-hmr', bundles: ['base'] }],
|
|
73
|
+
['jobs', { module: '@deepseek-ai/dsh-jobs-local', bundles: ['base'] }],
|
|
74
|
+
['llm', { module: '@deepseek-ai/dsh-llm', bundles: ['base'] }],
|
|
75
|
+
['llm-deepseek', { module: '@deepseek-ai/dsh-llm-deepseek', bundles: ['base'] }],
|
|
76
|
+
['llm-pi-ai', { module: '@deepseek-ai/dsh-llm-pi-ai', bundles: ['base'] }],
|
|
77
|
+
['llm-retry', { module: '@deepseek-ai/dsh-llm-retry', bundles: ['base'] }],
|
|
78
|
+
['locale', { module: '@deepseek-ai/dsh-client-locale', bundles: ['web-app'] }],
|
|
79
|
+
['message-feedback', { module: '@deepseek-ai/dsh-message-feedback', bundles: ['web-app'] }],
|
|
80
|
+
['modules', { module: '@deepseek-ai/dsh-client-modules', bundles: ['web-app'] }],
|
|
81
|
+
['permission', { module: '@deepseek-ai/dsh-permission-presets', bundles: ['base'] }],
|
|
82
|
+
['plan-mode', { module: '@deepseek-ai/dsh-plan-mode', bundles: ['base'] }],
|
|
83
|
+
['plugin-inventory', { module: '@deepseek-ai/dsh-host-plugin-inventory', bundles: ['web-app'] }],
|
|
84
|
+
['pwsh-sandbox', { module: '@deepseek-ai/dsh-pwsh-sandbox', bundles: ['base'] }],
|
|
85
|
+
['repeat-tool-reminder', { module: '@deepseek-ai/dsh-repeat-tool-reminder', bundles: ['base'] }],
|
|
86
|
+
['sandbox', { module: '@deepseek-ai/dsh-sandbox-local', bundles: ['base'] }],
|
|
87
|
+
['sandbox-policy', { module: '@deepseek-ai/dsh-sandbox-policy', bundles: ['base'] }],
|
|
88
|
+
['session', { module: '@deepseek-ai/dsh-session', bundles: ['base'] }],
|
|
89
|
+
['session-checkpoint-policy', { module: '@deepseek-ai/dsh-session-checkpoint-policy', bundles: ['base'] }],
|
|
90
|
+
['session-log-download', { module: '@deepseek-ai/dsh-session-log-export', bundles: ['web-app'] }],
|
|
91
|
+
['session-persistence-jsonl', { module: '@deepseek-ai/dsh-session-persistence-jsonl', bundles: ['base'] }],
|
|
92
|
+
['session-projection', { module: '@deepseek-ai/dsh-session-projection', bundles: ['base'] }],
|
|
93
|
+
['session-projection-cache', { module: '@deepseek-ai/dsh-session-projection-cache', bundles: ['web-app'] }],
|
|
94
|
+
['session-query-sqlite', { module: '@deepseek-ai/dsh-session-query-sqlite', bundles: ['base'] }],
|
|
95
|
+
['session-stats', { module: '@deepseek-ai/dsh-session-stats', bundles: ['web-app'] }],
|
|
96
|
+
['session-telemetry-otel', { module: '@deepseek-ai/dsh-session-telemetry-otel', bundles: ['base'] }],
|
|
97
|
+
['session-title', { module: '@deepseek-ai/dsh-session-title', bundles: ['base'] }],
|
|
98
|
+
['session-title-llm', { module: '@deepseek-ai/dsh-session-title-first-prompt-llm', bundles: ['base'] }],
|
|
99
|
+
['settings', { module: '@deepseek-ai/dsh-settings-file', bundles: ['base'] }],
|
|
100
|
+
['shell-env', { module: '@deepseek-ai/dsh-shell-env', bundles: ['base'] }],
|
|
101
|
+
['skill', { module: '@deepseek-ai/dsh-skill', bundles: ['base'] }],
|
|
102
|
+
['skill-badge', { module: '@deepseek-ai/dsh-skill-badge', bundles: ['base'] }],
|
|
103
|
+
['skill-filesystem', { module: '@deepseek-ai/dsh-skill-filesystem', bundles: ['base'] }],
|
|
104
|
+
['spill-local', { module: '@deepseek-ai/dsh-spill-local', bundles: ['base'] }],
|
|
105
|
+
['spill-policy', { module: '@deepseek-ai/dsh-spill-policy', bundles: ['base'] }],
|
|
106
|
+
['storage', { module: '@deepseek-ai/dsh-storage', bundles: ['web-app'] }],
|
|
107
|
+
['storage-domain', { module: '@deepseek-ai/dsh-storage-domain', bundles: ['web-app'] }],
|
|
108
|
+
['storage-json', { module: '@deepseek-ai/dsh-storage-json', bundles: ['web-app'] }],
|
|
109
|
+
['subagent', { module: '@deepseek-ai/dsh-subagent', bundles: ['base'] }],
|
|
110
|
+
['subagent-fork-in-process', { module: '@deepseek-ai/dsh-subagent-fork-in-process', bundles: ['base'] }],
|
|
111
|
+
['subagent-spawn-in-process', { module: '@deepseek-ai/dsh-subagent-spawn-in-process', bundles: ['base'] }],
|
|
112
|
+
['subprocess', { module: '@deepseek-ai/dsh-subprocess-local', bundles: ['base'] }],
|
|
113
|
+
['system-prompt', { module: '@deepseek-ai/dsh-system-prompt', bundles: ['base'] }],
|
|
114
|
+
['timeout-policy', { module: '@deepseek-ai/dsh-tool-call-timeout-policy', bundles: ['base'] }],
|
|
115
|
+
['timer', { module: '@deepseek-ai/cordis-plugin-timer', bundles: ['base'] }],
|
|
116
|
+
['token-meter', { module: '@deepseek-ai/dsh-token-meter', bundles: ['base'] }],
|
|
117
|
+
['tool-bash', { module: '@deepseek-ai/dsh-tool-bash', bundles: ['base'] }],
|
|
118
|
+
['tool-fs', { module: '@deepseek-ai/dsh-tool-fs', bundles: ['base'] }],
|
|
119
|
+
['tool-fs-search', { module: '@deepseek-ai/dsh-tool-fs-search', bundles: ['base'] }],
|
|
120
|
+
['tool-goal', { module: '@deepseek-ai/dsh-tool-goal', bundles: ['base'] }],
|
|
121
|
+
['tool-jobs', { module: '@deepseek-ai/dsh-tool-jobs', bundles: ['base'] }],
|
|
122
|
+
['tool-pwsh', { module: '@deepseek-ai/dsh-tool-pwsh', bundles: ['base'] }],
|
|
123
|
+
['tool-ralph', { module: '@deepseek-ai/dsh-tool-ralph', bundles: ['base'] }],
|
|
124
|
+
['tool-result-pruner', { module: '@deepseek-ai/dsh-compaction-tool-result-pruner', bundles: ['base'] }],
|
|
125
|
+
['tool-skill', { module: '@deepseek-ai/dsh-tool-skill', bundles: ['base'] }],
|
|
126
|
+
['tool-str-replace-editor', { module: '@deepseek-ai/dsh-tool-str-replace-editor', bundles: ['base'] }],
|
|
127
|
+
['tool-subagent', { module: '@deepseek-ai/dsh-tool-subagent', bundles: ['base'] }],
|
|
128
|
+
['tool-subagent-control', { module: '@deepseek-ai/dsh-tool-subagent-control', bundles: ['base'] }],
|
|
129
|
+
['tool-subagent-fork', { module: '@deepseek-ai/dsh-tool-subagent', bundles: ['base'] }],
|
|
130
|
+
['tool-subagent-list-agents', { module: '@deepseek-ai/dsh-tool-subagent-control/list-agents', bundles: ['base'] }],
|
|
131
|
+
['tool-subagent-report', { module: '@deepseek-ai/dsh-tool-subagent-report', bundles: ['base'] }],
|
|
132
|
+
['tool-todo', { module: '@deepseek-ai/dsh-tool-todo', bundles: ['base'] }],
|
|
133
|
+
['tool-web', { module: '@deepseek-ai/dsh-tool-web', bundles: ['base'] }],
|
|
134
|
+
['tool-workflow', { module: '@deepseek-ai/dsh-tool-workflow', bundles: ['base'] }],
|
|
135
|
+
['tools', { module: '@deepseek-ai/dsh-tools', bundles: ['base'] }],
|
|
136
|
+
['typert', { module: '@deepseek-ai/dsh-typert-registry', bundles: ['base'] }],
|
|
137
|
+
['typert-gateway', { module: '@deepseek-ai/dsh-api-gateway', bundles: ['base'] }],
|
|
138
|
+
['typert-loader', { module: '@deepseek-ai/dsh-typert-loader', bundles: ['base'] }],
|
|
139
|
+
['ui-agent-preset', { module: '@deepseek-ai/dsh-client-ui-agent-preset', bundles: ['web-app'] }],
|
|
140
|
+
['ui-commands', { module: '@deepseek-ai/dsh-client-ui-commands', bundles: ['web-app'] }],
|
|
141
|
+
['ui-conversation', { module: '@deepseek-ai/dsh-client-ui-conversation', bundles: ['web-app'] }],
|
|
142
|
+
['ui-cordis', { module: '@deepseek-ai/dsh-client-ui-cordis', bundles: ['web-app'] }],
|
|
143
|
+
['ui-deliverables', { module: '@deepseek-ai/dsh-client-ui-deliverables', bundles: ['web-app'] }],
|
|
144
|
+
['ui-goal', { module: '@deepseek-ai/dsh-client-ui-goal', bundles: ['web-app'] }],
|
|
145
|
+
['ui-input-trigger', { module: '@deepseek-ai/dsh-client-ui-input-trigger', bundles: ['web-app'] }],
|
|
146
|
+
['ui-jobs', { module: '@deepseek-ai/dsh-client-ui-jobs', bundles: ['web-app'] }],
|
|
147
|
+
['ui-layout', { module: '@deepseek-ai/dsh-client-ui-layout', bundles: ['web-app'] }],
|
|
148
|
+
['ui-message-feedback', { module: '@deepseek-ai/dsh-client-ui-message-feedback', bundles: ['web-app'] }],
|
|
149
|
+
['ui-model-selection', { module: '@deepseek-ai/dsh-client-ui-model-selection', bundles: ['web-app'] }],
|
|
150
|
+
['ui-permission', { module: '@deepseek-ai/dsh-client-ui-permission-presets', bundles: ['web-app'] }],
|
|
151
|
+
['ui-plan', { module: '@deepseek-ai/dsh-client-ui-plan', bundles: ['web-app'] }],
|
|
152
|
+
['ui-settings', { module: '@deepseek-ai/dsh-client-ui-settings', bundles: ['web-app'] }],
|
|
153
|
+
['ui-settings-general', { module: '@deepseek-ai/dsh-client-ui-settings-general', bundles: ['web-app'] }],
|
|
154
|
+
['ui-settings-models', { module: '@deepseek-ai/dsh-client-ui-settings-models', bundles: ['web-app'] }],
|
|
155
|
+
['ui-settings-plugin-inventory', { module: '@deepseek-ai/dsh-client-ui-settings-plugin-inventory', bundles: ['web-app'] }],
|
|
156
|
+
['ui-settings-plugins', { module: '@deepseek-ai/dsh-client-ui-settings-plugins', bundles: ['web-app'] }],
|
|
157
|
+
['ui-sidebar', { module: '@deepseek-ai/dsh-client-ui-sidebar', bundles: ['web-app'] }],
|
|
158
|
+
['ui-skill', { module: '@deepseek-ai/dsh-client-ui-skill', bundles: ['web-app'] }],
|
|
159
|
+
['ui-subagent', { module: '@deepseek-ai/dsh-client-ui-subagent', bundles: ['web-app'] }],
|
|
160
|
+
['ui-theme', { module: '@deepseek-ai/dsh-client-ui-theme', bundles: ['web-app'] }],
|
|
161
|
+
['ui-tool', { module: '@deepseek-ai/dsh-client-ui-tool', bundles: ['web-app'] }],
|
|
162
|
+
['ui-trajectory', { module: '@deepseek-ai/dsh-client-ui-trajectory', bundles: ['web-app'] }],
|
|
163
|
+
['ui-user-questions', { module: '@deepseek-ai/dsh-client-ui-user-questions', bundles: ['web-app'] }],
|
|
164
|
+
['ui-workflow-run', { module: '@deepseek-ai/dsh-client-ui-workflow-run', bundles: ['web-app'] }],
|
|
165
|
+
['ui-workspace', { module: '@deepseek-ai/dsh-client-ui-workspace', bundles: ['web-app'] }],
|
|
166
|
+
['user-questions', { module: '@deepseek-ai/dsh-user-questions', bundles: ['base'] }],
|
|
167
|
+
['web', { module: '@deepseek-ai/dsh-web', bundles: ['base'] }],
|
|
168
|
+
['web-runtime', { module: '@deepseek-ai/dsh-web-app', bundles: ['web-app'] }],
|
|
169
|
+
['web-search-deepseek', { module: '@deepseek-ai/dsh-web-search-deepseek', bundles: ['base'] }],
|
|
170
|
+
['web-startup', { module: '@deepseek-ai/dsh-web-app/startup', bundles: ['web-app'] }],
|
|
171
|
+
['webserver', { module: '@deepseek-ai/dsh-host-webserver', bundles: ['web-app'] }],
|
|
172
|
+
['workflow-worker-thread', { module: '@deepseek-ai/dsh-workflow-worker-thread', bundles: ['base'] }],
|
|
173
|
+
['workspace', { module: '@deepseek-ai/dsh-workspace', bundles: ['web-app'] }],
|
|
174
|
+
]);
|
|
175
|
+
/** Row ids the shipped bundles define. */
|
|
176
|
+
export const CORE_ROW_IDS = new Set(CORE_ROWS.keys());
|
|
177
|
+
/**
|
|
178
|
+
* The core rows whose whole purpose is to constrain what the agent may do.
|
|
179
|
+
* Disabling or reconfiguring one of these from a third-party patch layer is
|
|
180
|
+
* the highest-value finding this tool produces, and it is plain YAML.
|
|
181
|
+
*
|
|
182
|
+
* Each entry names what stops holding when the row stops running.
|
|
183
|
+
*/
|
|
184
|
+
export const SECURITY_ROW_IDS = new Map([
|
|
185
|
+
['approval', 'user approval prompts for tool calls'],
|
|
186
|
+
['permission', 'the permission preset that decides which tools may run unattended'],
|
|
187
|
+
['sandbox', 'the sandbox service'],
|
|
188
|
+
['sandbox-policy', 'the policy that decides what the sandbox permits'],
|
|
189
|
+
['bash-sandbox', 'sandboxing of bash tool invocations'],
|
|
190
|
+
['pwsh-sandbox', 'sandboxing of PowerShell tool invocations'],
|
|
191
|
+
['fs-sandbox', 'filesystem access confinement'],
|
|
192
|
+
['fs-observation-policy', 'the read-before-write policy on file edits'],
|
|
193
|
+
['subprocess', 'the mediated subprocess service tools are supposed to go through'],
|
|
194
|
+
['credentials', 'credential storage'],
|
|
195
|
+
['timeout-policy', 'tool execution timeouts'],
|
|
196
|
+
['spill-policy', 'the policy bounding oversized tool output'],
|
|
197
|
+
['session-persistence-jsonl', 'the session log, which is the audit record'],
|
|
198
|
+
['session-telemetry-otel', 'telemetry export'],
|
|
199
|
+
['session-checkpoint-policy', 'session checkpointing'],
|
|
200
|
+
['tools', 'the tool registry itself'],
|
|
201
|
+
['agent-loop', 'the agent loop itself'],
|
|
202
|
+
]);
|
|
203
|
+
/**
|
|
204
|
+
* Capability seam keys from
|
|
205
|
+
* `packages/extensions/tool-cordis/src/api-catalog.ts` (`SERVICE_API[].key`).
|
|
206
|
+
* A plugin calling `ctx.provide(key, …)` or `ctx.set(key, …)` on one of these
|
|
207
|
+
* replaces a core service for every consumer in its scope.
|
|
208
|
+
*/
|
|
209
|
+
export const SEAM_KEYS = new Set([
|
|
210
|
+
'agentDefaultModel', 'agentLoop', 'agentPresets', 'agents', 'apiProxy', 'approval',
|
|
211
|
+
'attachments', 'clientModules', 'codeRuntime', 'commands', 'compaction', 'credentials',
|
|
212
|
+
'directoryPicker', 'e2b', 'fs', 'goals', 'invariants', 'jobs', 'llm', 'lsp',
|
|
213
|
+
'messageFeedback', 'permissionPresets', 'planMode', 'sandbox', 'sandboxPolicy',
|
|
214
|
+
'sessionPersistence', 'sessionProjectionCache', 'sessionProjections', 'sessionQuery',
|
|
215
|
+
'sessionReferenceResolver', 'sessions', 'sessionTelemetry', 'sessionTitle', 'settings',
|
|
216
|
+
'shell', 'shellEnv', 'skills', 'spillStore', 'storage', 'storageDomain', 'subagents',
|
|
217
|
+
'subprocess', 'systemPrompt', 'terminals', 'timer', 'tokenMeter', 'toolResultPruner',
|
|
218
|
+
'tools', 'typert', 'typertGateway', 'userQuestions', 'web', 'webServer', 'workflowEngine',
|
|
219
|
+
'workspaceRegistry',
|
|
220
|
+
]);
|
|
221
|
+
/** The subset of {@link SEAM_KEYS} whose replacement removes a constraint. */
|
|
222
|
+
export const SECURITY_SEAM_KEYS = new Set([
|
|
223
|
+
'approval', 'sandbox', 'sandboxPolicy', 'permissionPresets', 'credentials', 'subprocess',
|
|
224
|
+
'shell', 'fs', 'tools', 'agentLoop', 'sessionPersistence', 'sessionTelemetry', 'invariants',
|
|
225
|
+
]);
|
|
226
|
+
/**
|
|
227
|
+
* Waterfall events, from `EVENT_API` in the api-catalog. A listener on one of
|
|
228
|
+
* these receives a trailing `next` and MUST call it to delegate; returning
|
|
229
|
+
* without calling it short-circuits the chain including the built-in behavior.
|
|
230
|
+
*
|
|
231
|
+
* Note there is no `fs/read-intent` — the intent family is write and edit only.
|
|
232
|
+
*/
|
|
233
|
+
export const WATERFALL_EVENTS = new Set([
|
|
234
|
+
'agent/pre-step', 'agent/request', 'agent/request-error', 'approval/request',
|
|
235
|
+
'fs/edit-intent', 'fs/write-intent', 'llm/stream', 'session-telemetry/record',
|
|
236
|
+
'system-prompt/assemble', 'tools/code-dispatch-log', 'tools/execute',
|
|
237
|
+
'tools/post-execute', 'tools/pre-execute',
|
|
238
|
+
]);
|
|
239
|
+
/** Waterfall events whose short-circuit removes a decision the user would otherwise make. */
|
|
240
|
+
export const DECISION_EVENTS = new Set([
|
|
241
|
+
'approval/request', 'tools/pre-execute', 'tools/execute', 'fs/write-intent', 'fs/edit-intent',
|
|
242
|
+
]);
|
|
243
|
+
/**
|
|
244
|
+
* Globals the dynamic-package sandbox (`cordis-host-runner/src/sandbox.ts`)
|
|
245
|
+
* traps and redirects to a `ctx` service, plus `process`, which it leaves
|
|
246
|
+
* `undefined`. An installed bundle layer is a plain ESM import and gets none of
|
|
247
|
+
* these restrictions — which is exactly why using one is worth reporting.
|
|
248
|
+
*/
|
|
249
|
+
export const SANDBOX_DENIED_GLOBALS = new Map([
|
|
250
|
+
['require', "redirected to ctx services (inject: ['fs'] / ['web'] / ['bash'])"],
|
|
251
|
+
['fetch', "redirected to the cordis web service (inject: ['web'])"],
|
|
252
|
+
['setTimeout', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
253
|
+
['setInterval', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
254
|
+
['setImmediate', "redirected to the cordis timer service (inject: ['timer'])"],
|
|
255
|
+
]);
|
|
256
|
+
/**
|
|
257
|
+
* Node builtins that start or evaluate code off the mediated path. A mounted
|
|
258
|
+
* layer importing one of these is doing what `ctx.subprocess` and `ctx.sandbox`
|
|
259
|
+
* exist to mediate, from a position where nothing mediates it.
|
|
260
|
+
*/
|
|
261
|
+
export const UNMEDIATED_PROCESS_MODULES = new Map([
|
|
262
|
+
['child_process', 'spawns processes without ctx.subprocess or ctx.sandbox'],
|
|
263
|
+
['worker_threads', 'runs code in a thread the harness does not supervise'],
|
|
264
|
+
['vm', 'evaluates code outside every harness seam'],
|
|
265
|
+
]);
|
|
266
|
+
/**
|
|
267
|
+
* Modules and globals that move bytes off the machine. The harness's own
|
|
268
|
+
* dynamic-package sandbox traps `fetch` and redirects it to the `ctx.web`
|
|
269
|
+
* service; a mounted layer gets no such redirect.
|
|
270
|
+
*/
|
|
271
|
+
export const NETWORK_MODULES = new Set([
|
|
272
|
+
'http', 'https', 'http2', 'net', 'tls', 'dgram',
|
|
273
|
+
'undici', 'axios', 'node-fetch', 'got', 'superagent', 'ws', 'request',
|
|
274
|
+
]);
|
|
275
|
+
/**
|
|
276
|
+
* Filesystem modules that bypass `ctx.fs`. Reads and writes through these are
|
|
277
|
+
* invisible to `fs/write-intent`, `fs/edit-intent`, `fs/observed`, and the
|
|
278
|
+
* `fs-sandbox` row, so no policy sees them.
|
|
279
|
+
*/
|
|
280
|
+
export const UNMEDIATED_FS_MODULES = new Set(['fs', 'fs/promises']);
|
|
281
|
+
/** The npm package that turns a Cordis row into an MCP server connection. */
|
|
282
|
+
export const MCP_CLIENT_PACKAGE = '@deepseek-ai/dsh-mcp-client';
|
|
283
|
+
/** The row id that owns filesystem skill discovery, and whose config selects the roots. */
|
|
284
|
+
export const SKILL_FILESYSTEM_ROW = 'skill-filesystem';
|
|
285
|
+
/** `skill-filesystem` config keys that point discovery at a new directory. */
|
|
286
|
+
export const SKILL_ROOT_CONFIG_KEYS = ['customSkillDirs', 'bundledSkillDir'];
|
|
287
|
+
/**
|
|
288
|
+
* `package.json` script names npm and pnpm run around installation. A plugin
|
|
289
|
+
* only needs one of these to run code before the user has read a line of it.
|
|
290
|
+
*/
|
|
291
|
+
export const INSTALL_LIFECYCLE_SCRIPTS = [
|
|
292
|
+
'preinstall', 'install', 'postinstall', 'prepare', 'prepublish', 'preprepare', 'postprepare',
|
|
293
|
+
];
|
|
294
|
+
/** Entry fields the loader never interpolates: a `!!js` node here is inert data. */
|
|
295
|
+
export const STATIC_ENTRY_FIELDS = [
|
|
296
|
+
'id', 'name', 'group', 'inject', 'intercept', 'isolate',
|
|
297
|
+
];
|
|
298
|
+
/**
|
|
299
|
+
* Calls a `!!js` expression may make that reach nothing the harness does not
|
|
300
|
+
* already hand it.
|
|
301
|
+
*
|
|
302
|
+
* `dsh-app-boot` does `ctx.provide('dshHomePath', dshHomePath)` before mounting
|
|
303
|
+
* any entry (`packages/boot/app-boot/src/index.ts`), and the loader evaluates
|
|
304
|
+
* every expression under `with (ctx)`, so `dshHomePath(...)` is in scope by
|
|
305
|
+
* design and documented as such in that package's README. The two `process`
|
|
306
|
+
* reads are the ones the shipped bundles use.
|
|
307
|
+
*/
|
|
308
|
+
export const HARNESS_INERT_CALLS = new Set([
|
|
309
|
+
'dshHomePath', 'process.cwd', 'process.uptime',
|
|
310
|
+
]);
|
|
311
|
+
/**
|
|
312
|
+
* The entry fields that decide which services a row sees, and which of them it
|
|
313
|
+
* substitutes for its subtree.
|
|
314
|
+
*
|
|
315
|
+
* `isolate` is the sharpest: `vendor/loader/src/config/isolate.ts` re-maps a
|
|
316
|
+
* named service to a fresh symbol realm, so every descendant that injects that
|
|
317
|
+
* name gets the row's realm instead of the profile's. Setting it on a security
|
|
318
|
+
* service is a Tier A declaration with the same reach as replacing the service
|
|
319
|
+
* in code, and it is plain YAML.
|
|
320
|
+
*/
|
|
321
|
+
export const SERVICE_REMAPPING_FIELDS = ['isolate', 'intercept'];
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading `package.json` from an untrusted package.
|
|
3
|
+
*
|
|
4
|
+
* This is a file boundary with a hostile author on the other side, so nothing
|
|
5
|
+
* here trusts the parse type. Every field is narrowed before use and a field
|
|
6
|
+
* of the wrong shape is treated as absent rather than throwing — a plugin that
|
|
7
|
+
* ships `"scripts": "postinstall"` should still be analysed for everything
|
|
8
|
+
* else, and "this manifest is malformed" is itself worth reporting.
|
|
9
|
+
* @module dsh-plugin-inspector/manifest
|
|
10
|
+
*/
|
|
11
|
+
/** Thrown when `package.json` is not JSON at all. */
|
|
12
|
+
export class ManifestError extends Error {
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether a value is a plain object, which is the only shape any of the fields
|
|
16
|
+
* this module reads is allowed to have.
|
|
17
|
+
* @param value - the parsed value.
|
|
18
|
+
* @returns true for a non-null, non-array object.
|
|
19
|
+
*/
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read a `Record<string, string>` field, dropping entries of the wrong type.
|
|
25
|
+
* @param source - the parsed manifest object.
|
|
26
|
+
* @param field - the field name.
|
|
27
|
+
* @param defects - sink for shape complaints.
|
|
28
|
+
* @returns the field's string-valued entries.
|
|
29
|
+
*/
|
|
30
|
+
function stringMap(source, field, defects) {
|
|
31
|
+
const value = source[field];
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return {};
|
|
34
|
+
if (!isRecord(value)) {
|
|
35
|
+
defects.push(`"${field}" is not an object`);
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
40
|
+
if (typeof entry === 'string')
|
|
41
|
+
result[key] = entry;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read the `dsh` section, narrowing each nested field independently so one
|
|
47
|
+
* malformed subfield does not discard the rest.
|
|
48
|
+
* @param value - the raw `dsh` value.
|
|
49
|
+
* @param defects - sink for shape complaints.
|
|
50
|
+
* @returns the narrowed section.
|
|
51
|
+
*/
|
|
52
|
+
function readDshSection(value, defects) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
return {};
|
|
55
|
+
if (!isRecord(value)) {
|
|
56
|
+
defects.push('"dsh" is not an object');
|
|
57
|
+
return {};
|
|
58
|
+
}
|
|
59
|
+
const section = {};
|
|
60
|
+
const bundle = value.bundle;
|
|
61
|
+
if (isRecord(bundle)) {
|
|
62
|
+
section.bundle = typeof bundle.patch === 'string' ? { patch: bundle.patch } : {};
|
|
63
|
+
}
|
|
64
|
+
else if (bundle !== undefined) {
|
|
65
|
+
defects.push('"dsh.bundle" is not an object');
|
|
66
|
+
}
|
|
67
|
+
const profile = value.profile;
|
|
68
|
+
if (isRecord(profile) && Array.isArray(profile.bundles)) {
|
|
69
|
+
section.profile = { bundles: profile.bundles.filter((entry) => typeof entry === 'string') };
|
|
70
|
+
}
|
|
71
|
+
const client = value.client;
|
|
72
|
+
if (isRecord(client)) {
|
|
73
|
+
section.client = client;
|
|
74
|
+
}
|
|
75
|
+
else if (client !== undefined) {
|
|
76
|
+
defects.push('"dsh.client" is not an object');
|
|
77
|
+
}
|
|
78
|
+
return section;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Read the command names `bin` installs. npm accepts both the string form,
|
|
82
|
+
* which names one command after the package, and the object form.
|
|
83
|
+
* @param parsed - the parsed manifest object.
|
|
84
|
+
* @returns the command names, in declaration order.
|
|
85
|
+
*/
|
|
86
|
+
function readBinNames(parsed) {
|
|
87
|
+
const bin = parsed.bin;
|
|
88
|
+
if (typeof bin === 'string')
|
|
89
|
+
return [typeof parsed.name === 'string' ? parsed.name : '<unnamed>'];
|
|
90
|
+
if (!isRecord(bin))
|
|
91
|
+
return [];
|
|
92
|
+
return Object.keys(bin);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse an untrusted `package.json`.
|
|
96
|
+
* @param text - the file's UTF-8 content.
|
|
97
|
+
* @returns the narrowed manifest, including any shape defects found.
|
|
98
|
+
* @throws ManifestError when the text is not a JSON object.
|
|
99
|
+
*/
|
|
100
|
+
export function parseManifest(text) {
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new ManifestError(`package.json is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
if (!isRecord(parsed))
|
|
109
|
+
throw new ManifestError('package.json must hold a JSON object');
|
|
110
|
+
const defects = [];
|
|
111
|
+
const files = parsed.files;
|
|
112
|
+
const exportsValue = parsed.exports;
|
|
113
|
+
return {
|
|
114
|
+
name: typeof parsed.name === 'string' ? parsed.name : '<unnamed>',
|
|
115
|
+
version: typeof parsed.version === 'string' ? parsed.version : '<unversioned>',
|
|
116
|
+
license: typeof parsed.license === 'string' ? parsed.license : null,
|
|
117
|
+
scripts: stringMap(parsed, 'scripts', defects),
|
|
118
|
+
dependencies: stringMap(parsed, 'dependencies', defects),
|
|
119
|
+
peerDependencies: stringMap(parsed, 'peerDependencies', defects),
|
|
120
|
+
optionalDependencies: stringMap(parsed, 'optionalDependencies', defects),
|
|
121
|
+
devDependencies: stringMap(parsed, 'devDependencies', defects),
|
|
122
|
+
files: Array.isArray(files) ? files.filter((entry) => typeof entry === 'string') : null,
|
|
123
|
+
binNames: readBinNames(parsed),
|
|
124
|
+
exportPaths: isRecord(exportsValue) ? Object.keys(exportsValue) : [],
|
|
125
|
+
dsh: readDshSection(parsed.dsh, defects),
|
|
126
|
+
defects,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Every package name the manifest admits the package may load at runtime:
|
|
131
|
+
* its own name, its dependencies, and its peer dependencies. Used to decide
|
|
132
|
+
* whether an inserted Cordis row names a module the manifest accounts for.
|
|
133
|
+
* @param manifest - the parsed manifest.
|
|
134
|
+
* @returns the declared package names.
|
|
135
|
+
*/
|
|
136
|
+
export function declaredPackages(manifest) {
|
|
137
|
+
return new Set([
|
|
138
|
+
manifest.name,
|
|
139
|
+
...Object.keys(manifest.dependencies),
|
|
140
|
+
...Object.keys(manifest.peerDependencies),
|
|
141
|
+
...Object.keys(manifest.optionalDependencies),
|
|
142
|
+
]);
|
|
143
|
+
}
|