@agentique.io/validator 0.1.0 → 0.2.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 +5 -3
- package/package.json +1 -1
- package/src/cli.mjs +0 -0
- package/src/intake/scanner.mjs +207 -23
- package/src/validator.mjs +384 -17
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Static local upload-preparation validator for Agentique resource packages.
|
|
4
4
|
|
|
5
|
-
`agentique-validator` is a no-execution checker that validates public manifests, package inventory, path safety, and upload-prep metadata without uploading, publishing, installing dependencies, or executing submitted code.
|
|
5
|
+
`agentique-validator` is a no-execution checker that validates public manifests, package inventory, path safety, registry trust metadata, parser/variant metadata, and upload-prep metadata without uploading, publishing, installing dependencies, or executing submitted code.
|
|
6
6
|
|
|
7
7
|
Local validation is not platform approval and is not safety certification. `agentique.io` remains the source of truth for upload, scan, review, moderation, publication, distribution state, and readback.
|
|
8
8
|
|
|
@@ -28,6 +28,8 @@ Exit codes:
|
|
|
28
28
|
- Blocked executable extension rejection.
|
|
29
29
|
- Secret-like value detection with redacted findings.
|
|
30
30
|
- Forbidden public-content path and term checks.
|
|
31
|
+
- Registry trust checks for creator-safe package context, creator checkpoints, generated draft boundaries, and explicit patch/delta metadata.
|
|
32
|
+
- Parser/variant checks for static parser evidence, sanitized resource graph summaries, compatibility reasons, and source-only variant states.
|
|
31
33
|
|
|
32
34
|
## External Intake
|
|
33
35
|
|
|
@@ -49,8 +51,8 @@ The report includes:
|
|
|
49
51
|
- Secret findings with redacted previews and stable fingerprints.
|
|
50
52
|
- License inventory with missing, unknown, and conflict findings.
|
|
51
53
|
|
|
52
|
-
External intake output is advisory review evidence.
|
|
54
|
+
External intake output is advisory review evidence. Registry trust and parser/variant findings are local preparation findings. Neither output is publication approval, safety assurance, moderation status, runtime compatibility proof, platform download availability, or legal review.
|
|
53
55
|
|
|
54
56
|
## Status
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
Published as `@agentique.io/validator`. Local validation output is not platform approval and is not safety certification.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
File without changes
|
package/src/intake/scanner.mjs
CHANGED
|
@@ -46,7 +46,11 @@ const DANGEROUS_CAPABILITY_RULES = Object.freeze([
|
|
|
46
46
|
}),
|
|
47
47
|
Object.freeze({
|
|
48
48
|
category: "credential-environment-access",
|
|
49
|
-
pattern: /\b(?:process\.env|os\.environ|getenv\(|GITHUB_TOKEN|AWS_SECRET_ACCESS_KEY|npm_token|pypi_token
|
|
49
|
+
pattern: /\b(?:process\.env|os\.environ|getenv\(|GITHUB_TOKEN|AWS_SECRET_ACCESS_KEY|npm_token|pypi_token)\b/i
|
|
50
|
+
}),
|
|
51
|
+
Object.freeze({
|
|
52
|
+
category: "dotenv-file-reference",
|
|
53
|
+
pattern: /(?:^|[\s"'=:\/\\])\.env(?:\.[A-Za-z0-9_-]+)?(?:\b|$)/i
|
|
50
54
|
}),
|
|
51
55
|
Object.freeze({
|
|
52
56
|
category: "encoded-payload",
|
|
@@ -222,7 +226,8 @@ async function applyLicenseInventory({ filePath, rel, findings, licenses }) {
|
|
|
222
226
|
source: "license-file",
|
|
223
227
|
expression: null,
|
|
224
228
|
normalized,
|
|
225
|
-
status: normalized ? "recognized" : "unknown"
|
|
229
|
+
status: normalized ? "recognized" : "unknown",
|
|
230
|
+
policy: licensePolicyForExpression(normalized)
|
|
226
231
|
})
|
|
227
232
|
);
|
|
228
233
|
}
|
|
@@ -255,7 +260,8 @@ async function collectPackageLicense({ filePath, rel, findings, licenses }) {
|
|
|
255
260
|
source: "package-json",
|
|
256
261
|
expression,
|
|
257
262
|
normalized,
|
|
258
|
-
status: normalized ? "recognized" : "unknown"
|
|
263
|
+
status: normalized ? "recognized" : "unknown",
|
|
264
|
+
policy: licensePolicyForExpression(normalized)
|
|
259
265
|
})
|
|
260
266
|
);
|
|
261
267
|
}
|
|
@@ -274,20 +280,123 @@ function isLicenseFileName(basename) {
|
|
|
274
280
|
return basename === "license" || basename === "licence" || basename.startsWith("license.") || basename.startsWith("licence.") || basename === "copying";
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
const LICENSE_ID_NORMALIZATION = new Map([
|
|
284
|
+
["0BSD", "0BSD"],
|
|
285
|
+
["AGPL-3.0", "AGPL-3.0-only"],
|
|
286
|
+
["AGPL-3.0-ONLY", "AGPL-3.0-only"],
|
|
287
|
+
["AGPL-3.0-OR-LATER", "AGPL-3.0-or-later"],
|
|
288
|
+
["APACHE-2.0", "Apache-2.0"],
|
|
289
|
+
["ARTISTIC-2.0", "Artistic-2.0"],
|
|
290
|
+
["BSD-2-CLAUSE", "BSD-2-Clause"],
|
|
291
|
+
["BSD-3-CLAUSE", "BSD-3-Clause"],
|
|
292
|
+
["BSL-1.1", "BSL-1.1"],
|
|
293
|
+
["CC-BY-4.0", "CC-BY-4.0"],
|
|
294
|
+
["CC0-1.0", "CC0-1.0"],
|
|
295
|
+
["GPL-2.0", "GPL-2.0-only"],
|
|
296
|
+
["GPL-2.0-ONLY", "GPL-2.0-only"],
|
|
297
|
+
["GPL-2.0-OR-LATER", "GPL-2.0-or-later"],
|
|
298
|
+
["GPL-3.0", "GPL-3.0-only"],
|
|
299
|
+
["GPL-3.0-ONLY", "GPL-3.0-only"],
|
|
300
|
+
["GPL-3.0-OR-LATER", "GPL-3.0-or-later"],
|
|
301
|
+
["ISC", "ISC"],
|
|
302
|
+
["LGPL-2.1", "LGPL-2.1-only"],
|
|
303
|
+
["LGPL-2.1-ONLY", "LGPL-2.1-only"],
|
|
304
|
+
["LGPL-2.1-OR-LATER", "LGPL-2.1-or-later"],
|
|
305
|
+
["LGPL-3.0", "LGPL-3.0-only"],
|
|
306
|
+
["LGPL-3.0-ONLY", "LGPL-3.0-only"],
|
|
307
|
+
["LGPL-3.0-OR-LATER", "LGPL-3.0-or-later"],
|
|
308
|
+
["MIT", "MIT"],
|
|
309
|
+
["MPL-2.0", "MPL-2.0"],
|
|
310
|
+
["UNLICENSE", "Unlicense"]
|
|
311
|
+
]);
|
|
312
|
+
|
|
313
|
+
const LICENSE_POLICY = new Map([
|
|
314
|
+
["0BSD", "allowed"],
|
|
315
|
+
["AGPL-3.0-only", "blocked"],
|
|
316
|
+
["AGPL-3.0-or-later", "blocked"],
|
|
317
|
+
["Apache-2.0", "allowed"],
|
|
318
|
+
["Artistic-2.0", "needs-review"],
|
|
319
|
+
["BSD-2-Clause", "allowed"],
|
|
320
|
+
["BSD-3-Clause", "allowed"],
|
|
321
|
+
["BSL-1.1", "allowed"],
|
|
322
|
+
["CC-BY-4.0", "needs-review"],
|
|
323
|
+
["CC0-1.0", "allowed"],
|
|
324
|
+
["GPL-2.0-only", "needs-review"],
|
|
325
|
+
["GPL-2.0-or-later", "needs-review"],
|
|
326
|
+
["GPL-3.0-only", "needs-review"],
|
|
327
|
+
["GPL-3.0-or-later", "needs-review"],
|
|
328
|
+
["ISC", "allowed"],
|
|
329
|
+
["LGPL-2.1-only", "needs-review"],
|
|
330
|
+
["LGPL-2.1-or-later", "needs-review"],
|
|
331
|
+
["LGPL-3.0-only", "needs-review"],
|
|
332
|
+
["LGPL-3.0-or-later", "needs-review"],
|
|
333
|
+
["MIT", "allowed"],
|
|
334
|
+
["MPL-2.0", "allowed"],
|
|
335
|
+
["Unlicense", "allowed"]
|
|
336
|
+
]);
|
|
337
|
+
|
|
277
338
|
function normalizeLicenseExpression(expression) {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
339
|
+
const trimmed = expression.trim();
|
|
340
|
+
if (!trimmed) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const single = normalizeLicenseIdentifier(trimmed);
|
|
345
|
+
if (single) {
|
|
346
|
+
return single;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const tokens = trimmed.replace(/[()]/g, " ").trim().split(/\s+(AND|OR)\s+/i);
|
|
350
|
+
if (tokens.length < 3 || tokens.length % 2 === 0) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const normalizedTokens = [];
|
|
355
|
+
for (const [index, token] of tokens.entries()) {
|
|
356
|
+
const value = token.trim();
|
|
357
|
+
if (!value) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (index % 2 === 1) {
|
|
362
|
+
const operator = value.toUpperCase();
|
|
363
|
+
if (operator !== "AND" && operator !== "OR") {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
normalizedTokens.push(operator);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const normalized = normalizeLicenseIdentifier(value);
|
|
371
|
+
if (!normalized) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
normalizedTokens.push(normalized);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return normalizedTokens.join(" ");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function normalizeLicenseIdentifier(value) {
|
|
381
|
+
return LICENSE_ID_NORMALIZATION.get(value.trim().toUpperCase()) ?? null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function licensePolicyForExpression(normalized) {
|
|
385
|
+
if (!normalized) {
|
|
386
|
+
return "unknown";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const identifiers = normalized.split(/\s+(?:AND|OR)\s+/).map((value) => value.trim()).filter(Boolean);
|
|
390
|
+
if (identifiers.some((identifier) => LICENSE_POLICY.get(identifier) === "blocked")) {
|
|
391
|
+
return "blocked";
|
|
392
|
+
}
|
|
393
|
+
if (identifiers.some((identifier) => LICENSE_POLICY.get(identifier) === "needs-review")) {
|
|
394
|
+
return "needs-review";
|
|
395
|
+
}
|
|
396
|
+
if (identifiers.every((identifier) => LICENSE_POLICY.get(identifier) === "allowed")) {
|
|
397
|
+
return "allowed";
|
|
398
|
+
}
|
|
399
|
+
return "unknown";
|
|
291
400
|
}
|
|
292
401
|
|
|
293
402
|
function normalizeLicenseText(content) {
|
|
@@ -298,20 +407,41 @@ function normalizeLicenseText(content) {
|
|
|
298
407
|
return "Apache-2.0";
|
|
299
408
|
}
|
|
300
409
|
if (/GNU GENERAL PUBLIC LICENSE[\s\S]{0,800}Version 3/i.test(content)) {
|
|
301
|
-
return "GPL-3.0";
|
|
410
|
+
return "GPL-3.0-only";
|
|
302
411
|
}
|
|
303
412
|
if (/GNU GENERAL PUBLIC LICENSE[\s\S]{0,800}Version 2/i.test(content)) {
|
|
304
|
-
return "GPL-2.0";
|
|
413
|
+
return "GPL-2.0-only";
|
|
414
|
+
}
|
|
415
|
+
if (/GNU LESSER GENERAL PUBLIC LICENSE[\s\S]{0,800}Version 3/i.test(content)) {
|
|
416
|
+
return "LGPL-3.0-only";
|
|
417
|
+
}
|
|
418
|
+
if (/GNU LESSER GENERAL PUBLIC LICENSE[\s\S]{0,800}Version 2\.1/i.test(content)) {
|
|
419
|
+
return "LGPL-2.1-only";
|
|
420
|
+
}
|
|
421
|
+
if (/GNU AFFERO GENERAL PUBLIC LICENSE[\s\S]{0,800}Version 3/i.test(content)) {
|
|
422
|
+
return "AGPL-3.0-only";
|
|
305
423
|
}
|
|
306
424
|
if (/Redistribution and use in source and binary forms/i.test(content) && /Neither the name/i.test(content)) {
|
|
307
425
|
return "BSD-3-Clause";
|
|
308
426
|
}
|
|
427
|
+
if (/Redistribution and use in source and binary forms/i.test(content)) {
|
|
428
|
+
return "BSD-2-Clause";
|
|
429
|
+
}
|
|
309
430
|
if (/ISC License/i.test(content)) {
|
|
310
431
|
return "ISC";
|
|
311
432
|
}
|
|
312
433
|
if (/Mozilla Public License Version 2\.0/i.test(content)) {
|
|
313
434
|
return "MPL-2.0";
|
|
314
435
|
}
|
|
436
|
+
if (/This is free and unencumbered software released into the public domain/i.test(content)) {
|
|
437
|
+
return "Unlicense";
|
|
438
|
+
}
|
|
439
|
+
if (/Creative Commons CC0 1\.0 Universal/i.test(content)) {
|
|
440
|
+
return "CC0-1.0";
|
|
441
|
+
}
|
|
442
|
+
if (/Boost Software License[\s\S]{0,200}Version 1\.1/i.test(content)) {
|
|
443
|
+
return "BSL-1.1";
|
|
444
|
+
}
|
|
315
445
|
return null;
|
|
316
446
|
}
|
|
317
447
|
|
|
@@ -329,17 +459,26 @@ function applyLicenseGates({ licenses, findings }) {
|
|
|
329
459
|
}
|
|
330
460
|
|
|
331
461
|
for (const item of licenses) {
|
|
462
|
+
const policy = item.policy ?? "unknown";
|
|
463
|
+
const known = item.status === "recognized" && policy !== "unknown";
|
|
332
464
|
findings.push(
|
|
333
465
|
createFinding({
|
|
334
|
-
code:
|
|
335
|
-
severity:
|
|
336
|
-
message:
|
|
466
|
+
code: known ? `license.${policy}` : "license.unknown",
|
|
467
|
+
severity: policy === "allowed" ? "low" : "high",
|
|
468
|
+
message: known
|
|
469
|
+
? policy === "allowed"
|
|
470
|
+
? "License signal is recognized and allowed by public intake policy."
|
|
471
|
+
: policy === "needs-review"
|
|
472
|
+
? "License signal is recognized but requires review by public intake policy."
|
|
473
|
+
: "License signal is recognized but blocked by public intake policy."
|
|
474
|
+
: "Unknown license signal requires manual review.",
|
|
337
475
|
path: item.path,
|
|
338
|
-
blocking:
|
|
476
|
+
blocking: policy !== "allowed",
|
|
339
477
|
details: {
|
|
340
478
|
source: item.source,
|
|
341
479
|
expression: item.expression ?? undefined,
|
|
342
|
-
normalized: item.normalized ?? undefined
|
|
480
|
+
normalized: item.normalized ?? undefined,
|
|
481
|
+
policy
|
|
343
482
|
}
|
|
344
483
|
})
|
|
345
484
|
);
|
|
@@ -884,9 +1023,54 @@ async function readTextPrefix({ filePath, rel, maxBytes, findings, purpose }) {
|
|
|
884
1023
|
return buffer ? buffer.toString("utf8") : "";
|
|
885
1024
|
}
|
|
886
1025
|
|
|
1026
|
+
function truncationFindingForPurpose(purpose) {
|
|
1027
|
+
if (purpose === "secret-scan") {
|
|
1028
|
+
return {
|
|
1029
|
+
code: "secret.truncated",
|
|
1030
|
+
severity: "critical",
|
|
1031
|
+
message: "Secret scan input exceeded inspected prefix."
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (purpose === "dangerous-capability") {
|
|
1035
|
+
return {
|
|
1036
|
+
code: "dangerous.truncated",
|
|
1037
|
+
severity: "high",
|
|
1038
|
+
message: "Dangerous capability input exceeded inspected prefix."
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
if (purpose.startsWith("script-")) {
|
|
1042
|
+
return {
|
|
1043
|
+
code: "script.truncated",
|
|
1044
|
+
severity: "high",
|
|
1045
|
+
message: "Script or workflow input exceeded inspected prefix."
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
return null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
887
1051
|
async function readBufferPrefix({ filePath, rel, maxBytes, findings, purpose }) {
|
|
888
1052
|
let handle;
|
|
889
1053
|
try {
|
|
1054
|
+
const stat = await fs.stat(filePath);
|
|
1055
|
+
const truncationFinding = truncationFindingForPurpose(purpose);
|
|
1056
|
+
// IMPORTANT: high-risk external intake reads must fail closed when bounded prefix inspection is incomplete.
|
|
1057
|
+
if (truncationFinding && stat.size > maxBytes) {
|
|
1058
|
+
findings.push(
|
|
1059
|
+
createFinding({
|
|
1060
|
+
code: truncationFinding.code,
|
|
1061
|
+
severity: truncationFinding.severity,
|
|
1062
|
+
message: truncationFinding.message,
|
|
1063
|
+
path: rel,
|
|
1064
|
+
blocking: true,
|
|
1065
|
+
details: {
|
|
1066
|
+
purpose,
|
|
1067
|
+
bytes: stat.size,
|
|
1068
|
+
maxBytes
|
|
1069
|
+
}
|
|
1070
|
+
})
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
890
1074
|
handle = await fs.open(filePath, "r");
|
|
891
1075
|
const buffer = Buffer.alloc(maxBytes);
|
|
892
1076
|
const result = await handle.read(buffer, 0, maxBytes, 0);
|
package/src/validator.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
2
|
+
import { createReadStream, promises as fs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import Ajv from "ajv/dist/2020.js";
|
|
5
5
|
import addFormats from "ajv-formats";
|
|
@@ -9,8 +9,10 @@ const schemaFiles = [
|
|
|
9
9
|
"context-bundle.schema.json",
|
|
10
10
|
"output-contract.schema.json",
|
|
11
11
|
"package-manifest.schema.json",
|
|
12
|
+
"parser-variant.schema.json",
|
|
12
13
|
"permission-risk.schema.json",
|
|
13
14
|
"public-readback.schema.json",
|
|
15
|
+
"registry-trust.schema.json",
|
|
14
16
|
"resource-manifest.schema.json",
|
|
15
17
|
"skill-metadata.schema.json",
|
|
16
18
|
"surfacing-metadata.schema.json",
|
|
@@ -28,6 +30,10 @@ const blockedExtensions = new Set([
|
|
|
28
30
|
".sh"
|
|
29
31
|
]);
|
|
30
32
|
|
|
33
|
+
// IMPORTANT: keep these gates before package text/JSON reads; they bound memory use for untrusted package inputs.
|
|
34
|
+
const MAX_VALIDATOR_JSON_BYTES = 1024 * 1024;
|
|
35
|
+
const MAX_PACKAGE_FILE_BYTES = 1024 * 1024;
|
|
36
|
+
|
|
31
37
|
const sensitivePathSegments = new Set(["private", ".env", ".git", ".cache", "node_modules"]);
|
|
32
38
|
|
|
33
39
|
const internalDotDirs = ["planning", "sessions"].map((name) => `\\.${name}`).join("|");
|
|
@@ -71,6 +77,30 @@ const secretLikePatterns = [
|
|
|
71
77
|
{ id: "credential-url", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^/\s"'<>:]+:[^@\s"'<>]+@[^\s"'<>]+/i }
|
|
72
78
|
];
|
|
73
79
|
|
|
80
|
+
const safeSecretExamplePatterns = [
|
|
81
|
+
{
|
|
82
|
+
id: "database-url",
|
|
83
|
+
pattern: /^(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis):\/\/(?:<user>|user|root|default):(?:<password>|password)@(?:localhost|127\.0\.0\.1)(?::\d+)?(?:\/[A-Za-z0-9._/-]*)?$/i
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "credential-url",
|
|
87
|
+
pattern: /^https?:\/\/(?:<user>|user):(?:<password>|password)@example\.(?:com|org|net)(?:\/[^\s"'<>]*)?$/i
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const platformManagedCreatorKeys = new Set([
|
|
92
|
+
"approved",
|
|
93
|
+
"listed",
|
|
94
|
+
"latestVersion",
|
|
95
|
+
"platformManaged",
|
|
96
|
+
"platformProjection",
|
|
97
|
+
"platformTrustScore",
|
|
98
|
+
"publishedAt",
|
|
99
|
+
"publicationState",
|
|
100
|
+
"scanPassed",
|
|
101
|
+
"verifiedBadge"
|
|
102
|
+
]);
|
|
103
|
+
|
|
74
104
|
export async function validatePackage(options) {
|
|
75
105
|
const command = options.command ?? "validate";
|
|
76
106
|
const packageDir = path.resolve(options.packageDir);
|
|
@@ -119,11 +149,12 @@ export async function validatePackage(options) {
|
|
|
119
149
|
const filePath = resolveInside(packageDir, rel);
|
|
120
150
|
if (!filePath) continue;
|
|
121
151
|
try {
|
|
122
|
-
const
|
|
152
|
+
const stat = await fs.stat(filePath);
|
|
153
|
+
const digest = await hashFileSha256(filePath);
|
|
123
154
|
inventory.push({
|
|
124
155
|
path: rel,
|
|
125
|
-
sha256:
|
|
126
|
-
bytes:
|
|
156
|
+
sha256: digest,
|
|
157
|
+
bytes: stat.size
|
|
127
158
|
});
|
|
128
159
|
} catch {
|
|
129
160
|
// Missing files are already reported by inspectPackageFile.
|
|
@@ -134,12 +165,7 @@ export async function validatePackage(options) {
|
|
|
134
165
|
ok: findings.length === 0,
|
|
135
166
|
command,
|
|
136
167
|
packageDir,
|
|
137
|
-
manifest: manifest
|
|
138
|
-
? {
|
|
139
|
-
name: typeof manifest.name === "string" ? manifest.name : null,
|
|
140
|
-
formatVersion: typeof manifest.formatVersion === "string" ? manifest.formatVersion : null
|
|
141
|
-
}
|
|
142
|
-
: null,
|
|
168
|
+
manifest: summarizeManifest(manifest),
|
|
143
169
|
inventory,
|
|
144
170
|
findings
|
|
145
171
|
});
|
|
@@ -178,8 +204,19 @@ async function readRequiredJson(filePath) {
|
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
async function readJsonFile(filePath, findings, location) {
|
|
207
|
+
const raw = await readTextFileWithinLimit({
|
|
208
|
+
filePath,
|
|
209
|
+
findings,
|
|
210
|
+
location,
|
|
211
|
+
maxBytes: MAX_VALIDATOR_JSON_BYTES,
|
|
212
|
+
oversizedCode: "json-too-large",
|
|
213
|
+
readErrorCode: "json-read"
|
|
214
|
+
});
|
|
215
|
+
if (raw === null) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
try {
|
|
182
|
-
const raw = await fs.readFile(filePath, "utf8");
|
|
183
220
|
return JSON.parse(raw);
|
|
184
221
|
} catch (error) {
|
|
185
222
|
findings.push(finding("json-read", `Unable to read valid JSON: ${error.code ?? "parse_error"}`, location));
|
|
@@ -212,24 +249,69 @@ async function inspectPackageFile({ packageDir, rel, expectedHash, findings, ajv
|
|
|
212
249
|
return;
|
|
213
250
|
}
|
|
214
251
|
|
|
215
|
-
let
|
|
252
|
+
let stat;
|
|
216
253
|
try {
|
|
217
|
-
|
|
254
|
+
stat = await fs.stat(filePath);
|
|
218
255
|
} catch {
|
|
219
256
|
findings.push(finding("missing-file", "Package file listed in manifest is missing.", rel));
|
|
220
257
|
return;
|
|
221
258
|
}
|
|
222
259
|
|
|
223
|
-
|
|
260
|
+
if (stat.size > MAX_PACKAGE_FILE_BYTES) {
|
|
261
|
+
findings.push(finding("file-too-large", `Package file exceeds ${MAX_PACKAGE_FILE_BYTES} byte limit.`, rel));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let actualHash;
|
|
266
|
+
try {
|
|
267
|
+
actualHash = `sha256:${await hashFileSha256(filePath)}`;
|
|
268
|
+
} catch {
|
|
269
|
+
findings.push(finding("hash-read", "Unable to hash package file.", rel));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
224
273
|
if (expectedHash && expectedHash !== actualHash) {
|
|
225
274
|
findings.push(finding("hash-mismatch", "Package file hash does not match manifest.", rel));
|
|
226
275
|
}
|
|
227
276
|
|
|
228
|
-
const text =
|
|
277
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
229
278
|
scanText(text, rel, findings);
|
|
230
279
|
inspectStructuredPackageFile({ rel, text, findings, ajv });
|
|
231
280
|
}
|
|
232
281
|
|
|
282
|
+
async function readTextFileWithinLimit({ filePath, findings, location, maxBytes, oversizedCode, readErrorCode }) {
|
|
283
|
+
let stat;
|
|
284
|
+
try {
|
|
285
|
+
stat = await fs.stat(filePath);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
findings.push(finding(readErrorCode, `Unable to read valid JSON: ${error.code ?? "read_error"}`, location));
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (stat.size > maxBytes) {
|
|
292
|
+
findings.push(finding(oversizedCode, `File exceeds ${maxBytes} byte limit.`, location));
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
return await fs.readFile(filePath, "utf8");
|
|
298
|
+
} catch (error) {
|
|
299
|
+
findings.push(finding(readErrorCode, `Unable to read valid JSON: ${error.code ?? "read_error"}`, location));
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function hashFileSha256(filePath) {
|
|
305
|
+
const hash = createHash("sha256");
|
|
306
|
+
await new Promise((resolve, reject) => {
|
|
307
|
+
const stream = createReadStream(filePath);
|
|
308
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
309
|
+
stream.on("error", reject);
|
|
310
|
+
stream.on("end", resolve);
|
|
311
|
+
});
|
|
312
|
+
return hash.digest("hex");
|
|
313
|
+
}
|
|
314
|
+
|
|
233
315
|
function validateManifestContracts(manifest, findings) {
|
|
234
316
|
if (!isRecord(manifest)) return;
|
|
235
317
|
|
|
@@ -262,6 +344,272 @@ function validateManifestContracts(manifest, findings) {
|
|
|
262
344
|
)
|
|
263
345
|
);
|
|
264
346
|
}
|
|
347
|
+
|
|
348
|
+
validateRegistryTrustContracts(manifest.registryTrust, findings);
|
|
349
|
+
validateParserVariantContracts(manifest.parserVariant, findings);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function validateRegistryTrustContracts(registryTrust, findings) {
|
|
353
|
+
if (!isRecord(registryTrust)) return;
|
|
354
|
+
|
|
355
|
+
collectPlatformManagedCreatorKeys(registryTrust, "manifest.registryTrust", findings);
|
|
356
|
+
|
|
357
|
+
const packageContext = registryTrust.packageContext;
|
|
358
|
+
if (isRecord(packageContext)) {
|
|
359
|
+
const version = typeof packageContext.version === "string" ? packageContext.version : "";
|
|
360
|
+
const sourceUrl = typeof packageContext.sourceUrl === "string" ? packageContext.sourceUrl : "";
|
|
361
|
+
|
|
362
|
+
if (/[\s<>=*xX]/.test(version) || version.startsWith("^") || version.startsWith("~") || !sourceUrl.startsWith("https://")) {
|
|
363
|
+
findings.push(
|
|
364
|
+
finding(
|
|
365
|
+
"package-context-unsafe",
|
|
366
|
+
"Package context must use an exact public version and HTTPS source URL.",
|
|
367
|
+
"manifest.registryTrust.packageContext"
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeof packageContext.packageDigest !== "string") {
|
|
373
|
+
findings.push(
|
|
374
|
+
finding(
|
|
375
|
+
"desired-state-fingerprint-missing",
|
|
376
|
+
"Registry trust package context must include a public package digest for desired-state comparison.",
|
|
377
|
+
"manifest.registryTrust.packageContext.packageDigest"
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (typeof packageContext.ownershipEvidenceVersion !== "string") {
|
|
383
|
+
findings.push(
|
|
384
|
+
finding(
|
|
385
|
+
"scanner-policy-missing",
|
|
386
|
+
"Registry trust package context must include an ownership evidence version for policy freshness comparison.",
|
|
387
|
+
"manifest.registryTrust.packageContext.ownershipEvidenceVersion"
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (isRecord(registryTrust.generatedDraft) && registryTrust.generatedDraft.draftOnly !== true) {
|
|
394
|
+
findings.push(
|
|
395
|
+
finding(
|
|
396
|
+
"generated-draft-boundary",
|
|
397
|
+
"Generated draft metadata must remain draft-only.",
|
|
398
|
+
"manifest.registryTrust.generatedDraft"
|
|
399
|
+
)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (isRecord(registryTrust.patchDelta)) {
|
|
404
|
+
const operations = Array.isArray(registryTrust.patchDelta.operations) ? registryTrust.patchDelta.operations : [];
|
|
405
|
+
const ambiguousOperation = operations.find(
|
|
406
|
+
(operation) =>
|
|
407
|
+
!isRecord(operation) ||
|
|
408
|
+
operation.path === "/" ||
|
|
409
|
+
(["add", "replace"].includes(operation.op) && typeof operation.valueSummary !== "string")
|
|
410
|
+
);
|
|
411
|
+
if (ambiguousOperation || operations.length === 0) {
|
|
412
|
+
findings.push(
|
|
413
|
+
finding(
|
|
414
|
+
"patch-delta-ambiguous",
|
|
415
|
+
"Patch and delta metadata must describe explicit partial-update operations.",
|
|
416
|
+
"manifest.registryTrust.patchDelta"
|
|
417
|
+
)
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function validateParserVariantContracts(parserVariant, findings) {
|
|
424
|
+
if (!isRecord(parserVariant)) return;
|
|
425
|
+
|
|
426
|
+
const parserEvidence = parserVariant.parserEvidence;
|
|
427
|
+
if (isRecord(parserEvidence)) {
|
|
428
|
+
const parseStatus = typeof parserEvidence.parseStatus === "string" ? parserEvidence.parseStatus : "";
|
|
429
|
+
const parseConfidence = typeof parserEvidence.parseConfidence === "string" ? parserEvidence.parseConfidence : "";
|
|
430
|
+
|
|
431
|
+
if (parserEvidence.noExecution !== true) {
|
|
432
|
+
findings.push(
|
|
433
|
+
finding(
|
|
434
|
+
"parser-evidence-review-required",
|
|
435
|
+
"Parser evidence must include a no-execution proof before public use.",
|
|
436
|
+
"manifest.parserVariant.parserEvidence"
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
["partial", "unsupported", "blocked", "failed"].includes(parseStatus) ||
|
|
443
|
+
["low", "unknown"].includes(parseConfidence)
|
|
444
|
+
) {
|
|
445
|
+
findings.push(
|
|
446
|
+
finding(
|
|
447
|
+
"parser-evidence-review-required",
|
|
448
|
+
"Parser evidence requires review before parser or variant availability claims.",
|
|
449
|
+
"manifest.parserVariant.parserEvidence"
|
|
450
|
+
)
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const platformVariants = Array.isArray(parserVariant.platformVariants) ? parserVariant.platformVariants : [];
|
|
456
|
+
platformVariants.forEach((variant, index) => {
|
|
457
|
+
if (!isRecord(variant)) return;
|
|
458
|
+
const location = `manifest.parserVariant.platformVariants[${index}]`;
|
|
459
|
+
const download = isRecord(variant.download) ? variant.download : {};
|
|
460
|
+
|
|
461
|
+
if (variant.managedBy === "platform" || download.availability === "available") {
|
|
462
|
+
findings.push(
|
|
463
|
+
finding(
|
|
464
|
+
"platform-variant-overclaim",
|
|
465
|
+
"Creator manifests cannot claim platform-managed variant state or platform download availability.",
|
|
466
|
+
location
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (variant.state === "unsupported") {
|
|
472
|
+
findings.push(
|
|
473
|
+
finding(
|
|
474
|
+
"variant-unsupported",
|
|
475
|
+
"Variant metadata declares an unsupported platform target.",
|
|
476
|
+
location
|
|
477
|
+
)
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (variant.state === "stale" || variant.validationState === "stale") {
|
|
482
|
+
findings.push(
|
|
483
|
+
finding(
|
|
484
|
+
"variant-stale",
|
|
485
|
+
"Variant metadata declares stale parser or platform evidence.",
|
|
486
|
+
location
|
|
487
|
+
)
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function collectPlatformManagedCreatorKeys(value, location, findings) {
|
|
494
|
+
if (Array.isArray(value)) {
|
|
495
|
+
value.forEach((item, index) => collectPlatformManagedCreatorKeys(item, `${location}[${index}]`, findings));
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!isRecord(value)) return;
|
|
499
|
+
|
|
500
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
501
|
+
if (platformManagedCreatorKeys.has(key)) {
|
|
502
|
+
findings.push(
|
|
503
|
+
finding(
|
|
504
|
+
"platform-managed-overclaim",
|
|
505
|
+
"Creator manifests cannot set platform-managed trust, scan, publication, or badge state.",
|
|
506
|
+
location
|
|
507
|
+
)
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
collectPlatformManagedCreatorKeys(nested, `${location}.${key}`, findings);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function summarizeManifest(manifest) {
|
|
515
|
+
if (!isRecord(manifest)) return null;
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
name: typeof manifest.name === "string" ? manifest.name : null,
|
|
519
|
+
formatVersion: typeof manifest.formatVersion === "string" ? manifest.formatVersion : null,
|
|
520
|
+
...(isRecord(manifest.parserVariant) ? { parserVariant: summarizeParserVariant(manifest.parserVariant) } : {}),
|
|
521
|
+
...(isRecord(manifest.registryTrust) ? { registryTrust: summarizeRegistryTrust(manifest.registryTrust) } : {})
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function summarizeParserVariant(parserVariant) {
|
|
526
|
+
const parserEvidence = isRecord(parserVariant.parserEvidence) ? parserVariant.parserEvidence : null;
|
|
527
|
+
const resourceGraphSummary = isRecord(parserVariant.resourceGraphSummary) ? parserVariant.resourceGraphSummary : null;
|
|
528
|
+
const compatibility = isRecord(parserVariant.compatibility) ? parserVariant.compatibility : null;
|
|
529
|
+
const platformVariants = Array.isArray(parserVariant.platformVariants) ? parserVariant.platformVariants : [];
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
parserEvidence: parserEvidence
|
|
533
|
+
? {
|
|
534
|
+
sourceEcosystem: typeof parserEvidence.sourceEcosystem === "string" ? parserEvidence.sourceEcosystem : null,
|
|
535
|
+
sourceFormat: typeof parserEvidence.sourceFormat === "string" ? parserEvidence.sourceFormat : null,
|
|
536
|
+
parseStatus: typeof parserEvidence.parseStatus === "string" ? parserEvidence.parseStatus : null,
|
|
537
|
+
parseConfidence: typeof parserEvidence.parseConfidence === "string" ? parserEvidence.parseConfidence : null,
|
|
538
|
+
sanitizerStatus: typeof parserEvidence.sanitizerStatus === "string" ? parserEvidence.sanitizerStatus : null,
|
|
539
|
+
noExecution: parserEvidence.noExecution === true,
|
|
540
|
+
outputDigestPresent: typeof parserEvidence.outputDigest === "string"
|
|
541
|
+
}
|
|
542
|
+
: null,
|
|
543
|
+
resourceGraphSummary: resourceGraphSummary
|
|
544
|
+
? {
|
|
545
|
+
sanitized: resourceGraphSummary.sanitized === true,
|
|
546
|
+
nodeCount: numberOrNull(resourceGraphSummary.nodeCount),
|
|
547
|
+
edgeCount: numberOrNull(resourceGraphSummary.edgeCount),
|
|
548
|
+
capabilityCount: numberOrNull(resourceGraphSummary.capabilityCount),
|
|
549
|
+
sourceFileCount: numberOrNull(resourceGraphSummary.sourceFileCount)
|
|
550
|
+
}
|
|
551
|
+
: null,
|
|
552
|
+
compatibility: compatibility
|
|
553
|
+
? {
|
|
554
|
+
status: typeof compatibility.status === "string" ? compatibility.status : null,
|
|
555
|
+
reasons: stringArrayOrEmpty(compatibility.reasons),
|
|
556
|
+
reasonCount: Array.isArray(compatibility.reasons) ? compatibility.reasons.length : 0
|
|
557
|
+
}
|
|
558
|
+
: null,
|
|
559
|
+
platformVariants: platformVariants.filter(isRecord).map((variant) => {
|
|
560
|
+
const download = isRecord(variant.download) ? variant.download : {};
|
|
561
|
+
return {
|
|
562
|
+
platformId: typeof variant.platformId === "string" ? variant.platformId : null,
|
|
563
|
+
artifactKind: typeof variant.artifactKind === "string" ? variant.artifactKind : null,
|
|
564
|
+
state: typeof variant.state === "string" ? variant.state : null,
|
|
565
|
+
validationState: typeof variant.validationState === "string" ? variant.validationState : null,
|
|
566
|
+
downloadAvailability: typeof download.availability === "string" ? download.availability : null,
|
|
567
|
+
reasons: stringArrayOrEmpty(variant.reasons),
|
|
568
|
+
reasonCount: Array.isArray(variant.reasons) ? variant.reasons.length : 0
|
|
569
|
+
};
|
|
570
|
+
})
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function summarizeRegistryTrust(registryTrust) {
|
|
575
|
+
const packageContext = isRecord(registryTrust.packageContext) ? registryTrust.packageContext : null;
|
|
576
|
+
const generatedDraft = isRecord(registryTrust.generatedDraft) ? registryTrust.generatedDraft : null;
|
|
577
|
+
const patchDelta = isRecord(registryTrust.patchDelta) ? registryTrust.patchDelta : null;
|
|
578
|
+
const creatorCheckpoints = Array.isArray(registryTrust.creatorCheckpoints) ? registryTrust.creatorCheckpoints : [];
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
packageContext: packageContext
|
|
582
|
+
? {
|
|
583
|
+
packageName: typeof packageContext.packageName === "string" ? packageContext.packageName : null,
|
|
584
|
+
version: typeof packageContext.version === "string" ? packageContext.version : null,
|
|
585
|
+
sourceUrl: typeof packageContext.sourceUrl === "string" ? packageContext.sourceUrl : null,
|
|
586
|
+
packageDigestPresent: typeof packageContext.packageDigest === "string"
|
|
587
|
+
}
|
|
588
|
+
: null,
|
|
589
|
+
desiredStateFingerprintPresent: packageContext ? typeof packageContext.packageDigest === "string" : false,
|
|
590
|
+
scannerPolicyVersionExpectation: "platform-managed-readback",
|
|
591
|
+
creatorCheckpointCount: creatorCheckpoints.length,
|
|
592
|
+
generatedDraft: generatedDraft
|
|
593
|
+
? {
|
|
594
|
+
draftOnly: generatedDraft.draftOnly === true,
|
|
595
|
+
kind: typeof generatedDraft.kind === "string" ? generatedDraft.kind : null
|
|
596
|
+
}
|
|
597
|
+
: null,
|
|
598
|
+
patchDelta: patchDelta
|
|
599
|
+
? {
|
|
600
|
+
mode: typeof patchDelta.mode === "string" ? patchDelta.mode : null,
|
|
601
|
+
operationCount: Array.isArray(patchDelta.operations) ? patchDelta.operations.length : 0
|
|
602
|
+
}
|
|
603
|
+
: null
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function numberOrNull(value) {
|
|
608
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function stringArrayOrEmpty(value) {
|
|
612
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
265
613
|
}
|
|
266
614
|
|
|
267
615
|
function inspectStructuredPackageFile({ rel, text, findings, ajv }) {
|
|
@@ -301,12 +649,18 @@ function schemaIdForPackageJson(normalizedRel) {
|
|
|
301
649
|
if (normalizedRel.startsWith("tools/")) {
|
|
302
650
|
return "https://schemas.agentique.io/tool-listing.schema.json";
|
|
303
651
|
}
|
|
304
|
-
if (normalizedRel
|
|
652
|
+
if (isContextBundlePackageJson(normalizedRel)) {
|
|
305
653
|
return "https://schemas.agentique.io/context-bundle.schema.json";
|
|
306
654
|
}
|
|
307
655
|
return null;
|
|
308
656
|
}
|
|
309
657
|
|
|
658
|
+
function isContextBundlePackageJson(normalizedRel) {
|
|
659
|
+
const parts = normalizedRel.split("/");
|
|
660
|
+
const basename = parts.at(-1) ?? "";
|
|
661
|
+
return normalizedRel.startsWith("bundle/") || (parts.length === 1 && basename.startsWith("context-bundle"));
|
|
662
|
+
}
|
|
663
|
+
|
|
310
664
|
function resolveInside(root, rel) {
|
|
311
665
|
const resolved = path.resolve(root, rel);
|
|
312
666
|
const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
@@ -337,12 +691,25 @@ function scanText(text, location, findings) {
|
|
|
337
691
|
}
|
|
338
692
|
}
|
|
339
693
|
for (const rule of secretLikePatterns) {
|
|
340
|
-
|
|
694
|
+
const unsafeMatch = [...matchPattern(text, rule.pattern)].find((match) => !isSafeSecretExample(rule.id, match[0]));
|
|
695
|
+
if (unsafeMatch) {
|
|
341
696
|
findings.push(finding(rule.id, "Secret-like value detected and redacted.", location));
|
|
342
697
|
}
|
|
343
698
|
}
|
|
344
699
|
}
|
|
345
700
|
|
|
701
|
+
function* matchPattern(text, pattern) {
|
|
702
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
703
|
+
const globalPattern = new RegExp(pattern.source, flags);
|
|
704
|
+
yield* text.matchAll(globalPattern);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function isSafeSecretExample(ruleId, matchText) {
|
|
708
|
+
return safeSecretExamplePatterns.some(
|
|
709
|
+
(safe) => (safe.id === ruleId || (ruleId === "credential-url" && safe.id === "database-url")) && safe.pattern.test(matchText)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
346
713
|
function createReport({ ok, command, packageDir, manifest, inventory, findings }) {
|
|
347
714
|
return {
|
|
348
715
|
ok,
|