owlrunkit 0.13.0 → 0.14.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/CHANGELOG.md +14 -0
- package/README.md +22 -4
- package/docs/architecture/OWLCODA_RUN_KIT_CONTRACT_V0_2.md +79 -0
- package/integrations/codex/skills/owlcoda-runkit/SKILL.md +101 -2
- package/integrations/codex/skills/owlcoda-runkit/assets/templates/config.json +1 -1
- package/package.json +1 -1
- package/packages/attest/distribution-manifest-v1.json +5 -0
- package/packages/attest/src/quick.mjs +5 -0
- package/registry-release-policy-v1.json +1 -1
- package/sbom.spdx.json +5 -5
- package/scripts/runkit-contract/core-contract.mjs +6 -1
- package/scripts/runkit-contract/onboarding-doctor.mjs +607 -0
- package/scripts/runkit-contract/profile-onboarding.mjs +332 -0
- package/scripts/runkit-contract/registry-adoption-gate.mjs +10 -7
- package/scripts/runkit-contract/registry-adoption.mjs +197 -0
- package/scripts/runkit-contract/resume-execution.mjs +8 -2
- package/scripts/runkit-contract/runkit-cli.mjs +174 -6
- package/scripts/runkit-contract/schemas/adoption-evidence-v1.schema.json +63 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
fstatSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
openSync,
|
|
6
|
+
readSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
currentCoreIdentity,
|
|
13
|
+
validateProjectConfigV2,
|
|
14
|
+
} from "./core-contract.mjs";
|
|
15
|
+
import {
|
|
16
|
+
expectedRegistryTarballUrl,
|
|
17
|
+
isCanonicalSha512Integrity,
|
|
18
|
+
} from "./registry-adoption-gate.mjs";
|
|
19
|
+
|
|
20
|
+
const PACKAGE_NAME = "owlrunkit";
|
|
21
|
+
const EXPECTED_VERSION = "0.14.0";
|
|
22
|
+
const MAX_JSON_BYTES = 1_048_576;
|
|
23
|
+
const MAX_JSON_DEPTH = 64;
|
|
24
|
+
const MAX_JSON_NODES = 50_000;
|
|
25
|
+
|
|
26
|
+
class StrictJsonParser {
|
|
27
|
+
constructor(text) {
|
|
28
|
+
this.text = text;
|
|
29
|
+
this.index = 0;
|
|
30
|
+
this.nodes = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parse() {
|
|
34
|
+
const value = this.parseValue(0);
|
|
35
|
+
this.skipWhitespace();
|
|
36
|
+
if (this.index !== this.text.length) this.fail("unexpected trailing content");
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
parseValue(depth) {
|
|
41
|
+
this.skipWhitespace();
|
|
42
|
+
this.nodes += 1;
|
|
43
|
+
if (this.nodes > MAX_JSON_NODES) throw new Error("JSON input exceeds the node limit");
|
|
44
|
+
const character = this.text[this.index];
|
|
45
|
+
if (character === "{") return this.parseObject(depth);
|
|
46
|
+
if (character === "[") return this.parseArray(depth);
|
|
47
|
+
if (character === '"') return this.parseString();
|
|
48
|
+
if (character === "t") return this.parseLiteral("true", true);
|
|
49
|
+
if (character === "f") return this.parseLiteral("false", false);
|
|
50
|
+
if (character === "n") return this.parseLiteral("null", null);
|
|
51
|
+
if (character === "-" || (character >= "0" && character <= "9")) {
|
|
52
|
+
return this.parseNumber();
|
|
53
|
+
}
|
|
54
|
+
this.fail("expected a JSON value");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
parseObject(depth) {
|
|
58
|
+
const childDepth = this.enter(depth);
|
|
59
|
+
this.index += 1;
|
|
60
|
+
this.skipWhitespace();
|
|
61
|
+
const value = {};
|
|
62
|
+
const keys = new Set();
|
|
63
|
+
if (this.text[this.index] === "}") {
|
|
64
|
+
this.index += 1;
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
while (true) {
|
|
68
|
+
if (this.text[this.index] !== '"') this.fail("expected an object key");
|
|
69
|
+
const key = this.parseString();
|
|
70
|
+
if (keys.has(key)) throw new Error(`duplicate object key: ${key}`);
|
|
71
|
+
keys.add(key);
|
|
72
|
+
this.skipWhitespace();
|
|
73
|
+
if (this.text[this.index] !== ":") this.fail("expected ':' after an object key");
|
|
74
|
+
this.index += 1;
|
|
75
|
+
Object.defineProperty(value, key, {
|
|
76
|
+
value: this.parseValue(childDepth),
|
|
77
|
+
enumerable: true,
|
|
78
|
+
configurable: true,
|
|
79
|
+
writable: true,
|
|
80
|
+
});
|
|
81
|
+
this.skipWhitespace();
|
|
82
|
+
if (this.text[this.index] === "}") {
|
|
83
|
+
this.index += 1;
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
if (this.text[this.index] !== ",") this.fail("expected ',' or '}' in an object");
|
|
87
|
+
this.index += 1;
|
|
88
|
+
this.skipWhitespace();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parseArray(depth) {
|
|
93
|
+
const childDepth = this.enter(depth);
|
|
94
|
+
this.index += 1;
|
|
95
|
+
this.skipWhitespace();
|
|
96
|
+
const value = [];
|
|
97
|
+
if (this.text[this.index] === "]") {
|
|
98
|
+
this.index += 1;
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
while (true) {
|
|
102
|
+
value.push(this.parseValue(childDepth));
|
|
103
|
+
this.skipWhitespace();
|
|
104
|
+
if (this.text[this.index] === "]") {
|
|
105
|
+
this.index += 1;
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
if (this.text[this.index] !== ",") this.fail("expected ',' or ']' in an array");
|
|
109
|
+
this.index += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
parseString() {
|
|
114
|
+
const start = this.index;
|
|
115
|
+
this.index += 1;
|
|
116
|
+
while (this.index < this.text.length) {
|
|
117
|
+
const character = this.text[this.index];
|
|
118
|
+
if (character === '"') {
|
|
119
|
+
this.index += 1;
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(this.text.slice(start, this.index));
|
|
122
|
+
} catch {
|
|
123
|
+
this.fail("invalid JSON string");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (character.charCodeAt(0) < 0x20) this.fail("unescaped control character");
|
|
127
|
+
if (character === "\\") {
|
|
128
|
+
this.index += 1;
|
|
129
|
+
const escape = this.text[this.index];
|
|
130
|
+
if (escape === "u") {
|
|
131
|
+
const digits = this.text.slice(this.index + 1, this.index + 5);
|
|
132
|
+
if (!/^[0-9a-fA-F]{4}$/.test(digits)) this.fail("invalid Unicode escape");
|
|
133
|
+
this.index += 4;
|
|
134
|
+
} else if (!'"\\/bfnrt'.includes(escape)) {
|
|
135
|
+
this.fail("invalid escape");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
this.index += 1;
|
|
139
|
+
}
|
|
140
|
+
this.fail("unterminated JSON string");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
parseLiteral(token, value) {
|
|
144
|
+
if (this.text.slice(this.index, this.index + token.length) !== token) {
|
|
145
|
+
this.fail(`invalid literal: expected ${token}`);
|
|
146
|
+
}
|
|
147
|
+
this.index += token.length;
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
parseNumber() {
|
|
152
|
+
const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(
|
|
153
|
+
this.text.slice(this.index),
|
|
154
|
+
);
|
|
155
|
+
if (!match) this.fail("invalid JSON number");
|
|
156
|
+
this.index += match[0].length;
|
|
157
|
+
const value = Number(match[0]);
|
|
158
|
+
if (!Number.isFinite(value)) this.fail("JSON number is outside the finite range");
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
enter(depth) {
|
|
163
|
+
const nextDepth = depth + 1;
|
|
164
|
+
if (nextDepth > MAX_JSON_DEPTH) throw new Error("JSON input exceeds the nesting limit");
|
|
165
|
+
return nextDepth;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
skipWhitespace() {
|
|
169
|
+
while (this.index < this.text.length && [" ", "\t", "\n", "\r"].includes(this.text[this.index])) {
|
|
170
|
+
this.index += 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fail(message) {
|
|
175
|
+
throw new Error(`${message} at byte offset ${this.index}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseJsonStrict(text) {
|
|
180
|
+
if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > MAX_JSON_BYTES) {
|
|
181
|
+
throw new Error("JSON input exceeds the input-byte limit");
|
|
182
|
+
}
|
|
183
|
+
return new StrictJsonParser(text).parse();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function resolveBoundedWorkspaceRoot(workspaceRoot) {
|
|
187
|
+
const requested = path.resolve(workspaceRoot);
|
|
188
|
+
const stat = lstatSync(requested);
|
|
189
|
+
if (
|
|
190
|
+
stat.isSymbolicLink()
|
|
191
|
+
|| !stat.isDirectory()
|
|
192
|
+
|| realpathSync(requested) !== requested
|
|
193
|
+
) {
|
|
194
|
+
throw new Error("workspace root must be a real directory without symlink ancestors");
|
|
195
|
+
}
|
|
196
|
+
return requested;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function identityMatches(left, right) {
|
|
200
|
+
return left.dev === right.dev
|
|
201
|
+
&& left.ino === right.ino
|
|
202
|
+
&& left.size === right.size
|
|
203
|
+
&& left.mtimeMs === right.mtimeMs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function readWorkspaceFileBounded(root, relativePath, { optional = false } = {}) {
|
|
207
|
+
const target = path.resolve(root, relativePath);
|
|
208
|
+
if (!strictDescendant(root, target)) throw new Error("workspace file escaped the workspace root");
|
|
209
|
+
const segments = path.relative(root, target).split(path.sep);
|
|
210
|
+
let current = root;
|
|
211
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
212
|
+
current = path.join(current, segments[index]);
|
|
213
|
+
let stat;
|
|
214
|
+
try {
|
|
215
|
+
stat = lstatSync(current);
|
|
216
|
+
} catch (error) {
|
|
217
|
+
if (optional && error?.code === "ENOENT") return null;
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
if (stat.isSymbolicLink()) throw new Error("workspace file path contains a symlink");
|
|
221
|
+
const leaf = index === segments.length - 1;
|
|
222
|
+
if ((!leaf && !stat.isDirectory()) || (leaf && !stat.isFile())) {
|
|
223
|
+
throw new Error("workspace file path has an invalid entry type");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const realTarget = realpathSync(target);
|
|
227
|
+
if (!strictDescendant(root, realTarget)) throw new Error("workspace file escaped the workspace root");
|
|
228
|
+
|
|
229
|
+
const descriptor = openSync(target, "r");
|
|
230
|
+
try {
|
|
231
|
+
const before = fstatSync(descriptor);
|
|
232
|
+
if (!before.isFile() || before.size > MAX_JSON_BYTES) {
|
|
233
|
+
throw new Error("workspace file exceeds the input-byte limit");
|
|
234
|
+
}
|
|
235
|
+
const buffer = Buffer.allocUnsafe(MAX_JSON_BYTES + 1);
|
|
236
|
+
let offset = 0;
|
|
237
|
+
while (offset <= MAX_JSON_BYTES) {
|
|
238
|
+
const bytesRead = readSync(descriptor, buffer, offset, buffer.length - offset, offset);
|
|
239
|
+
if (bytesRead === 0) break;
|
|
240
|
+
offset += bytesRead;
|
|
241
|
+
}
|
|
242
|
+
if (offset > MAX_JSON_BYTES) throw new Error("workspace file exceeds the input-byte limit");
|
|
243
|
+
const after = fstatSync(descriptor);
|
|
244
|
+
const currentStat = lstatSync(target);
|
|
245
|
+
if (
|
|
246
|
+
currentStat.isSymbolicLink()
|
|
247
|
+
|| !currentStat.isFile()
|
|
248
|
+
|| !identityMatches(before, after)
|
|
249
|
+
|| !identityMatches(after, currentStat)
|
|
250
|
+
|| realpathSync(target) !== realTarget
|
|
251
|
+
) {
|
|
252
|
+
throw new Error("workspace file identity changed while reading");
|
|
253
|
+
}
|
|
254
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset));
|
|
255
|
+
} finally {
|
|
256
|
+
closeSync(descriptor);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function readWorkspaceJsonBounded(root, relativePath, options) {
|
|
261
|
+
const text = readWorkspaceFileBounded(root, relativePath, options);
|
|
262
|
+
return text === null ? null : parseJsonStrict(text);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function readResponseJsonBounded(response) {
|
|
266
|
+
if (!response.body) throw new Error("registry response body is missing");
|
|
267
|
+
const reader = response.body.getReader();
|
|
268
|
+
const chunks = [];
|
|
269
|
+
let total = 0;
|
|
270
|
+
try {
|
|
271
|
+
while (true) {
|
|
272
|
+
const { done, value } = await reader.read();
|
|
273
|
+
if (done) break;
|
|
274
|
+
total += value.byteLength;
|
|
275
|
+
if (total > MAX_JSON_BYTES) {
|
|
276
|
+
await reader.cancel();
|
|
277
|
+
throw new Error("registry response exceeds the input-byte limit");
|
|
278
|
+
}
|
|
279
|
+
chunks.push(Buffer.from(value));
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
reader.releaseLock();
|
|
283
|
+
}
|
|
284
|
+
return parseJsonStrict(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function strictDescendant(root, candidate) {
|
|
288
|
+
const relative = path.relative(root, candidate);
|
|
289
|
+
return relative !== ""
|
|
290
|
+
&& relative !== ".."
|
|
291
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
292
|
+
&& !path.isAbsolute(relative);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function assertPathWithoutSymlinks(root, target, { targetType }) {
|
|
296
|
+
const realRoot = realpathSync(root);
|
|
297
|
+
const relative = path.relative(realRoot, target);
|
|
298
|
+
if (!strictDescendant(realRoot, target)) {
|
|
299
|
+
throw new Error("installed_package_path_invalid");
|
|
300
|
+
}
|
|
301
|
+
let current = realRoot;
|
|
302
|
+
for (const segment of relative.split(path.sep)) {
|
|
303
|
+
current = path.join(current, segment);
|
|
304
|
+
const stat = lstatSync(current);
|
|
305
|
+
if (stat.isSymbolicLink()) throw new Error("installed_package_symlink_rejected");
|
|
306
|
+
}
|
|
307
|
+
const finalStat = lstatSync(target);
|
|
308
|
+
if (
|
|
309
|
+
(targetType === "file" && !finalStat.isFile())
|
|
310
|
+
|| (targetType === "directory" && !finalStat.isDirectory())
|
|
311
|
+
) {
|
|
312
|
+
throw new Error("installed_package_path_invalid");
|
|
313
|
+
}
|
|
314
|
+
const realTarget = realpathSync(target);
|
|
315
|
+
if (!strictDescendant(realRoot, realTarget)) {
|
|
316
|
+
throw new Error("installed_package_path_invalid");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function dependencyDeclarations(manifest) {
|
|
321
|
+
return ["dependencies", "devDependencies", "optionalDependencies"]
|
|
322
|
+
.filter((section) => Object.hasOwn(manifest[section] ?? {}, PACKAGE_NAME))
|
|
323
|
+
.map((section) => ({ section, spec: manifest[section][PACKAGE_NAME] }));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function readLocalInstallBinding({ workspaceRoot } = {}) {
|
|
327
|
+
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
328
|
+
const manifestPath = path.join(root, "package.json");
|
|
329
|
+
const lockPath = path.join(root, "package-lock.json");
|
|
330
|
+
const installedRoot = path.join(root, "node_modules", PACKAGE_NAME);
|
|
331
|
+
const installedManifestPath = path.join(installedRoot, "package.json");
|
|
332
|
+
try {
|
|
333
|
+
for (const [target, targetType] of [
|
|
334
|
+
[installedRoot, "directory"],
|
|
335
|
+
]) {
|
|
336
|
+
assertPathWithoutSymlinks(root, target, { targetType });
|
|
337
|
+
}
|
|
338
|
+
const manifest = readWorkspaceJsonBounded(root, "package.json");
|
|
339
|
+
const lock = readWorkspaceJsonBounded(root, "package-lock.json");
|
|
340
|
+
const installedManifest = readWorkspaceJsonBounded(
|
|
341
|
+
root,
|
|
342
|
+
path.join("node_modules", PACKAGE_NAME, "package.json"),
|
|
343
|
+
);
|
|
344
|
+
const declarations = dependencyDeclarations(manifest);
|
|
345
|
+
if (declarations.length !== 1) {
|
|
346
|
+
return {
|
|
347
|
+
status: "mismatch",
|
|
348
|
+
issueCodes: ["registry_install_binding_ambiguous"],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const requestedSpec = declarations[0].spec;
|
|
352
|
+
const lockBinding = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
|
|
353
|
+
if (
|
|
354
|
+
requestedSpec !== EXPECTED_VERSION
|
|
355
|
+
|| lockBinding?.version !== EXPECTED_VERSION
|
|
356
|
+
|| installedManifest.name !== PACKAGE_NAME
|
|
357
|
+
|| installedManifest.version !== EXPECTED_VERSION
|
|
358
|
+
|| typeof lockBinding.resolved !== "string"
|
|
359
|
+
|| typeof lockBinding.integrity !== "string"
|
|
360
|
+
) {
|
|
361
|
+
return {
|
|
362
|
+
status: "mismatch",
|
|
363
|
+
issueCodes: ["registry_install_binding_mismatch"],
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
status: "bound",
|
|
368
|
+
requestedSpec: `${PACKAGE_NAME}@${requestedSpec}`,
|
|
369
|
+
packageName: PACKAGE_NAME,
|
|
370
|
+
version: EXPECTED_VERSION,
|
|
371
|
+
resolved: lockBinding.resolved,
|
|
372
|
+
integrity: lockBinding.integrity,
|
|
373
|
+
};
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return {
|
|
376
|
+
status: "mismatch",
|
|
377
|
+
issueCodes: [
|
|
378
|
+
error instanceof Error && error.message.startsWith("installed_package_")
|
|
379
|
+
? error.message
|
|
380
|
+
: "registry_install_binding_mismatch",
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function readRegistryExact({
|
|
387
|
+
registryUrl,
|
|
388
|
+
packageName = PACKAGE_NAME,
|
|
389
|
+
version = EXPECTED_VERSION,
|
|
390
|
+
timeoutMs = 1_500,
|
|
391
|
+
} = {}) {
|
|
392
|
+
const base = registryUrl.replace(/\/+$/, "");
|
|
393
|
+
const url = `${base}/${encodeURIComponent(packageName)}/${encodeURIComponent(version)}`;
|
|
394
|
+
let response;
|
|
395
|
+
try {
|
|
396
|
+
response = await fetch(url, {
|
|
397
|
+
headers: {
|
|
398
|
+
accept: "application/json",
|
|
399
|
+
},
|
|
400
|
+
redirect: "manual",
|
|
401
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
404
|
+
return {
|
|
405
|
+
status: "unavailable",
|
|
406
|
+
issueCodes: ["registry_dns_or_timeout"],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (response.status === 401 || response.status === 403) {
|
|
410
|
+
return {
|
|
411
|
+
status: "unauthorized",
|
|
412
|
+
issueCodes: ["registry_auth_failed"],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (response.status === 404) {
|
|
416
|
+
return {
|
|
417
|
+
status: "version_missing",
|
|
418
|
+
issueCodes: ["registry_version_missing"],
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (response.status >= 300 && response.status < 400) {
|
|
422
|
+
return {
|
|
423
|
+
status: "invalid",
|
|
424
|
+
issueCodes: ["registry_redirect_rejected"],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
return {
|
|
429
|
+
status: "unavailable",
|
|
430
|
+
issueCodes: [`registry_http_${response.status}`],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const metadata = await readResponseJsonBounded(response);
|
|
435
|
+
const dist = metadata.dist;
|
|
436
|
+
if (
|
|
437
|
+
metadata.name !== packageName
|
|
438
|
+
|| metadata.version !== version
|
|
439
|
+
|| !/^[0-9a-f]{40}$/i.test(dist?.shasum ?? "")
|
|
440
|
+
|| !isCanonicalSha512Integrity(dist?.integrity)
|
|
441
|
+
|| dist?.tarball !== expectedRegistryTarballUrl(packageName, version)
|
|
442
|
+
) {
|
|
443
|
+
return {
|
|
444
|
+
status: "invalid",
|
|
445
|
+
issueCodes: ["registry_release_evidence_invalid"],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
schemaVersion: "OwlCodaRunKitRegistryReleaseEvidenceV1",
|
|
450
|
+
status: "registry_verified",
|
|
451
|
+
registry: base,
|
|
452
|
+
packageName,
|
|
453
|
+
version,
|
|
454
|
+
shasum: dist.shasum,
|
|
455
|
+
integrity: dist.integrity,
|
|
456
|
+
tarballUrl: dist.tarball,
|
|
457
|
+
authorizationGranted: false,
|
|
458
|
+
};
|
|
459
|
+
} catch {
|
|
460
|
+
return {
|
|
461
|
+
status: "invalid",
|
|
462
|
+
issueCodes: ["registry_release_evidence_invalid"],
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function readProjectScripts(root) {
|
|
468
|
+
try {
|
|
469
|
+
const manifest = readWorkspaceJsonBounded(root, "package.json");
|
|
470
|
+
return Object.keys(manifest.scripts ?? {}).sort();
|
|
471
|
+
} catch {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function configCheck(root) {
|
|
477
|
+
try {
|
|
478
|
+
const config = readWorkspaceJsonBounded(
|
|
479
|
+
root,
|
|
480
|
+
path.join(".owlcoda", "runkit", "config.json"),
|
|
481
|
+
{ optional: true },
|
|
482
|
+
);
|
|
483
|
+
if (config === null) return { status: "missing" };
|
|
484
|
+
const gate = validateProjectConfigV2(config);
|
|
485
|
+
return gate.valid ? { status: "ok" } : { status: "invalid", issues: gate.issues };
|
|
486
|
+
} catch {
|
|
487
|
+
return { status: "invalid" };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function profilesCheck(root) {
|
|
492
|
+
try {
|
|
493
|
+
const profiles = readWorkspaceJsonBounded(
|
|
494
|
+
root,
|
|
495
|
+
path.join(".owlcoda", "runkit", "profiles.json"),
|
|
496
|
+
{ optional: true },
|
|
497
|
+
);
|
|
498
|
+
if (profiles === null) return { status: "missing" };
|
|
499
|
+
return profiles.schemaVersion === "OwlCodaRunKitProfilesV1" && Array.isArray(profiles.profiles)
|
|
500
|
+
? { status: "ok", count: profiles.profiles.length }
|
|
501
|
+
: { status: "invalid" };
|
|
502
|
+
} catch {
|
|
503
|
+
return { status: "invalid" };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function runDoctor({
|
|
508
|
+
workspaceRoot,
|
|
509
|
+
registryUrl,
|
|
510
|
+
timeoutMs = 1_500,
|
|
511
|
+
registryClient,
|
|
512
|
+
} = {}) {
|
|
513
|
+
const root = resolveBoundedWorkspaceRoot(workspaceRoot);
|
|
514
|
+
const core = currentCoreIdentity();
|
|
515
|
+
const installed = readLocalInstallBinding({ workspaceRoot: root });
|
|
516
|
+
const registry = registryClient
|
|
517
|
+
? await registryClient.readExact({ packageName: PACKAGE_NAME, version: EXPECTED_VERSION })
|
|
518
|
+
: await readRegistryExact({
|
|
519
|
+
registryUrl,
|
|
520
|
+
packageName: PACKAGE_NAME,
|
|
521
|
+
version: EXPECTED_VERSION,
|
|
522
|
+
timeoutMs,
|
|
523
|
+
});
|
|
524
|
+
const nodeOk = Number(process.versions.node.split(".")[0]) >= 20;
|
|
525
|
+
const config = configCheck(root);
|
|
526
|
+
const profiles = profilesCheck(root);
|
|
527
|
+
const checks = {
|
|
528
|
+
node: {
|
|
529
|
+
status: nodeOk ? "ok" : "unsupported",
|
|
530
|
+
version: process.versions.node,
|
|
531
|
+
},
|
|
532
|
+
core: {
|
|
533
|
+
status: core.coreVersion === EXPECTED_VERSION ? "ok" : "mismatch",
|
|
534
|
+
version: core.coreVersion,
|
|
535
|
+
manifestSha256: core.coreManifestSha256,
|
|
536
|
+
},
|
|
537
|
+
config,
|
|
538
|
+
installedPackage: installed.status === "bound"
|
|
539
|
+
? { status: "ok", version: installed.version }
|
|
540
|
+
: { status: "mismatch" },
|
|
541
|
+
registry: registry.status === "registry_verified"
|
|
542
|
+
? { status: "ok", version: registry.version }
|
|
543
|
+
: { status: "issue", issueCodes: registry.issueCodes ?? [] },
|
|
544
|
+
profiles,
|
|
545
|
+
projectScripts: {
|
|
546
|
+
status: "observed",
|
|
547
|
+
detected: readProjectScripts(root),
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
const issueCodes = [...(registry.issueCodes ?? [])];
|
|
551
|
+
if (!nodeOk) issueCodes.push("node_version_unsupported");
|
|
552
|
+
if (checks.core.status !== "ok") issueCodes.push("core_version_mismatch");
|
|
553
|
+
if (config.status === "missing") issueCodes.push("runkit_config_missing");
|
|
554
|
+
if (config.status === "invalid") issueCodes.push("runkit_config_invalid");
|
|
555
|
+
if (profiles.status === "missing") issueCodes.push("profiles_missing");
|
|
556
|
+
if (profiles.status === "invalid") issueCodes.push("profiles_invalid");
|
|
557
|
+
if (installed.status !== "bound") {
|
|
558
|
+
issueCodes.push(...(installed.issueCodes ?? ["registry_install_binding_mismatch"]));
|
|
559
|
+
}
|
|
560
|
+
if (
|
|
561
|
+
registry.status === "registry_verified"
|
|
562
|
+
&& (
|
|
563
|
+
installed.status !== "bound"
|
|
564
|
+
|| installed.version !== registry.version
|
|
565
|
+
|| installed.integrity !== registry.integrity
|
|
566
|
+
|| installed.resolved !== registry.tarballUrl
|
|
567
|
+
)
|
|
568
|
+
) {
|
|
569
|
+
issueCodes.push("registry_install_binding_mismatch");
|
|
570
|
+
}
|
|
571
|
+
const uniqueIssues = [...new Set(issueCodes)].sort();
|
|
572
|
+
const nextAllowedAction = uniqueIssues.length === 0
|
|
573
|
+
? "detect_profiles"
|
|
574
|
+
: uniqueIssues.some((code) => code === "node_version_unsupported")
|
|
575
|
+
? "upgrade_node"
|
|
576
|
+
: uniqueIssues.some((code) => code === "core_version_mismatch")
|
|
577
|
+
? "install_matching_core"
|
|
578
|
+
: uniqueIssues.some((code) => code.startsWith("runkit_config_"))
|
|
579
|
+
? "run_init_or_repair_config"
|
|
580
|
+
: uniqueIssues.some((code) => code.startsWith("profiles_"))
|
|
581
|
+
? "detect_or_repair_profiles"
|
|
582
|
+
: "repair_registry_binding";
|
|
583
|
+
return {
|
|
584
|
+
schemaVersion: "OwlCodaRunKitDoctorReportV1",
|
|
585
|
+
status: uniqueIssues.length === 0 ? "ready" : "issues_found",
|
|
586
|
+
checks,
|
|
587
|
+
issueCodes: uniqueIssues,
|
|
588
|
+
nextAllowedAction,
|
|
589
|
+
authorizationGranted: false,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function formatDoctorHuman(report) {
|
|
594
|
+
return [
|
|
595
|
+
`Doctor: ${report.status}`,
|
|
596
|
+
`Node: ${report.checks.node.status} (${report.checks.node.version})`,
|
|
597
|
+
`Core: ${report.checks.core.status} (${report.checks.core.version})`,
|
|
598
|
+
`Installed package: ${report.checks.installedPackage.status}`,
|
|
599
|
+
`Registry: ${report.checks.registry.status}`,
|
|
600
|
+
`Profiles: ${report.checks.profiles.status}`,
|
|
601
|
+
...(report.issueCodes.length > 0 ? [`Issues: ${report.issueCodes.join(", ")}`] : []),
|
|
602
|
+
`Next: ${report.nextAllowedAction === "detect_profiles"
|
|
603
|
+
? "owlrunkit profiles detect --workspace . --dry-run"
|
|
604
|
+
: report.nextAllowedAction}`,
|
|
605
|
+
"",
|
|
606
|
+
].join("\n");
|
|
607
|
+
}
|