@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 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. It is not publication approval, safety assurance, moderation status, or legal review.
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
- Local monorepo implementation exists for review. Package publishing remains blocked until release review passes.
58
+ Published as `@agentique.io/validator`. Local validation output is not platform approval and is not safety certification.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentique.io/validator",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Static local upload-preparation validator for Agentique resource packages.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/src/cli.mjs CHANGED
File without changes
@@ -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|\.env)\b/i
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 normalized = expression.trim().replace(/[()]/g, "").toUpperCase();
279
- const map = new Map([
280
- ["MIT", "MIT"],
281
- ["APACHE-2.0", "Apache-2.0"],
282
- ["GPL-2.0", "GPL-2.0"],
283
- ["GPL-2.0-ONLY", "GPL-2.0"],
284
- ["GPL-3.0", "GPL-3.0"],
285
- ["GPL-3.0-ONLY", "GPL-3.0"],
286
- ["BSD-3-CLAUSE", "BSD-3-Clause"],
287
- ["ISC", "ISC"],
288
- ["MPL-2.0", "MPL-2.0"]
289
- ]);
290
- return map.get(normalized) ?? null;
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: item.status === "recognized" ? "license.detected" : "license.unknown",
335
- severity: item.status === "recognized" ? "low" : "high",
336
- message: item.status === "recognized" ? "License signal was detected." : "Unknown license signal requires manual review.",
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: item.status !== "recognized",
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 bytes = await fs.readFile(filePath);
152
+ const stat = await fs.stat(filePath);
153
+ const digest = await hashFileSha256(filePath);
123
154
  inventory.push({
124
155
  path: rel,
125
- sha256: createHash("sha256").update(bytes).digest("hex"),
126
- bytes: bytes.length
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 bytes;
252
+ let stat;
216
253
  try {
217
- bytes = await fs.readFile(filePath);
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
- const actualHash = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
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 = bytes.toString("utf8");
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.includes("context-bundle")) {
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
- if (rule.pattern.test(text)) {
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,