loadout-ai 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/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
const MAX_SOURCE_BYTES = 1_000_000;
|
|
5
|
+
const MAX_LINES = 10_000;
|
|
6
|
+
const MAX_DEPTH = 30;
|
|
7
|
+
const MAX_NODES = 50_000;
|
|
8
|
+
const GIT_SHA = /^[0-9a-f]{40}$/i;
|
|
9
|
+
const HASH_ENVELOPE = /^(?:sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128}|[0-9a-f]{64})$/i;
|
|
10
|
+
function object(value, label) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12
|
+
throw new Error(`${label} must be a mapping`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function array(value, label) {
|
|
16
|
+
if (!Array.isArray(value))
|
|
17
|
+
throw new Error(`${label} must be a sequence`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function string(value, label) {
|
|
21
|
+
if (typeof value !== "string" || !value.trim())
|
|
22
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function optionalString(value, label) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
return string(value, label);
|
|
29
|
+
}
|
|
30
|
+
function ownEntries(value) {
|
|
31
|
+
return Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
32
|
+
}
|
|
33
|
+
function sourceRecord(filename, text) {
|
|
34
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
35
|
+
if (bytes > MAX_SOURCE_BYTES)
|
|
36
|
+
throw new Error(`Import source exceeds the ${MAX_SOURCE_BYTES}-byte read-only limit`);
|
|
37
|
+
return {
|
|
38
|
+
filename,
|
|
39
|
+
text,
|
|
40
|
+
bytes,
|
|
41
|
+
sha256: createHash("sha256").update(text).digest("hex"),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function stripYamlComment(value) {
|
|
45
|
+
let single = false;
|
|
46
|
+
let double = false;
|
|
47
|
+
let escaped = false;
|
|
48
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
49
|
+
const character = value[index];
|
|
50
|
+
if (escaped) {
|
|
51
|
+
escaped = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (double && character === "\\") {
|
|
55
|
+
escaped = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!double && character === "'") {
|
|
59
|
+
if (single && value[index + 1] === "'")
|
|
60
|
+
index += 1;
|
|
61
|
+
else
|
|
62
|
+
single = !single;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!single && character === '"') {
|
|
66
|
+
double = !double;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!single &&
|
|
70
|
+
!double &&
|
|
71
|
+
character === "#" &&
|
|
72
|
+
(index === 0 || /\s/.test(value[index - 1])))
|
|
73
|
+
return value.slice(0, index).trimEnd();
|
|
74
|
+
}
|
|
75
|
+
if (single || double)
|
|
76
|
+
throw new Error("Unterminated YAML quoted scalar");
|
|
77
|
+
return value.trimEnd();
|
|
78
|
+
}
|
|
79
|
+
function tokenizeYaml(text) {
|
|
80
|
+
if (text.includes("\0"))
|
|
81
|
+
throw new Error("YAML contains a NUL byte");
|
|
82
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
83
|
+
if (lines.length > MAX_LINES)
|
|
84
|
+
throw new Error(`YAML exceeds the ${MAX_LINES}-line read-only limit`);
|
|
85
|
+
const tokens = [];
|
|
86
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
87
|
+
const line = lines[index];
|
|
88
|
+
if (line.includes("\t"))
|
|
89
|
+
throw new Error(`YAML line ${index + 1} contains a tab`);
|
|
90
|
+
const indent = line.length - line.trimStart().length;
|
|
91
|
+
const content = stripYamlComment(line.slice(indent)).trimEnd();
|
|
92
|
+
if (!content)
|
|
93
|
+
continue;
|
|
94
|
+
if (content === "---" || content === "...")
|
|
95
|
+
throw new Error(`YAML document markers are outside the supported subset (line ${index + 1})`);
|
|
96
|
+
if (/^(?:!|&|\*)/.test(content) || /:\s*(?:!|&|\*)/.test(content))
|
|
97
|
+
throw new Error(`YAML tags, anchors, and aliases are not allowed (line ${index + 1})`);
|
|
98
|
+
if (/^<<\s*:/.test(content))
|
|
99
|
+
throw new Error(`YAML merge keys are not allowed (line ${index + 1})`);
|
|
100
|
+
tokens.push({ indent, content, line: index + 1 });
|
|
101
|
+
}
|
|
102
|
+
const normalized = [];
|
|
103
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
104
|
+
const token = tokens[index];
|
|
105
|
+
if (!token.content.startsWith("? ")) {
|
|
106
|
+
if (token.content === "?")
|
|
107
|
+
throw new Error(`Complex YAML mapping keys are outside the supported subset (line ${token.line})`);
|
|
108
|
+
normalized.push(token);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const value = tokens[index + 1];
|
|
112
|
+
if (!value ||
|
|
113
|
+
value.indent !== token.indent ||
|
|
114
|
+
!(value.content === ":" || value.content.startsWith(": ")))
|
|
115
|
+
throw new Error(`YAML explicit key at line ${token.line} must have one scalar value line`);
|
|
116
|
+
const key = token.content.slice(2).trim();
|
|
117
|
+
if (!key)
|
|
118
|
+
throw new Error(`YAML explicit key at line ${token.line} is empty`);
|
|
119
|
+
normalized.push({
|
|
120
|
+
indent: token.indent,
|
|
121
|
+
content: `${JSON.stringify(key)}:${value.content.slice(1)}`,
|
|
122
|
+
line: token.line,
|
|
123
|
+
});
|
|
124
|
+
index += 1;
|
|
125
|
+
}
|
|
126
|
+
if (normalized.length === 0)
|
|
127
|
+
throw new Error("YAML document is empty");
|
|
128
|
+
if (normalized[0].indent !== 0)
|
|
129
|
+
throw new Error("YAML root must begin at indentation zero");
|
|
130
|
+
return normalized;
|
|
131
|
+
}
|
|
132
|
+
function splitMapping(value, line) {
|
|
133
|
+
let single = false;
|
|
134
|
+
let double = false;
|
|
135
|
+
let square = 0;
|
|
136
|
+
let curly = 0;
|
|
137
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
138
|
+
const character = value[index];
|
|
139
|
+
if (!double && character === "'") {
|
|
140
|
+
if (single && value[index + 1] === "'")
|
|
141
|
+
index += 1;
|
|
142
|
+
else
|
|
143
|
+
single = !single;
|
|
144
|
+
}
|
|
145
|
+
else if (!single && character === '"') {
|
|
146
|
+
let slashes = 0;
|
|
147
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1)
|
|
148
|
+
slashes += 1;
|
|
149
|
+
if (slashes % 2 === 0)
|
|
150
|
+
double = !double;
|
|
151
|
+
}
|
|
152
|
+
else if (!single && !double) {
|
|
153
|
+
if (character === "[")
|
|
154
|
+
square += 1;
|
|
155
|
+
else if (character === "]")
|
|
156
|
+
square -= 1;
|
|
157
|
+
else if (character === "{")
|
|
158
|
+
curly += 1;
|
|
159
|
+
else if (character === "}")
|
|
160
|
+
curly -= 1;
|
|
161
|
+
else if (character === ":" &&
|
|
162
|
+
square === 0 &&
|
|
163
|
+
curly === 0 &&
|
|
164
|
+
(index === value.length - 1 || /\s/.test(value[index + 1]))) {
|
|
165
|
+
const key = value.slice(0, index).trim();
|
|
166
|
+
if (!key)
|
|
167
|
+
throw new Error(`YAML line ${line} has an empty key`);
|
|
168
|
+
return [key, value.slice(index + 1).trim()];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`YAML line ${line} is not a supported mapping entry`);
|
|
173
|
+
}
|
|
174
|
+
function splitFlow(value) {
|
|
175
|
+
const result = [];
|
|
176
|
+
let start = 0;
|
|
177
|
+
let single = false;
|
|
178
|
+
let double = false;
|
|
179
|
+
let square = 0;
|
|
180
|
+
let curly = 0;
|
|
181
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
182
|
+
const character = value[index];
|
|
183
|
+
if (!double && character === "'") {
|
|
184
|
+
if (single && value[index + 1] === "'")
|
|
185
|
+
index += 1;
|
|
186
|
+
else
|
|
187
|
+
single = !single;
|
|
188
|
+
}
|
|
189
|
+
else if (!single && character === '"') {
|
|
190
|
+
let slashes = 0;
|
|
191
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1)
|
|
192
|
+
slashes += 1;
|
|
193
|
+
if (slashes % 2 === 0)
|
|
194
|
+
double = !double;
|
|
195
|
+
}
|
|
196
|
+
else if (!single && !double) {
|
|
197
|
+
if (character === "[")
|
|
198
|
+
square += 1;
|
|
199
|
+
else if (character === "]")
|
|
200
|
+
square -= 1;
|
|
201
|
+
else if (character === "{")
|
|
202
|
+
curly += 1;
|
|
203
|
+
else if (character === "}")
|
|
204
|
+
curly -= 1;
|
|
205
|
+
else if (character === "," && square === 0 && curly === 0) {
|
|
206
|
+
result.push(value.slice(start, index).trim());
|
|
207
|
+
start = index + 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
result.push(value.slice(start).trim());
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
function parseYamlScalar(value, line) {
|
|
215
|
+
if (value === "" || value === "~" || value === "null" || value === "Null")
|
|
216
|
+
return null;
|
|
217
|
+
if (value === "true" || value === "True")
|
|
218
|
+
return true;
|
|
219
|
+
if (value === "false" || value === "False")
|
|
220
|
+
return false;
|
|
221
|
+
if (/^-?(?:0|[1-9][0-9]*)$/.test(value)) {
|
|
222
|
+
const parsed = Number(value);
|
|
223
|
+
if (Number.isSafeInteger(parsed))
|
|
224
|
+
return parsed;
|
|
225
|
+
}
|
|
226
|
+
if (value.startsWith('"')) {
|
|
227
|
+
try {
|
|
228
|
+
const parsed = JSON.parse(value);
|
|
229
|
+
if (typeof parsed !== "string")
|
|
230
|
+
throw new Error("not a string");
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
throw new Error(`YAML line ${line} has an invalid double-quoted scalar`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (value.startsWith("'")) {
|
|
238
|
+
if (!value.endsWith("'") || value.length < 2)
|
|
239
|
+
throw new Error(`YAML line ${line} has an invalid single-quoted scalar`);
|
|
240
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
241
|
+
}
|
|
242
|
+
if (value === "[]")
|
|
243
|
+
return [];
|
|
244
|
+
if (value === "{}")
|
|
245
|
+
return {};
|
|
246
|
+
if (value.startsWith("[") && value.endsWith("]"))
|
|
247
|
+
return splitFlow(value.slice(1, -1))
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.map((entry) => parseYamlScalar(entry, line));
|
|
250
|
+
if (value.startsWith("{") || value.endsWith("}") || /^[|>]/.test(value))
|
|
251
|
+
throw new Error(`YAML line ${line} uses a flow mapping or block scalar outside the supported subset`);
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
function yamlKey(value, line) {
|
|
255
|
+
const parsed = parseYamlScalar(value, line);
|
|
256
|
+
if (typeof parsed !== "string" || !parsed)
|
|
257
|
+
throw new Error(`YAML line ${line} has a non-string or empty key`);
|
|
258
|
+
if (parsed === "<<")
|
|
259
|
+
throw new Error(`YAML merge keys are not allowed (line ${line})`);
|
|
260
|
+
return parsed;
|
|
261
|
+
}
|
|
262
|
+
function addNode(state, depth) {
|
|
263
|
+
state.nodes += 1;
|
|
264
|
+
if (state.nodes > MAX_NODES)
|
|
265
|
+
throw new Error(`YAML exceeds the ${MAX_NODES}-node read-only limit`);
|
|
266
|
+
if (depth > MAX_DEPTH)
|
|
267
|
+
throw new Error(`YAML exceeds the ${MAX_DEPTH}-level nesting limit`);
|
|
268
|
+
}
|
|
269
|
+
function parseYamlBlock(state, start, indent, depth) {
|
|
270
|
+
addNode(state, depth);
|
|
271
|
+
const first = state.tokens[start];
|
|
272
|
+
if (!first || first.indent !== indent)
|
|
273
|
+
throw new Error("Invalid YAML indentation state");
|
|
274
|
+
if (first.content === "-" || first.content.startsWith("- "))
|
|
275
|
+
return parseYamlSequence(state, start, indent, depth);
|
|
276
|
+
return parseYamlMapping(state, start, indent, depth);
|
|
277
|
+
}
|
|
278
|
+
function parseYamlMapping(state, start, indent, depth) {
|
|
279
|
+
const result = {};
|
|
280
|
+
let index = start;
|
|
281
|
+
while (index < state.tokens.length) {
|
|
282
|
+
const token = state.tokens[index];
|
|
283
|
+
if (token.indent < indent)
|
|
284
|
+
break;
|
|
285
|
+
if (token.indent > indent)
|
|
286
|
+
throw new Error(`Unexpected YAML indentation at line ${token.line}`);
|
|
287
|
+
if (token.content === "-" || token.content.startsWith("- "))
|
|
288
|
+
break;
|
|
289
|
+
const [rawKey, rawValue] = splitMapping(token.content, token.line);
|
|
290
|
+
const key = yamlKey(rawKey, token.line);
|
|
291
|
+
if (Object.hasOwn(result, key))
|
|
292
|
+
throw new Error(`Duplicate YAML key '${key}' at line ${token.line}`);
|
|
293
|
+
addNode(state, depth + 1);
|
|
294
|
+
index += 1;
|
|
295
|
+
if (rawValue)
|
|
296
|
+
result[key] = parseYamlScalar(rawValue, token.line);
|
|
297
|
+
else if (index < state.tokens.length &&
|
|
298
|
+
(state.tokens[index].indent > indent ||
|
|
299
|
+
(state.tokens[index].indent === indent &&
|
|
300
|
+
(state.tokens[index].content === "-" ||
|
|
301
|
+
state.tokens[index].content.startsWith("- "))))) {
|
|
302
|
+
const child = parseYamlBlock(state, index, state.tokens[index].indent, depth + 1);
|
|
303
|
+
result[key] = child.value;
|
|
304
|
+
index = child.next;
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
result[key] = null;
|
|
308
|
+
}
|
|
309
|
+
return { value: result, next: index };
|
|
310
|
+
}
|
|
311
|
+
function parseYamlSequence(state, start, indent, depth) {
|
|
312
|
+
const result = [];
|
|
313
|
+
let index = start;
|
|
314
|
+
while (index < state.tokens.length) {
|
|
315
|
+
const token = state.tokens[index];
|
|
316
|
+
if (token.indent < indent)
|
|
317
|
+
break;
|
|
318
|
+
if (token.indent !== indent)
|
|
319
|
+
throw new Error(`Unexpected YAML indentation at line ${token.line}`);
|
|
320
|
+
if (!(token.content === "-" || token.content.startsWith("- ")))
|
|
321
|
+
break;
|
|
322
|
+
const rest = token.content.slice(1).trim();
|
|
323
|
+
addNode(state, depth + 1);
|
|
324
|
+
index += 1;
|
|
325
|
+
if (!rest) {
|
|
326
|
+
if (index >= state.tokens.length || state.tokens[index].indent <= indent)
|
|
327
|
+
result.push(null);
|
|
328
|
+
else {
|
|
329
|
+
const child = parseYamlBlock(state, index, state.tokens[index].indent, depth + 1);
|
|
330
|
+
result.push(child.value);
|
|
331
|
+
index = child.next;
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
let pair;
|
|
336
|
+
try {
|
|
337
|
+
pair = splitMapping(rest, token.line);
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
pair = undefined;
|
|
341
|
+
}
|
|
342
|
+
if (!pair) {
|
|
343
|
+
result.push(parseYamlScalar(rest, token.line));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const item = {};
|
|
347
|
+
const key = yamlKey(pair[0], token.line);
|
|
348
|
+
if (pair[1])
|
|
349
|
+
item[key] = parseYamlScalar(pair[1], token.line);
|
|
350
|
+
else if (index < state.tokens.length &&
|
|
351
|
+
state.tokens[index].indent > indent) {
|
|
352
|
+
const childIndent = state.tokens[index].indent;
|
|
353
|
+
const child = parseYamlBlock(state, index, childIndent, depth + 2);
|
|
354
|
+
item[key] = child.value;
|
|
355
|
+
index = child.next;
|
|
356
|
+
}
|
|
357
|
+
else
|
|
358
|
+
item[key] = null;
|
|
359
|
+
if (index < state.tokens.length && state.tokens[index].indent > indent) {
|
|
360
|
+
const continuationIndent = state.tokens[index].indent;
|
|
361
|
+
const continuation = parseYamlMapping(state, index, continuationIndent, depth + 1);
|
|
362
|
+
for (const [continuationKey, value] of Object.entries(continuation.value)) {
|
|
363
|
+
if (Object.hasOwn(item, continuationKey))
|
|
364
|
+
throw new Error(`Duplicate YAML key '${continuationKey}' in sequence mapping`);
|
|
365
|
+
item[continuationKey] = value;
|
|
366
|
+
}
|
|
367
|
+
index = continuation.next;
|
|
368
|
+
}
|
|
369
|
+
result.push(item);
|
|
370
|
+
}
|
|
371
|
+
return { value: result, next: index };
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Parse only Loadout's audited YAML subset. This is not a general YAML parser
|
|
375
|
+
* and deliberately rejects executable/ambiguous YAML features.
|
|
376
|
+
*/
|
|
377
|
+
export function parseBoundedYaml(text) {
|
|
378
|
+
const source = sourceRecord("input.yml", text);
|
|
379
|
+
const tokens = tokenizeYaml(source.text);
|
|
380
|
+
const state = { tokens, nodes: 0 };
|
|
381
|
+
const parsed = parseYamlBlock(state, 0, 0, 0);
|
|
382
|
+
if (parsed.next !== tokens.length)
|
|
383
|
+
throw new Error(`Unsupported YAML structure at line ${tokens[parsed.next].line}`);
|
|
384
|
+
return parsed.value;
|
|
385
|
+
}
|
|
386
|
+
function executionBoundary() {
|
|
387
|
+
return {
|
|
388
|
+
readOnly: true,
|
|
389
|
+
externalCommandsRun: false,
|
|
390
|
+
networkRequestsMade: false,
|
|
391
|
+
filesWritten: false,
|
|
392
|
+
installReady: false,
|
|
393
|
+
statement: "This parser only inventories declarative evidence. It does not resolve, fetch, execute, install, or write any dependency.",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function countHashMap(value, path) {
|
|
397
|
+
if (value === undefined)
|
|
398
|
+
return 0;
|
|
399
|
+
const hashes = object(value, path);
|
|
400
|
+
for (const [file, hash] of Object.entries(hashes))
|
|
401
|
+
if (typeof hash !== "string" || !HASH_ENVELOPE.test(hash))
|
|
402
|
+
throw new Error(`${path}.${file} has an invalid integrity envelope`);
|
|
403
|
+
return Object.keys(hashes).length;
|
|
404
|
+
}
|
|
405
|
+
function githubSource(repository, ref, path) {
|
|
406
|
+
return {
|
|
407
|
+
type: "github",
|
|
408
|
+
repository,
|
|
409
|
+
...(ref ? { ref } : {}),
|
|
410
|
+
...(path ? { path } : {}),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function gitSource(url, ref, path) {
|
|
414
|
+
return {
|
|
415
|
+
type: "git",
|
|
416
|
+
url,
|
|
417
|
+
...(ref ? { ref } : {}),
|
|
418
|
+
...(path ? { path } : {}),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function parseGitHubUrl(value) {
|
|
422
|
+
const match = value.match(/^(?:https?:\/\/github\.com\/|git@github\.com:)([^/#]+\/[^/#]+?)(?:\.git)?(?:#(.+))?$/);
|
|
423
|
+
if (!match)
|
|
424
|
+
return undefined;
|
|
425
|
+
return { repository: match[1], ...(match[2] ? { ref: match[2] } : {}) };
|
|
426
|
+
}
|
|
427
|
+
function uniqueId(value, index) {
|
|
428
|
+
const normalized = value
|
|
429
|
+
.replace(/^[a-z]+:\/\//i, "")
|
|
430
|
+
.replace(/[^a-zA-Z0-9._/-]+/g, "-")
|
|
431
|
+
.replace(/^[-/]+|[-/]+$/g, "")
|
|
432
|
+
.replace(/\//g, "-")
|
|
433
|
+
.toLowerCase();
|
|
434
|
+
return normalized || `imported-${index + 1}`;
|
|
435
|
+
}
|
|
436
|
+
function apmStringCandidate(declaration, index, kind, development, defaultHost) {
|
|
437
|
+
const warnings = [];
|
|
438
|
+
if (kind === "mcp") {
|
|
439
|
+
return {
|
|
440
|
+
id: uniqueId(declaration, index),
|
|
441
|
+
dependencyKind: kind,
|
|
442
|
+
development,
|
|
443
|
+
declared: declaration,
|
|
444
|
+
disposition: "manual-review-only",
|
|
445
|
+
warnings: [
|
|
446
|
+
"MCP identifiers do not contain enough runtime, transport, or credential evidence for safe conversion.",
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
if (/^(?:\.{1,2}\/|\/|~\/)/.test(declaration)) {
|
|
451
|
+
return {
|
|
452
|
+
id: uniqueId(basename(declaration), index),
|
|
453
|
+
dependencyKind: kind,
|
|
454
|
+
development,
|
|
455
|
+
declared: declaration,
|
|
456
|
+
source: { type: "local", path: declaration },
|
|
457
|
+
disposition: "ready-for-loadout-review",
|
|
458
|
+
warnings: [
|
|
459
|
+
"Local paths are machine-relative and have no remote provenance.",
|
|
460
|
+
],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const url = parseGitHubUrl(declaration);
|
|
464
|
+
if (url) {
|
|
465
|
+
return {
|
|
466
|
+
id: uniqueId(url.repository, index),
|
|
467
|
+
dependencyKind: kind,
|
|
468
|
+
development,
|
|
469
|
+
declared: declaration,
|
|
470
|
+
source: githubSource(url.repository, url.ref),
|
|
471
|
+
disposition: "ready-for-loadout-review",
|
|
472
|
+
warnings,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const hash = declaration.lastIndexOf("#");
|
|
476
|
+
const base = hash === -1 ? declaration : declaration.slice(0, hash);
|
|
477
|
+
const ref = hash === -1 ? undefined : declaration.slice(hash + 1);
|
|
478
|
+
const segments = base.split("/");
|
|
479
|
+
if (defaultHost === "github.com" && segments.length >= 2) {
|
|
480
|
+
const repository = `${segments[0]}/${segments[1]}`;
|
|
481
|
+
const path = segments.length > 2 ? segments.slice(2).join("/") : undefined;
|
|
482
|
+
return {
|
|
483
|
+
id: uniqueId(base, index),
|
|
484
|
+
dependencyKind: kind,
|
|
485
|
+
development,
|
|
486
|
+
declared: declaration,
|
|
487
|
+
source: githubSource(repository, ref, path),
|
|
488
|
+
disposition: "ready-for-loadout-review",
|
|
489
|
+
warnings,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
id: uniqueId(base, index),
|
|
494
|
+
dependencyKind: kind,
|
|
495
|
+
development,
|
|
496
|
+
declared: declaration,
|
|
497
|
+
source: gitSource(`https://${defaultHost}/${base}.git`, ref),
|
|
498
|
+
disposition: "ready-for-loadout-review",
|
|
499
|
+
warnings: [
|
|
500
|
+
"The APM default host was preserved as an HTTPS Git assumption for review.",
|
|
501
|
+
],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
function apmObjectCandidate(declaration, index, kind, development) {
|
|
505
|
+
const identity = optionalString(declaration.id, `dependency[${index}].id`) ??
|
|
506
|
+
optionalString(declaration.git, `dependency[${index}].git`) ??
|
|
507
|
+
optionalString(declaration.path, `dependency[${index}].path`) ??
|
|
508
|
+
optionalString(declaration.registry, `dependency[${index}].registry`) ??
|
|
509
|
+
`imported-${index + 1}`;
|
|
510
|
+
if (kind === "mcp")
|
|
511
|
+
return {
|
|
512
|
+
id: uniqueId(identity, index),
|
|
513
|
+
dependencyKind: kind,
|
|
514
|
+
development,
|
|
515
|
+
declared: declaration,
|
|
516
|
+
disposition: "manual-review-only",
|
|
517
|
+
warnings: [
|
|
518
|
+
"MCP declarations are preserved for manual runtime and credential review; no server is executed.",
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
const ref = optionalString(declaration.ref, `dependency[${index}].ref`);
|
|
522
|
+
const path = optionalString(declaration.path, `dependency[${index}].path`);
|
|
523
|
+
const git = optionalString(declaration.git, `dependency[${index}].git`);
|
|
524
|
+
if (git) {
|
|
525
|
+
const github = parseGitHubUrl(git);
|
|
526
|
+
return {
|
|
527
|
+
id: uniqueId(identity, index),
|
|
528
|
+
dependencyKind: kind,
|
|
529
|
+
development,
|
|
530
|
+
declared: declaration,
|
|
531
|
+
source: github
|
|
532
|
+
? githubSource(github.repository, ref ?? github.ref, path)
|
|
533
|
+
: gitSource(git, ref, path),
|
|
534
|
+
disposition: "ready-for-loadout-review",
|
|
535
|
+
warnings: [],
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (path)
|
|
539
|
+
return {
|
|
540
|
+
id: uniqueId(identity, index),
|
|
541
|
+
dependencyKind: kind,
|
|
542
|
+
development,
|
|
543
|
+
declared: declaration,
|
|
544
|
+
source: { type: "local", path },
|
|
545
|
+
disposition: "ready-for-loadout-review",
|
|
546
|
+
warnings: [
|
|
547
|
+
"Local paths are machine-relative and have no remote provenance.",
|
|
548
|
+
],
|
|
549
|
+
};
|
|
550
|
+
const registry = optionalString(declaration.registry, `dependency[${index}].registry`);
|
|
551
|
+
const id = optionalString(declaration.id, `dependency[${index}].id`);
|
|
552
|
+
if (registry)
|
|
553
|
+
return {
|
|
554
|
+
id: uniqueId(id ?? identity, index),
|
|
555
|
+
dependencyKind: kind,
|
|
556
|
+
development,
|
|
557
|
+
declared: declaration,
|
|
558
|
+
...(id && typeof declaration.version === "string"
|
|
559
|
+
? {
|
|
560
|
+
source: {
|
|
561
|
+
type: "remote-registry",
|
|
562
|
+
registry,
|
|
563
|
+
name: id,
|
|
564
|
+
version: declaration.version,
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
: {}),
|
|
568
|
+
disposition: "requires-resolution",
|
|
569
|
+
warnings: [
|
|
570
|
+
"Registry identity or version constraint requires immutable resolution before Loadout review.",
|
|
571
|
+
],
|
|
572
|
+
};
|
|
573
|
+
if (id)
|
|
574
|
+
return apmStringCandidate(id, index, kind, development, "github.com");
|
|
575
|
+
return {
|
|
576
|
+
id: uniqueId(identity, index),
|
|
577
|
+
dependencyKind: kind,
|
|
578
|
+
development,
|
|
579
|
+
declared: declaration,
|
|
580
|
+
disposition: "requires-resolution",
|
|
581
|
+
warnings: [
|
|
582
|
+
"Registry identity requires explicit registry and immutable resolution.",
|
|
583
|
+
],
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function apmCandidates(manifest, unsupported, preservedPaths) {
|
|
587
|
+
const dependencies = manifest.dependencies === undefined
|
|
588
|
+
? {}
|
|
589
|
+
: object(manifest.dependencies, "dependencies");
|
|
590
|
+
const devDependencies = manifest.devDependencies === undefined
|
|
591
|
+
? {}
|
|
592
|
+
: object(manifest.devDependencies, "devDependencies");
|
|
593
|
+
const defaultHost = optionalString(manifest.default_host, "default_host") ?? "github.com";
|
|
594
|
+
const result = [];
|
|
595
|
+
for (const [block, development] of [
|
|
596
|
+
[dependencies, false],
|
|
597
|
+
[devDependencies, true],
|
|
598
|
+
]) {
|
|
599
|
+
for (const kind of ["apm", "mcp"]) {
|
|
600
|
+
const entries = block[kind] === undefined ? [] : array(block[kind], kind);
|
|
601
|
+
entries.forEach((entry, index) => {
|
|
602
|
+
const path = `${development ? "devDependencies" : "dependencies"}.${kind}[${index}]`;
|
|
603
|
+
preservedPaths.push(path);
|
|
604
|
+
result.push(typeof entry === "string"
|
|
605
|
+
? apmStringCandidate(string(entry, path), result.length, kind === "apm" ? "package" : "mcp", development, defaultHost)
|
|
606
|
+
: apmObjectCandidate(object(entry, path), result.length, kind === "apm" ? "package" : "mcp", development));
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
for (const [field, value] of ownEntries(block))
|
|
610
|
+
if (!new Set(["apm", "mcp", "conflict_resolution"]).has(field))
|
|
611
|
+
unsupported.push({
|
|
612
|
+
path: `${development ? "devDependencies" : "dependencies"}.${field}`,
|
|
613
|
+
value,
|
|
614
|
+
reason: "Dependency block field is preserved but has no Loadout import semantic.",
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
const APM_MANIFEST_FIELDS = new Set([
|
|
620
|
+
"name",
|
|
621
|
+
"version",
|
|
622
|
+
"description",
|
|
623
|
+
"author",
|
|
624
|
+
"license",
|
|
625
|
+
"default_host",
|
|
626
|
+
"target",
|
|
627
|
+
"type",
|
|
628
|
+
"scripts",
|
|
629
|
+
"includes",
|
|
630
|
+
"registries",
|
|
631
|
+
"dependencies",
|
|
632
|
+
"devDependencies",
|
|
633
|
+
"compilation",
|
|
634
|
+
"policy",
|
|
635
|
+
"marketplace",
|
|
636
|
+
]);
|
|
637
|
+
export function planApmManifestImport(text, filename = "apm.yml") {
|
|
638
|
+
const source = sourceRecord(filename, text);
|
|
639
|
+
const manifest = object(parseBoundedYaml(text), "APM manifest");
|
|
640
|
+
const packageName = string(manifest.name, "APM manifest name");
|
|
641
|
+
const packageVersion = string(manifest.version, "APM manifest version");
|
|
642
|
+
const unsupported = [];
|
|
643
|
+
const preservedPaths = ["name", "version"];
|
|
644
|
+
for (const [field, value] of ownEntries(manifest)) {
|
|
645
|
+
if (field.startsWith("x-")) {
|
|
646
|
+
unsupported.push({
|
|
647
|
+
path: field,
|
|
648
|
+
value,
|
|
649
|
+
reason: "OpenAPM vendor extension is byte-preserved but not interpreted.",
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else if (!APM_MANIFEST_FIELDS.has(field))
|
|
653
|
+
unsupported.push({
|
|
654
|
+
path: field,
|
|
655
|
+
value,
|
|
656
|
+
reason: "Unknown OpenAPM field is byte-preserved but not interpreted.",
|
|
657
|
+
});
|
|
658
|
+
else if (!new Set(["name", "version", "dependencies", "devDependencies"]).has(field))
|
|
659
|
+
unsupported.push({
|
|
660
|
+
path: field,
|
|
661
|
+
value,
|
|
662
|
+
reason: "Manifest metadata/runtime field is preserved but not converted to a Loadout package.",
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
const candidates = apmCandidates(manifest, unsupported, preservedPaths);
|
|
666
|
+
const hasScripts = manifest.scripts !== undefined &&
|
|
667
|
+
Object.keys(object(manifest.scripts, "scripts")).length > 0;
|
|
668
|
+
return {
|
|
669
|
+
schemaVersion: 1,
|
|
670
|
+
format: "openapm-v0.1",
|
|
671
|
+
artifact: "manifest",
|
|
672
|
+
source,
|
|
673
|
+
packageName,
|
|
674
|
+
packageVersion,
|
|
675
|
+
candidates,
|
|
676
|
+
preservedPaths,
|
|
677
|
+
unsupported,
|
|
678
|
+
trust: {
|
|
679
|
+
level: "unverified",
|
|
680
|
+
evidence: ["Exact source bytes and SHA-256 preserved locally."],
|
|
681
|
+
uncertainties: [
|
|
682
|
+
"Manifest declarations are intent, not proof of resolved content or publisher identity.",
|
|
683
|
+
"OpenAPM v0.1 is a working draft and Loadout does not claim Consumer conformance.",
|
|
684
|
+
],
|
|
685
|
+
},
|
|
686
|
+
warnings: [
|
|
687
|
+
...(hasScripts
|
|
688
|
+
? [
|
|
689
|
+
"APM scripts were preserved as unsupported metadata and were not executed.",
|
|
690
|
+
]
|
|
691
|
+
: []),
|
|
692
|
+
"Targets, registries, compilation, policy, marketplace, and MCP runtime semantics require manual review.",
|
|
693
|
+
],
|
|
694
|
+
executionBoundary: executionBoundary(),
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const APM_LOCK_FIELDS = new Set([
|
|
698
|
+
"lockfile_version",
|
|
699
|
+
"generated_at",
|
|
700
|
+
"apm_version",
|
|
701
|
+
"dependencies",
|
|
702
|
+
"mcp_servers",
|
|
703
|
+
"mcp_configs",
|
|
704
|
+
"local_deployed_files",
|
|
705
|
+
"local_deployed_file_hashes",
|
|
706
|
+
"attestations",
|
|
707
|
+
]);
|
|
708
|
+
export function planApmLockEvidenceImport(text, filename = "apm.lock.yaml") {
|
|
709
|
+
const source = sourceRecord(filename, text);
|
|
710
|
+
const lock = object(parseBoundedYaml(text), "APM lockfile");
|
|
711
|
+
const version = String(lock.lockfile_version ?? "");
|
|
712
|
+
if (version !== "1" && version !== "2")
|
|
713
|
+
throw new Error("APM lockfile lockfile_version must be 1 or 2");
|
|
714
|
+
const dependencies = array(lock.dependencies, "APM lockfile dependencies");
|
|
715
|
+
const candidates = [];
|
|
716
|
+
const preservedPaths = ["lockfile_version"];
|
|
717
|
+
const unsupported = [];
|
|
718
|
+
let integrityCount = 0;
|
|
719
|
+
dependencies.forEach((entry, index) => {
|
|
720
|
+
const dependency = object(entry, `dependencies[${index}]`);
|
|
721
|
+
const name = optionalString(dependency.name, `dependencies[${index}].name`) ??
|
|
722
|
+
optionalString(dependency.repo_url, `dependencies[${index}].repo_url`) ??
|
|
723
|
+
`locked-${index + 1}`;
|
|
724
|
+
const resolvedCommit = optionalString(dependency.resolved_commit, `dependencies[${index}].resolved_commit`);
|
|
725
|
+
if (resolvedCommit && !GIT_SHA.test(resolvedCommit))
|
|
726
|
+
throw new Error(`dependencies[${index}].resolved_commit is not a 40-hex Git SHA`);
|
|
727
|
+
const integrity = optionalString(dependency.tree_sha256, `dependencies[${index}].tree_sha256`) ??
|
|
728
|
+
optionalString(dependency.resolved_hash, `dependencies[${index}].resolved_hash`) ??
|
|
729
|
+
optionalString(dependency.content_hash, `dependencies[${index}].content_hash`);
|
|
730
|
+
if (integrity && !HASH_ENVELOPE.test(integrity))
|
|
731
|
+
throw new Error(`dependencies[${index}] has an invalid integrity envelope`);
|
|
732
|
+
if (integrity)
|
|
733
|
+
integrityCount += 1;
|
|
734
|
+
integrityCount += countHashMap(dependency.deployed_file_hashes, `dependencies[${index}].deployed_file_hashes`);
|
|
735
|
+
const repoUrl = optionalString(dependency.resolved_url, `dependencies[${index}].resolved_url`) ??
|
|
736
|
+
optionalString(dependency.repo_url, `dependencies[${index}].repo_url`);
|
|
737
|
+
const localPath = optionalString(dependency.local_path, `dependencies[${index}].local_path`);
|
|
738
|
+
const virtualPath = optionalString(dependency.virtual_path, `dependencies[${index}].virtual_path`);
|
|
739
|
+
let candidateSource;
|
|
740
|
+
if (localPath)
|
|
741
|
+
candidateSource = { type: "local", path: localPath };
|
|
742
|
+
else if (repoUrl && !repoUrl.startsWith("_local/")) {
|
|
743
|
+
const github = parseGitHubUrl(repoUrl);
|
|
744
|
+
candidateSource = github
|
|
745
|
+
? githubSource(github.repository, resolvedCommit ??
|
|
746
|
+
optionalString(dependency.resolved_ref, "resolved_ref") ??
|
|
747
|
+
github.ref, virtualPath)
|
|
748
|
+
: gitSource(repoUrl, resolvedCommit ??
|
|
749
|
+
optionalString(dependency.resolved_ref, "resolved_ref"), virtualPath);
|
|
750
|
+
}
|
|
751
|
+
candidates.push({
|
|
752
|
+
id: uniqueId(name, index),
|
|
753
|
+
dependencyKind: "package",
|
|
754
|
+
development: dependency.is_dev === true,
|
|
755
|
+
declared: dependency,
|
|
756
|
+
...(candidateSource ? { source: candidateSource } : {}),
|
|
757
|
+
...(resolvedCommit ? { resolvedCommit } : {}),
|
|
758
|
+
...(integrity ? { integrity } : {}),
|
|
759
|
+
disposition: candidateSource
|
|
760
|
+
? "ready-for-loadout-review"
|
|
761
|
+
: "manual-review-only",
|
|
762
|
+
warnings: [
|
|
763
|
+
...(localPath
|
|
764
|
+
? ["Local lock evidence has no remote publisher provenance."]
|
|
765
|
+
: []),
|
|
766
|
+
...(!candidateSource
|
|
767
|
+
? ["Lock entry has no safely convertible source pointer."]
|
|
768
|
+
: []),
|
|
769
|
+
],
|
|
770
|
+
});
|
|
771
|
+
preservedPaths.push(`dependencies[${index}]`);
|
|
772
|
+
});
|
|
773
|
+
integrityCount += countHashMap(lock.local_deployed_file_hashes, "local_deployed_file_hashes");
|
|
774
|
+
for (const [field, value] of ownEntries(lock))
|
|
775
|
+
if (!new Set(["lockfile_version", "dependencies"]).has(field))
|
|
776
|
+
unsupported.push({
|
|
777
|
+
path: field,
|
|
778
|
+
value,
|
|
779
|
+
reason: APM_LOCK_FIELDS.has(field)
|
|
780
|
+
? "OpenAPM lock evidence is preserved but not converted into an executable Loadout action."
|
|
781
|
+
: "Unknown lockfile field is byte-preserved but not interpreted.",
|
|
782
|
+
});
|
|
783
|
+
return {
|
|
784
|
+
schemaVersion: 1,
|
|
785
|
+
format: "openapm-v0.1",
|
|
786
|
+
artifact: "lock-evidence",
|
|
787
|
+
source,
|
|
788
|
+
candidates,
|
|
789
|
+
preservedPaths,
|
|
790
|
+
unsupported,
|
|
791
|
+
trust: {
|
|
792
|
+
level: integrityCount > 0 ? "integrity-evidence-present" : "unverified",
|
|
793
|
+
evidence: [
|
|
794
|
+
"Exact lockfile bytes and SHA-256 preserved locally.",
|
|
795
|
+
...(integrityCount > 0
|
|
796
|
+
? [
|
|
797
|
+
`${integrityCount} declared content or file integrity digest or digests were observed.`,
|
|
798
|
+
]
|
|
799
|
+
: []),
|
|
800
|
+
],
|
|
801
|
+
uncertainties: [
|
|
802
|
+
"Declared hashes are imported as evidence only; Loadout has not fetched or re-hashed the referenced content.",
|
|
803
|
+
"A lockfile does not establish publisher identity, signature validity, or current safety.",
|
|
804
|
+
],
|
|
805
|
+
},
|
|
806
|
+
warnings: [
|
|
807
|
+
"MCP configs, deployments, attestations, and deployed-file maps remain preserved evidence only.",
|
|
808
|
+
],
|
|
809
|
+
executionBoundary: executionBoundary(),
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function openPackageCandidate(value, index, development) {
|
|
813
|
+
const declaration = object(value, `dependency[${index}]`);
|
|
814
|
+
const name = string(declaration.name, `dependency[${index}].name`);
|
|
815
|
+
const version = optionalString(declaration.version, `dependency[${index}].version`);
|
|
816
|
+
const base = optionalString(declaration.base, `dependency[${index}].base`);
|
|
817
|
+
const partialPath = optionalString(declaration.path, `dependency[${index}].path`);
|
|
818
|
+
const url = optionalString(declaration.url, `dependency[${index}].url`) ??
|
|
819
|
+
optionalString(declaration.git, `dependency[${index}].git`);
|
|
820
|
+
const legacyRef = optionalString(declaration.ref, `dependency[${index}].ref`);
|
|
821
|
+
const legacySubdirectory = optionalString(declaration.subdirectory, `dependency[${index}].subdirectory`);
|
|
822
|
+
const warnings = [];
|
|
823
|
+
if (declaration.git !== undefined ||
|
|
824
|
+
declaration.ref !== undefined ||
|
|
825
|
+
declaration.subdirectory !== undefined)
|
|
826
|
+
warnings.push("Deprecated OpenPackage source fields were preserved and normalized for review.");
|
|
827
|
+
if (partialPath)
|
|
828
|
+
warnings.push("OpenPackage partial resource selection has no exact Loadout equivalent; the path is preserved for manual review.");
|
|
829
|
+
if (url) {
|
|
830
|
+
const hash = url.lastIndexOf("#");
|
|
831
|
+
const rawUrl = hash === -1 ? url : url.slice(0, hash);
|
|
832
|
+
const embeddedRef = hash === -1 ? undefined : url.slice(hash + 1);
|
|
833
|
+
const github = parseGitHubUrl(url);
|
|
834
|
+
const sourcePath = base ?? legacySubdirectory;
|
|
835
|
+
return {
|
|
836
|
+
id: uniqueId(name, index),
|
|
837
|
+
dependencyKind: "package",
|
|
838
|
+
development,
|
|
839
|
+
declared: declaration,
|
|
840
|
+
source: github
|
|
841
|
+
? githubSource(github.repository, legacyRef ?? github.ref, sourcePath)
|
|
842
|
+
: gitSource(rawUrl, legacyRef ?? embeddedRef, sourcePath),
|
|
843
|
+
disposition: partialPath
|
|
844
|
+
? "manual-review-only"
|
|
845
|
+
: "ready-for-loadout-review",
|
|
846
|
+
warnings,
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (base)
|
|
850
|
+
return {
|
|
851
|
+
id: uniqueId(name, index),
|
|
852
|
+
dependencyKind: "package",
|
|
853
|
+
development,
|
|
854
|
+
declared: declaration,
|
|
855
|
+
source: { type: "local", path: base },
|
|
856
|
+
disposition: partialPath
|
|
857
|
+
? "manual-review-only"
|
|
858
|
+
: "ready-for-loadout-review",
|
|
859
|
+
warnings: [
|
|
860
|
+
...warnings,
|
|
861
|
+
"Local paths have no remote publisher provenance.",
|
|
862
|
+
],
|
|
863
|
+
};
|
|
864
|
+
return {
|
|
865
|
+
id: uniqueId(name, index),
|
|
866
|
+
dependencyKind: "package",
|
|
867
|
+
development,
|
|
868
|
+
declared: declaration,
|
|
869
|
+
...(version
|
|
870
|
+
? {
|
|
871
|
+
source: {
|
|
872
|
+
type: "remote-registry",
|
|
873
|
+
registry: "https://openpackage.dev",
|
|
874
|
+
name,
|
|
875
|
+
version,
|
|
876
|
+
},
|
|
877
|
+
}
|
|
878
|
+
: {}),
|
|
879
|
+
disposition: "requires-resolution",
|
|
880
|
+
warnings: [
|
|
881
|
+
...warnings,
|
|
882
|
+
"Registry versions and ranges are declarations, not immutable resolutions; registry access is intentionally not performed.",
|
|
883
|
+
],
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
const OPENPACKAGE_MANIFEST_FIELDS = new Set([
|
|
887
|
+
"name",
|
|
888
|
+
"version",
|
|
889
|
+
"private",
|
|
890
|
+
"partial",
|
|
891
|
+
"platforms",
|
|
892
|
+
"description",
|
|
893
|
+
"keywords",
|
|
894
|
+
"author",
|
|
895
|
+
"license",
|
|
896
|
+
"homepage",
|
|
897
|
+
"repository",
|
|
898
|
+
"dependencies",
|
|
899
|
+
"dev-dependencies",
|
|
900
|
+
"packages",
|
|
901
|
+
"dev-packages",
|
|
902
|
+
]);
|
|
903
|
+
export function planOpenPackageManifestImport(text, filename = "openpackage.yml") {
|
|
904
|
+
const source = sourceRecord(filename, text);
|
|
905
|
+
const manifest = object(parseBoundedYaml(text), "OpenPackage manifest");
|
|
906
|
+
const packageName = string(manifest.name, "OpenPackage manifest name");
|
|
907
|
+
const packageVersion = optionalString(manifest.version, "OpenPackage manifest version");
|
|
908
|
+
const candidates = [];
|
|
909
|
+
const preservedPaths = ["name"];
|
|
910
|
+
const unsupported = [];
|
|
911
|
+
for (const [field, development] of [
|
|
912
|
+
["dependencies", false],
|
|
913
|
+
["dev-dependencies", true],
|
|
914
|
+
["packages", false],
|
|
915
|
+
["dev-packages", true],
|
|
916
|
+
]) {
|
|
917
|
+
const values = manifest[field] === undefined ? [] : array(manifest[field], field);
|
|
918
|
+
values.forEach((value, index) => {
|
|
919
|
+
candidates.push(openPackageCandidate(value, candidates.length, development));
|
|
920
|
+
preservedPaths.push(`${field}[${index}]`);
|
|
921
|
+
});
|
|
922
|
+
if ((field === "packages" || field === "dev-packages") && values.length > 0)
|
|
923
|
+
unsupported.push({
|
|
924
|
+
path: field,
|
|
925
|
+
value: values,
|
|
926
|
+
reason: "Deprecated OpenPackage dependency field was preserved and interpreted with a migration warning.",
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
for (const [field, value] of ownEntries(manifest))
|
|
930
|
+
if (!OPENPACKAGE_MANIFEST_FIELDS.has(field))
|
|
931
|
+
unsupported.push({
|
|
932
|
+
path: field,
|
|
933
|
+
value,
|
|
934
|
+
reason: "Unknown OpenPackage field is byte-preserved but not interpreted.",
|
|
935
|
+
});
|
|
936
|
+
else if (!new Set([
|
|
937
|
+
"name",
|
|
938
|
+
"version",
|
|
939
|
+
"dependencies",
|
|
940
|
+
"dev-dependencies",
|
|
941
|
+
"packages",
|
|
942
|
+
"dev-packages",
|
|
943
|
+
]).has(field))
|
|
944
|
+
unsupported.push({
|
|
945
|
+
path: field,
|
|
946
|
+
value,
|
|
947
|
+
reason: "OpenPackage metadata is preserved but not converted to a Loadout package field.",
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
schemaVersion: 1,
|
|
951
|
+
format: "openpackage-current",
|
|
952
|
+
artifact: "manifest",
|
|
953
|
+
source,
|
|
954
|
+
packageName,
|
|
955
|
+
...(packageVersion ? { packageVersion } : {}),
|
|
956
|
+
candidates,
|
|
957
|
+
preservedPaths,
|
|
958
|
+
unsupported,
|
|
959
|
+
trust: {
|
|
960
|
+
level: "unverified",
|
|
961
|
+
evidence: ["Exact source bytes and SHA-256 preserved locally."],
|
|
962
|
+
uncertainties: [
|
|
963
|
+
"Manifest version ranges are intent and were not resolved against OpenPackage registries.",
|
|
964
|
+
"OpenPackage does not expose a stable normative manifest schema in its public docs; this parser targets its current documented types.",
|
|
965
|
+
],
|
|
966
|
+
},
|
|
967
|
+
warnings: [
|
|
968
|
+
"Platforms, resource subsets, package contents, conversions, and MCP runtime files require manual review.",
|
|
969
|
+
],
|
|
970
|
+
executionBoundary: executionBoundary(),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
export function planOpenPackageIndexEvidenceImport(text, filename = "openpackage.index.yml") {
|
|
974
|
+
const source = sourceRecord(filename, text);
|
|
975
|
+
const index = object(parseBoundedYaml(text), "OpenPackage index");
|
|
976
|
+
const packages = object(index.packages, "OpenPackage index packages");
|
|
977
|
+
const candidates = [];
|
|
978
|
+
const preservedPaths = [];
|
|
979
|
+
const unsupported = [];
|
|
980
|
+
let hashCount = 0;
|
|
981
|
+
for (const [name, rawPackage] of ownEntries(packages)) {
|
|
982
|
+
const pkg = object(rawPackage, `packages.${name}`);
|
|
983
|
+
const path = string(pkg.path, `packages.${name}.path`);
|
|
984
|
+
const files = object(pkg.files, `packages.${name}.files`);
|
|
985
|
+
for (const mappings of Object.values(files)) {
|
|
986
|
+
for (const mapping of array(mappings, `packages.${name}.files mapping`)) {
|
|
987
|
+
if (mapping && typeof mapping === "object" && !Array.isArray(mapping)) {
|
|
988
|
+
const item = mapping;
|
|
989
|
+
if (typeof item.hash === "string")
|
|
990
|
+
hashCount += 1;
|
|
991
|
+
if (typeof item.sourceHash === "string")
|
|
992
|
+
hashCount += 1;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
candidates.push({
|
|
997
|
+
id: uniqueId(name, candidates.length),
|
|
998
|
+
dependencyKind: "package",
|
|
999
|
+
development: false,
|
|
1000
|
+
declared: pkg,
|
|
1001
|
+
source: { type: "local", path },
|
|
1002
|
+
disposition: "manual-review-only",
|
|
1003
|
+
warnings: [
|
|
1004
|
+
"Workspace index paths describe installed local state, not portable package provenance.",
|
|
1005
|
+
],
|
|
1006
|
+
});
|
|
1007
|
+
preservedPaths.push(`packages.${name}`);
|
|
1008
|
+
}
|
|
1009
|
+
for (const [field, value] of ownEntries(index))
|
|
1010
|
+
if (field !== "packages")
|
|
1011
|
+
unsupported.push({
|
|
1012
|
+
path: field,
|
|
1013
|
+
value,
|
|
1014
|
+
reason: "Unknown OpenPackage index field is byte-preserved but not interpreted.",
|
|
1015
|
+
});
|
|
1016
|
+
return {
|
|
1017
|
+
schemaVersion: 1,
|
|
1018
|
+
format: "openpackage-current",
|
|
1019
|
+
artifact: "lock-evidence",
|
|
1020
|
+
source,
|
|
1021
|
+
candidates,
|
|
1022
|
+
preservedPaths,
|
|
1023
|
+
unsupported,
|
|
1024
|
+
trust: {
|
|
1025
|
+
level: hashCount > 0 ? "integrity-evidence-present" : "unverified",
|
|
1026
|
+
evidence: [
|
|
1027
|
+
"Exact index bytes and SHA-256 preserved locally.",
|
|
1028
|
+
...(hashCount > 0
|
|
1029
|
+
? [
|
|
1030
|
+
`${hashCount} workspace mapping hash value or values were observed.`,
|
|
1031
|
+
]
|
|
1032
|
+
: []),
|
|
1033
|
+
],
|
|
1034
|
+
uncertainties: [
|
|
1035
|
+
"OpenPackage index hashes use implementation-specific workspace evidence and were not revalidated.",
|
|
1036
|
+
"The index is not a portable dependency lockfile and does not prove registry or publisher identity.",
|
|
1037
|
+
],
|
|
1038
|
+
},
|
|
1039
|
+
warnings: [
|
|
1040
|
+
"File mappings, platforms, namespaces, marketplace metadata, and merge keys remain evidence only.",
|
|
1041
|
+
],
|
|
1042
|
+
executionBoundary: executionBoundary(),
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
async function readBoundedFile(path) {
|
|
1046
|
+
const absolute = resolve(path);
|
|
1047
|
+
const text = await readFile(absolute, "utf8");
|
|
1048
|
+
sourceRecord(absolute, text);
|
|
1049
|
+
return { path: absolute, text };
|
|
1050
|
+
}
|
|
1051
|
+
export async function planApmImportFiles(manifestPath, lockPath) {
|
|
1052
|
+
const manifest = await readBoundedFile(manifestPath);
|
|
1053
|
+
const lock = lockPath ? await readBoundedFile(lockPath) : undefined;
|
|
1054
|
+
return {
|
|
1055
|
+
manifest: planApmManifestImport(manifest.text, manifest.path),
|
|
1056
|
+
...(lock
|
|
1057
|
+
? { lockEvidence: planApmLockEvidenceImport(lock.text, lock.path) }
|
|
1058
|
+
: {}),
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
export async function planOpenPackageImportFiles(manifestPath, indexPath) {
|
|
1062
|
+
const manifest = await readBoundedFile(manifestPath);
|
|
1063
|
+
const index = indexPath ? await readBoundedFile(indexPath) : undefined;
|
|
1064
|
+
return {
|
|
1065
|
+
manifest: planOpenPackageManifestImport(manifest.text, manifest.path),
|
|
1066
|
+
...(index
|
|
1067
|
+
? {
|
|
1068
|
+
lockEvidence: planOpenPackageIndexEvidenceImport(index.text, index.path),
|
|
1069
|
+
}
|
|
1070
|
+
: {}),
|
|
1071
|
+
};
|
|
1072
|
+
}
|