codex-plugin-doctor 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/core/review-bundle.js +72 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -358,9 +358,9 @@ jobs:
|
|
|
358
358
|
runs-on: ubuntu-latest
|
|
359
359
|
steps:
|
|
360
360
|
- uses: actions/checkout@v5
|
|
361
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
361
|
+
- uses: Esquetta/CodexPluginDoctor@v1.13.0
|
|
362
362
|
with:
|
|
363
|
-
version: "1.
|
|
363
|
+
version: "1.13.0"
|
|
364
364
|
path: .
|
|
365
365
|
runtime: "true"
|
|
366
366
|
policy: codex-publish
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDoctorAttestation, renderDoctorAttestationJson, verifyDoctorAttestation } from "./attestation.js";
|
|
5
5
|
import { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceJson, verifyDoctorReleaseEvidence } from "./release-evidence.js";
|
|
@@ -10,14 +10,60 @@ import { readJsonFile } from "./read-json-file.js";
|
|
|
10
10
|
function isPlainObject(value) {
|
|
11
11
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
12
|
}
|
|
13
|
+
function hasExactKeys(value, keys) {
|
|
14
|
+
const actualKeys = Object.keys(value);
|
|
15
|
+
return actualKeys.length === keys.length && keys.every((key) => actualKeys.includes(key));
|
|
16
|
+
}
|
|
17
|
+
function isStatus(value) {
|
|
18
|
+
return value === "pass" || value === "warn" || value === "fail";
|
|
19
|
+
}
|
|
20
|
+
function isRuntimePolicyDecision(value) {
|
|
21
|
+
return value === "allow" ||
|
|
22
|
+
value === "review" ||
|
|
23
|
+
value === "sandbox_recommended" ||
|
|
24
|
+
value === "deny";
|
|
25
|
+
}
|
|
26
|
+
function isBundleFileMap(value) {
|
|
27
|
+
const expectedKeys = Object.keys(relativeBundleFiles());
|
|
28
|
+
return isPlainObject(value) &&
|
|
29
|
+
hasExactKeys(value, expectedKeys) &&
|
|
30
|
+
expectedKeys.every((key) => typeof value[key] === "string");
|
|
31
|
+
}
|
|
32
|
+
function isIntegrityEntry(value) {
|
|
33
|
+
if (!isPlainObject(value)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const bytes = value.bytes;
|
|
37
|
+
return typeof value.path === "string" &&
|
|
38
|
+
typeof value.digest === "string" &&
|
|
39
|
+
/^sha256:[a-f0-9]{64}$/.test(value.digest) &&
|
|
40
|
+
typeof bytes === "number" &&
|
|
41
|
+
Number.isSafeInteger(bytes) &&
|
|
42
|
+
bytes >= 0;
|
|
43
|
+
}
|
|
44
|
+
function isManifestIntegrity(value) {
|
|
45
|
+
return isPlainObject(value) &&
|
|
46
|
+
value.algorithm === "sha256" &&
|
|
47
|
+
isPlainObject(value.files) &&
|
|
48
|
+
Object.values(value.files).every(isIntegrityEntry);
|
|
49
|
+
}
|
|
13
50
|
function isDoctorReviewBundleManifest(value) {
|
|
14
51
|
return isPlainObject(value) &&
|
|
15
52
|
value.schemaVersion === "1.0.0" &&
|
|
16
53
|
value.kind === "doctor.review.bundle" &&
|
|
54
|
+
typeof value.generatedAt === "string" &&
|
|
55
|
+
typeof value.version === "string" &&
|
|
17
56
|
typeof value.targetPath === "string" &&
|
|
18
57
|
typeof value.outputDirectory === "string" &&
|
|
58
|
+
isStatus(value.status) &&
|
|
59
|
+
(value.exitCode === 0 || value.exitCode === 1) &&
|
|
19
60
|
isPlainObject(value.summary) &&
|
|
20
|
-
|
|
61
|
+
isRuntimePolicyDecision(value.summary.runtimePolicy) &&
|
|
62
|
+
typeof value.summary.releaseReady === "boolean" &&
|
|
63
|
+
isStatus(value.summary.attestation) &&
|
|
64
|
+
(value.summary.releaseEvidence === "pass" || value.summary.releaseEvidence === "fail") &&
|
|
65
|
+
isBundleFileMap(value.files) &&
|
|
66
|
+
(value.integrity === undefined || isManifestIntegrity(value.integrity));
|
|
21
67
|
}
|
|
22
68
|
function statusRank(status) {
|
|
23
69
|
return status === "fail" ? 2 : status === "warn" ? 1 : 0;
|
|
@@ -50,6 +96,21 @@ function isPathInsideDirectory(candidatePath, directoryPath) {
|
|
|
50
96
|
const relativePath = path.relative(directoryPath, candidatePath);
|
|
51
97
|
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
52
98
|
}
|
|
99
|
+
async function resolveBundleArtifactPath(bundleDirectory, relativePath) {
|
|
100
|
+
const resolvedBundleDirectory = path.resolve(bundleDirectory);
|
|
101
|
+
const artifactPath = path.resolve(resolvedBundleDirectory, relativePath);
|
|
102
|
+
if (!isPathInsideDirectory(artifactPath, resolvedBundleDirectory)) {
|
|
103
|
+
throw new Error("Bundle artifact path resolves outside the bundle directory.");
|
|
104
|
+
}
|
|
105
|
+
const [canonicalBundleDirectory, canonicalArtifactPath] = await Promise.all([
|
|
106
|
+
realpath(resolvedBundleDirectory),
|
|
107
|
+
realpath(artifactPath)
|
|
108
|
+
]);
|
|
109
|
+
if (!isPathInsideDirectory(canonicalArtifactPath, canonicalBundleDirectory)) {
|
|
110
|
+
throw new Error("Bundle artifact canonical path resolves outside the bundle directory.");
|
|
111
|
+
}
|
|
112
|
+
return canonicalArtifactPath;
|
|
113
|
+
}
|
|
53
114
|
function bundleStatus(runtimePolicy, releaseEvidence) {
|
|
54
115
|
if (runtimePolicy.status === "fail" || releaseEvidence.status === "fail") {
|
|
55
116
|
return "fail";
|
|
@@ -171,7 +232,7 @@ export function renderDoctorReviewBundleJson(bundle) {
|
|
|
171
232
|
return JSON.stringify(bundle.manifest, null, 2);
|
|
172
233
|
}
|
|
173
234
|
async function readBundleJsonFile(bundleDirectory, relativePath) {
|
|
174
|
-
return readJsonFile(
|
|
235
|
+
return readJsonFile(await resolveBundleArtifactPath(bundleDirectory, relativePath));
|
|
175
236
|
}
|
|
176
237
|
async function readBundleDiffSnapshot(bundleDirectory) {
|
|
177
238
|
const manifestArtifact = await readBundleJsonFile(bundleDirectory, "manifest.json");
|
|
@@ -348,15 +409,7 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
|
|
|
348
409
|
const files = manifest?.files ?? relativeBundleFiles();
|
|
349
410
|
for (const [fileKey, relativePath] of Object.entries(files)) {
|
|
350
411
|
try {
|
|
351
|
-
const artifactPath =
|
|
352
|
-
if (!isPathInsideDirectory(artifactPath, resolvedBundleDirectory)) {
|
|
353
|
-
checks.push({
|
|
354
|
-
id: `review_bundle.file.${fileKey}`,
|
|
355
|
-
status: "fail",
|
|
356
|
-
message: `${relativePath} resolves outside the review bundle directory.`
|
|
357
|
-
});
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
412
|
+
const artifactPath = await resolveBundleArtifactPath(resolvedBundleDirectory, relativePath);
|
|
360
413
|
const fileStat = await stat(artifactPath);
|
|
361
414
|
checks.push({
|
|
362
415
|
id: `review_bundle.file.${fileKey}`,
|
|
@@ -427,8 +480,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
|
|
|
427
480
|
});
|
|
428
481
|
continue;
|
|
429
482
|
}
|
|
430
|
-
const
|
|
431
|
-
if (!isPathInsideDirectory(
|
|
483
|
+
const resolvedIntegrityPath = path.resolve(resolvedBundleDirectory, expected.path);
|
|
484
|
+
if (!isPathInsideDirectory(resolvedIntegrityPath, resolvedBundleDirectory)) {
|
|
432
485
|
integrityStatus = "fail";
|
|
433
486
|
checks.push({
|
|
434
487
|
id: `review_bundle.integrity.${fileKey}.path`,
|
|
@@ -437,6 +490,7 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
|
|
|
437
490
|
});
|
|
438
491
|
continue;
|
|
439
492
|
}
|
|
493
|
+
const integrityPath = await resolveBundleArtifactPath(resolvedBundleDirectory, expected.path);
|
|
440
494
|
const content = await readFile(integrityPath);
|
|
441
495
|
const digest = sha256(content);
|
|
442
496
|
const matches = digest === expected.digest && content.byteLength === expected.bytes;
|
|
@@ -505,7 +559,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
|
|
|
505
559
|
});
|
|
506
560
|
}
|
|
507
561
|
try {
|
|
508
|
-
|
|
562
|
+
const attestationPath = await resolveBundleArtifactPath(resolvedBundleDirectory, files.attestationJson);
|
|
563
|
+
attestation = await verifyDoctorAttestation(attestationPath, targetPath, { signingKey: options.signingKey });
|
|
509
564
|
checks.push({
|
|
510
565
|
id: "review_bundle.attestation",
|
|
511
566
|
status: attestation.status,
|
|
@@ -522,7 +577,8 @@ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
|
|
|
522
577
|
});
|
|
523
578
|
}
|
|
524
579
|
try {
|
|
525
|
-
|
|
580
|
+
const releaseEvidencePath = await resolveBundleArtifactPath(resolvedBundleDirectory, files.releaseEvidenceJson);
|
|
581
|
+
releaseEvidence = await verifyDoctorReleaseEvidence(releaseEvidencePath, {
|
|
526
582
|
signingKey: options.signingKey,
|
|
527
583
|
targetPath
|
|
528
584
|
});
|
package/package.json
CHANGED