agentcert 0.2.5 → 0.2.7

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
@@ -49,6 +49,11 @@ files, 10 MiB per file, and 50 MiB total. Skipped references are reported in
49
49
  the CLI and hosted run timeline. Companion uploads are restricted to
50
50
  PNG/JPEG/WebP, JSON/JSONL, HTML, PDF, and ZIP; other extensions are skipped
51
51
  before they are read. Pass `--no-artifacts` to upload only the JSON bundle.
52
+ Hosted pushes embed an `agentcert.artifact_manifest.v0.1` declaration with the
53
+ normalized path, SHA-256 digest, byte size, and kind of every prepared
54
+ companion artifact. The control plane reports `complete` only after every
55
+ hosted object exactly matches that declaration; missing, skipped, undeclared,
56
+ or mismatched artifacts remain observable as `partial` or `rejected`.
52
57
  Project API keys can create runs, record events, and upload evidence, but
53
58
  cannot approve their own runtime actions.
54
59
 
@@ -0,0 +1,37 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AGENTCERT_ARTIFACT_MANIFEST_VERSION, } from "./types.js";
3
+ export function buildArtifactManifest(artifacts) {
4
+ const entries = artifacts.map((artifact) => ({
5
+ path: normalizeManifestPath(artifact.sourcePath),
6
+ sha256: createHash("sha256").update(artifact.bytes).digest("hex"),
7
+ sizeBytes: artifact.bytes.byteLength,
8
+ kind: artifact.kind,
9
+ })).sort((left, right) => left.path.localeCompare(right.path));
10
+ const paths = new Set();
11
+ for (const entry of entries) {
12
+ if (paths.has(entry.path))
13
+ throw new Error(`Artifact manifest contains duplicate path ${entry.path}.`);
14
+ paths.add(entry.path);
15
+ }
16
+ return { schemaVersion: AGENTCERT_ARTIFACT_MANIFEST_VERSION, entries };
17
+ }
18
+ export function withArtifactManifest(bundle, artifacts) {
19
+ return { ...bundle, artifactManifest: buildArtifactManifest(artifacts) };
20
+ }
21
+ export function serializeHostedEvidenceBundle(bundle, artifacts) {
22
+ const reconciledBundle = withArtifactManifest(bundle, artifacts);
23
+ return {
24
+ bundle: reconciledBundle,
25
+ bytes: new TextEncoder().encode(`${JSON.stringify(reconciledBundle, null, 2)}\n`),
26
+ };
27
+ }
28
+ export function normalizeManifestPath(value) {
29
+ const path = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
30
+ if (!path || path.length > 1024 || path.startsWith("/") || /^[A-Za-z]:\//.test(path)) {
31
+ throw new Error(`Artifact manifest path must be a relative path: ${value}`);
32
+ }
33
+ if (path.split("/").some((segment) => segment === ".." || segment === "")) {
34
+ throw new Error(`Artifact manifest path cannot contain empty or parent segments: ${value}`);
35
+ }
36
+ return path;
37
+ }
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { collectCompanionArtifacts, MAX_REPORTED_COMPANION_ARTIFACT_SKIPS } from
9
9
  import { recordsFromAgentCertResult, evaluateFailureClassifier, renderCorpusSummary, summarizeCorpus, writeReviewedFailureDataset, } from "./corpus.js";
10
10
  import { openCorpusStore, parseCorpusStoreKind } from "./corpus-store.js";
11
11
  import { pushEvidenceToControlPlane, verifyControlPlaneConnection } from "./control-plane.js";
12
+ import { runEvidenceConformance } from "./conformance.js";
12
13
  import { DEFAULT_AGENTCERT_SERVER, resolveConnection, saveConnection } from "./credentials.js";
13
14
  import { applyFailureReviews, appendFailureReview, createFailureReview, findFailurePattern, parseFailureReviewStatus, parseFailureType, parseReviewConfidence, readFailureReviews, } from "./failure-review.js";
14
15
  import { buildMonitorSnapshot, writeMonitorSnapshot } from "./monitor.js";
@@ -515,6 +516,32 @@ else if (command === "validate") {
515
516
  }
516
517
  }
517
518
  }
519
+ else if (command === "conformance") {
520
+ const file = readFlag("--file") ?? readFirstPositionalAfterCommand();
521
+ if (!file)
522
+ throw new Error("Missing evidence file. Usage: agentcert conformance <file> --artifact-root <directory>.");
523
+ const artifactRoot = resolve(readFlag("--artifact-root") ?? dirname(resolve(file)));
524
+ const report = await runEvidenceConformance(await readJson(file), {
525
+ evidenceFile: file,
526
+ artifactRoot,
527
+ implementation: readFlag("--implementation"),
528
+ });
529
+ const output = `${JSON.stringify(report, null, 2)}\n`;
530
+ const outPath = readFlag("--out");
531
+ if (outPath) {
532
+ await mkdir(dirname(resolve(outPath)), { recursive: true });
533
+ await writeFile(outPath, output);
534
+ process.stdout.write(`Wrote conformance report: ${resolve(outPath)}\n`);
535
+ }
536
+ process.stdout.write(`${report.valid ? "Conformant" : "Non-conformant"} evidence implementation: ${report.implementation}\n`);
537
+ for (const item of report.checks) {
538
+ process.stdout.write(`- ${item.status === "passed" ? "PASS" : "FAIL"} ${item.id}: ${item.message}\n`);
539
+ for (const error of item.errors)
540
+ process.stdout.write(` - ${error}\n`);
541
+ }
542
+ if (!report.valid)
543
+ process.exitCode = 1;
544
+ }
518
545
  else {
519
546
  process.stdout.write(`Usage:
520
547
  agentcert init --subject my-browser-agent
@@ -543,6 +570,7 @@ else {
543
570
  agentcert serve --corpus .agentcert/corpus/corpus.jsonl --static public-demo/agentcert-monitor --artifact-root public-demo/browser-agent-robustness/evidence/tripwire-public-demo
544
571
  agentcert validate .agentcert/latest/agentcert-evidence.json
545
572
  agentcert validate .agentcert/latest/agentcert-evidence.json --check-artifacts
573
+ agentcert conformance .agentcert/latest/agentcert-evidence.json --artifact-root . --implementation my-agent
546
574
  agentcert schema validate --schema evidence-bundle --file .agentcert/latest/agentcert-evidence.json
547
575
  `);
548
576
  }
@@ -911,12 +939,13 @@ jobs:
911
939
  # permissions:
912
940
  # contents: write
913
941
  steps:
914
- - uses: actions/checkout@v4
915
- - uses: actions/setup-node@v4
942
+ - uses: actions/checkout@v7
943
+ - uses: actions/setup-node@v6
916
944
  with:
917
945
  node-version: "20"
918
946
 
919
- - uses: Kakarottoooo/agentcert/actions/tripwire@v0
947
+ - id: agentcert
948
+ uses: Kakarottoooo/agentcert/actions/tripwire@v0
920
949
  with:
921
950
  config: tripwire.yml
922
951
  out: .tripwire/latest
@@ -1,4 +1,10 @@
1
1
  export function renderCommandHelp(command) {
2
+ if (command === "conformance")
3
+ return `Usage:
4
+ agentcert conformance <evidence.json> --artifact-root <directory> [--implementation <name>] [--out <report.json>]
5
+
6
+ Checks schema identity, v0.1 compatibility, artifact manifest structure, and exact artifact hashes and sizes.
7
+ `;
2
8
  if (command !== "push")
3
9
  return undefined;
4
10
  return `Usage:
@@ -0,0 +1,115 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { normalizeManifestPath } from "./artifact-manifest.js";
5
+ import { validateAgentCertSchema } from "./schema-validator.js";
6
+ const SUPPORTED_TOP_LEVEL_FIELDS = new Set([
7
+ "schemaName", "schemaVersion", "schemaSemver", "kind", "runId", "generatedAt", "subject", "verdict", "summary",
8
+ "results", "evidence", "artifacts", "artifactManifest", "standards",
9
+ ]);
10
+ export async function runEvidenceConformance(input, options) {
11
+ const checks = [];
12
+ const schema = validateAgentCertSchema("evidence-bundle", input);
13
+ checks.push(check("schema", "Evidence bundle satisfies the AgentCert v0.1 semantic contract.", schema.errors));
14
+ const bundle = object(input);
15
+ const compatibilityErrors = bundle
16
+ ? Object.keys(bundle).filter((key) => !SUPPORTED_TOP_LEVEL_FIELDS.has(key)).map((key) => `Unsupported top-level field: ${key}.`)
17
+ : ["Evidence bundle must be an object."];
18
+ checks.push(check("compatibility", "Evidence bundle uses only v0.1-compatible top-level fields.", compatibilityErrors));
19
+ const manifestErrors = [];
20
+ const entries = parseManifest(bundle?.artifactManifest, manifestErrors);
21
+ checks.push(check("manifest", "Artifact manifest declares unique normalized paths, SHA-256, byte size, and kind.", manifestErrors));
22
+ const byteErrors = [];
23
+ if (manifestErrors.length === 0) {
24
+ const root = resolve(options.artifactRoot);
25
+ for (const entry of entries) {
26
+ const path = resolve(root, entry.path);
27
+ if (path !== root && !path.startsWith(`${root}\\`) && !path.startsWith(`${root}/`)) {
28
+ byteErrors.push(`${entry.path}: resolved path escapes artifact root.`);
29
+ continue;
30
+ }
31
+ try {
32
+ const bytes = await readFile(path);
33
+ const digest = createHash("sha256").update(bytes).digest("hex");
34
+ if (bytes.byteLength !== entry.sizeBytes)
35
+ byteErrors.push(`${entry.path}: expected ${entry.sizeBytes} bytes, observed ${bytes.byteLength}.`);
36
+ if (digest !== entry.sha256)
37
+ byteErrors.push(`${entry.path}: SHA-256 does not match the declared digest.`);
38
+ }
39
+ catch (error) {
40
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "read_failed";
41
+ byteErrors.push(`${entry.path}: artifact could not be read (${code}).`);
42
+ }
43
+ }
44
+ }
45
+ else {
46
+ byteErrors.push("Artifact bytes were not checked because the manifest is invalid.");
47
+ }
48
+ checks.push(check("artifact-bytes", "Every declared artifact matches its exact runtime bytes.", byteErrors));
49
+ const passed = checks.filter((item) => item.status === "passed").length;
50
+ const failed = checks.length - passed;
51
+ return {
52
+ schemaVersion: "agentcert.conformance.v0.1",
53
+ kind: "agentcert.evidence_conformance_report",
54
+ implementation: options.implementation?.trim() || "third-party",
55
+ evidenceFile: resolve(options.evidenceFile),
56
+ artifactRoot: resolve(options.artifactRoot),
57
+ generatedAt: (options.now ?? new Date()).toISOString(),
58
+ valid: failed === 0,
59
+ summary: { passed, failed },
60
+ checks,
61
+ };
62
+ }
63
+ function parseManifest(input, errors) {
64
+ const manifest = object(input);
65
+ if (!manifest) {
66
+ errors.push("artifactManifest is required for conformance.");
67
+ return [];
68
+ }
69
+ if (manifest.schemaVersion !== "agentcert.artifact_manifest.v0.1")
70
+ errors.push("artifactManifest.schemaVersion must be agentcert.artifact_manifest.v0.1.");
71
+ if (!Array.isArray(manifest.entries)) {
72
+ errors.push("artifactManifest.entries must be an array.");
73
+ return [];
74
+ }
75
+ if (manifest.entries.length > 500)
76
+ errors.push("artifactManifest.entries cannot exceed 500 entries.");
77
+ const paths = new Set();
78
+ const entries = [];
79
+ manifest.entries.forEach((inputEntry, index) => {
80
+ const entry = object(inputEntry);
81
+ if (!entry) {
82
+ errors.push(`artifactManifest.entries[${index}] must be an object.`);
83
+ return;
84
+ }
85
+ let path;
86
+ try {
87
+ path = typeof entry.path === "string" ? normalizeManifestPath(entry.path) : undefined;
88
+ }
89
+ catch (error) {
90
+ errors.push(`artifactManifest.entries[${index}].path: ${error.message}`);
91
+ }
92
+ if (!path)
93
+ errors.push(`artifactManifest.entries[${index}].path must be a normalized relative path.`);
94
+ else if (paths.has(path))
95
+ errors.push(`artifactManifest contains duplicate path ${path}.`);
96
+ else
97
+ paths.add(path);
98
+ if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
99
+ errors.push(`artifactManifest.entries[${index}].sha256 must be 64 lowercase hex characters.`);
100
+ if (!Number.isInteger(entry.sizeBytes) || entry.sizeBytes < 0)
101
+ errors.push(`artifactManifest.entries[${index}].sizeBytes must be a non-negative integer.`);
102
+ if (typeof entry.kind !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(entry.kind))
103
+ errors.push(`artifactManifest.entries[${index}].kind must be a stable lowercase identifier.`);
104
+ if (path && typeof entry.sha256 === "string" && Number.isInteger(entry.sizeBytes) && typeof entry.kind === "string") {
105
+ entries.push({ path, sha256: entry.sha256, sizeBytes: entry.sizeBytes, kind: entry.kind });
106
+ }
107
+ });
108
+ return entries;
109
+ }
110
+ function check(id, message, errors) {
111
+ return { id, status: errors.length === 0 ? "passed" : "failed", message, errors };
112
+ }
113
+ function object(input) {
114
+ return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
115
+ }
@@ -1,3 +1,4 @@
1
+ import { serializeHostedEvidenceBundle } from "./artifact-manifest.js";
1
2
  import { MAX_REPORTED_COMPANION_ARTIFACT_SKIPS, } from "./companion-artifacts.js";
2
3
  export class ControlPlaneRequestError extends Error {
3
4
  status;
@@ -40,8 +41,16 @@ export async function pushEvidenceToControlPlane(options) {
40
41
  }
41
42
  const request = options.fetch ?? fetch;
42
43
  const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}`;
43
- const externalId = options.externalId ?? options.bundle.runId;
44
- const kind = hostedRunKind(options.bundle);
44
+ const companionArtifacts = options.companionArtifacts ?? [];
45
+ const hostedEvidence = options.companionArtifacts === undefined && options.bundle.artifactManifest
46
+ ? {
47
+ bundle: options.bundle,
48
+ bytes: new TextEncoder().encode(`${JSON.stringify(options.bundle, null, 2)}\n`),
49
+ }
50
+ : serializeHostedEvidenceBundle(options.bundle, companionArtifacts);
51
+ const bundle = hostedEvidence.bundle;
52
+ const externalId = options.externalId ?? bundle.runId;
53
+ const kind = hostedRunKind(bundle);
45
54
  const headers = { authorization: `Bearer ${options.apiKey}` };
46
55
  const run = await requestJson(request, `${projectUrl}/runs`, {
47
56
  method: "POST",
@@ -49,11 +58,11 @@ export async function pushEvidenceToControlPlane(options) {
49
58
  body: JSON.stringify({
50
59
  externalId,
51
60
  kind,
52
- schemaVersion: options.bundle.schemaVersion,
53
- startedAt: options.bundle.generatedAt,
61
+ schemaVersion: bundle.schemaVersion,
62
+ startedAt: bundle.generatedAt,
54
63
  metadata: {
55
- subject: options.bundle.subject,
56
- products: options.bundle.summary.products,
64
+ subject: bundle.subject,
65
+ products: bundle.summary.products,
57
66
  },
58
67
  }),
59
68
  });
@@ -65,12 +74,12 @@ export async function pushEvidenceToControlPlane(options) {
65
74
  sequence: 0,
66
75
  type: "agentcert.evidence.created",
67
76
  actor: "agentcert-cli",
68
- occurredAt: options.bundle.generatedAt,
77
+ occurredAt: bundle.generatedAt,
69
78
  payload: {
70
- verdict: options.bundle.verdict,
71
- totalEvidence: options.bundle.summary.totalEvidence,
72
- criticalEvidence: options.bundle.summary.criticalEvidence,
73
- highEvidence: options.bundle.summary.highEvidence,
79
+ verdict: bundle.verdict,
80
+ totalEvidence: bundle.summary.totalEvidence,
81
+ criticalEvidence: bundle.summary.criticalEvidence,
82
+ highEvidence: bundle.summary.highEvidence,
74
83
  },
75
84
  }],
76
85
  }),
@@ -78,22 +87,21 @@ export async function pushEvidenceToControlPlane(options) {
78
87
  const query = new URLSearchParams({
79
88
  fileName: options.fileName ?? "agentcert-evidence.json",
80
89
  kind: "evidence_bundle",
81
- schemaVersion: options.bundle.schemaVersion,
90
+ schemaVersion: bundle.schemaVersion,
82
91
  runId: run.id,
83
92
  });
84
93
  const evidence = await requestJson(request, `${projectUrl}/evidence?${query}`, {
85
94
  method: "POST",
86
95
  headers: { ...headers, "content-type": "application/json" },
87
- body: new Uint8Array(options.evidenceBytes).buffer,
96
+ body: new Uint8Array(hostedEvidence.bytes).buffer,
88
97
  });
89
- const companionArtifacts = options.companionArtifacts ?? [];
90
98
  const skippedArtifacts = options.skippedCompanionArtifacts ?? [];
91
99
  let artifactBytesUploaded = 0;
92
100
  for (const artifact of companionArtifacts) {
93
101
  const artifactQuery = new URLSearchParams({
94
102
  fileName: artifact.fileName,
95
103
  kind: artifact.kind,
96
- schemaVersion: options.bundle.schemaVersion,
104
+ schemaVersion: bundle.schemaVersion,
97
105
  runId: run.id,
98
106
  sourcePath: artifact.sourcePath,
99
107
  });
@@ -113,7 +121,7 @@ export async function pushEvidenceToControlPlane(options) {
113
121
  sequence: 1,
114
122
  type: "agentcert.companion_artifacts.processed",
115
123
  actor: "agentcert-cli",
116
- occurredAt: options.bundle.generatedAt,
124
+ occurredAt: bundle.generatedAt,
117
125
  payload: {
118
126
  uploadedCount: companionArtifacts.length,
119
127
  skippedCount: skippedArtifacts.length,
@@ -127,19 +135,20 @@ export async function pushEvidenceToControlPlane(options) {
127
135
  }),
128
136
  });
129
137
  }
130
- const firstDivergence = options.bundle.evidence.find((item) => item.severity === "critical" || item.severity === "high")?.message;
138
+ const firstDivergence = bundle.evidence.find((item) => item.severity === "critical" || item.severity === "high")?.message;
131
139
  await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/complete`, {
132
140
  method: "POST",
133
141
  headers: { ...headers, "content-type": "application/json" },
134
142
  body: JSON.stringify({
135
- status: options.bundle.verdict.passed ? "passed" : "failed",
136
- score: options.bundle.verdict.score,
137
- summary: `${options.bundle.subject.name}: ${options.bundle.verdict.level}`,
143
+ status: bundle.verdict.passed ? "passed" : "failed",
144
+ score: bundle.verdict.score,
145
+ summary: `${bundle.subject.name}: ${bundle.verdict.level}`,
138
146
  firstDivergence,
139
- completedAt: options.bundle.generatedAt,
147
+ completedAt: bundle.generatedAt,
140
148
  metadata: {
141
149
  evidenceId: evidence.id,
142
- evidenceSchemaVersion: options.bundle.schemaVersion,
150
+ evidenceSchemaVersion: bundle.schemaVersion,
151
+ artifactManifestVersion: bundle.artifactManifest?.schemaVersion,
143
152
  companionArtifactsUploaded: companionArtifacts.length,
144
153
  companionArtifactsSkipped: skippedArtifacts.length,
145
154
  companionArtifactBytesUploaded: artifactBytesUploaded,
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export * from "./bundle.js";
2
2
  export * from "./corpus.js";
3
3
  export * from "./corpus-store.js";
4
4
  export * from "./control-plane.js";
5
+ export * from "./conformance.js";
5
6
  export * from "./credentials.js";
6
7
  export * from "./failure-review.js";
7
8
  export * from "./evidence-signing.js";
@@ -59,10 +59,33 @@ function validateEvidenceBundle(value, errors) {
59
59
  requiredArray(value, "evidence", errors);
60
60
  requiredObject(value, "artifacts", errors);
61
61
  requiredArray(value, "standards", errors);
62
+ const subject = recordValue(value.subject);
63
+ if (subject) {
64
+ requiredStringAt(subject, "name", "subject.name", errors);
65
+ requiredEnumAt(subject, "type", ["agent", "mcp-server", "tool", "application", "unknown"], "subject.type", errors);
66
+ }
67
+ const verdict = recordValue(value.verdict);
68
+ if (verdict) {
69
+ requiredBooleanAt(verdict, "passed", "verdict.passed", errors);
70
+ requiredNumberRange(verdict, "score", "verdict.score", 0, 100, errors);
71
+ requiredStringAt(verdict, "level", "verdict.level", errors);
72
+ }
73
+ const summary = recordValue(value.summary);
74
+ if (summary) {
75
+ stringArray(summary.products, "summary.products", errors);
76
+ requiredNonNegativeInteger(summary, "criticalEvidence", "summary.criticalEvidence", errors);
77
+ requiredNonNegativeInteger(summary, "highEvidence", "summary.highEvidence", errors);
78
+ requiredNonNegativeInteger(summary, "totalEvidence", "summary.totalEvidence", errors);
79
+ }
80
+ validateTimestamp(value.generatedAt, "generatedAt", errors);
81
+ validateArtifactMap(value.artifacts, "artifacts", errors);
82
+ validateResultArray(value.results, "results", errors);
83
+ validateEvidenceArray(value.evidence, "evidence", errors);
84
+ validateStandards(value.standards, errors);
62
85
  }
63
86
  function validateResult(value, errors) {
64
87
  requiredConst(value, "schemaVersion", "1", errors);
65
- requiredEnum(value, "product", ["mcpbench", "tripwire-ci", "onegent-runtime", "agentcert-cli"], errors);
88
+ requiredString(value, "product", errors);
66
89
  requiredString(value, "runId", errors);
67
90
  requiredString(value, "timestamp", errors);
68
91
  requiredEnum(value, "phase", ["pre-release", "runtime"], errors);
@@ -70,6 +93,62 @@ function validateResult(value, errors) {
70
93
  requiredBoolean(value, "passed", errors);
71
94
  requiredObject(value, "artifacts", errors);
72
95
  requiredArray(value, "evidence", errors);
96
+ requiredNumberRange(value, "score", "score", 0, 100, errors);
97
+ validateTimestamp(value.timestamp, "timestamp", errors);
98
+ validateArtifactMap(value.artifacts, "artifacts", errors);
99
+ validateEvidenceArray(value.evidence, "evidence", errors);
100
+ }
101
+ function validateResultArray(input, path, errors) {
102
+ if (!Array.isArray(input))
103
+ return;
104
+ input.forEach((item, index) => {
105
+ const value = object(item, `${path}[${index}]`, errors);
106
+ if (!value)
107
+ return;
108
+ const nested = [];
109
+ validateResult(value, nested);
110
+ errors.push(...nested.map((error) => `${path}[${index}].${error}`));
111
+ });
112
+ }
113
+ function validateEvidenceArray(input, path, errors) {
114
+ if (!Array.isArray(input))
115
+ return;
116
+ input.forEach((item, index) => {
117
+ const value = object(item, `${path}[${index}]`, errors);
118
+ if (!value)
119
+ return;
120
+ requiredStringAt(value, "id", `${path}[${index}].id`, errors);
121
+ requiredStringAt(value, "kind", `${path}[${index}].kind`, errors);
122
+ requiredEnumAt(value, "severity", ["critical", "high", "medium", "low", "info"], `${path}[${index}].severity`, errors);
123
+ requiredStringAt(value, "message", `${path}[${index}].message`, errors);
124
+ for (const field of ["source", "artifactPath", "suggestedFix"]) {
125
+ if (value[field] !== undefined && typeof value[field] !== "string")
126
+ errors.push(`${path}[${index}].${field} must be a string.`);
127
+ }
128
+ if (value.metadata !== undefined && !recordValue(value.metadata))
129
+ errors.push(`${path}[${index}].metadata must be an object.`);
130
+ });
131
+ }
132
+ function validateArtifactMap(input, path, errors) {
133
+ const value = recordValue(input);
134
+ if (!value)
135
+ return;
136
+ for (const [key, item] of Object.entries(value))
137
+ if (typeof item !== "string")
138
+ errors.push(`${path}.${key} must be a string.`);
139
+ }
140
+ function validateStandards(input, errors) {
141
+ if (!Array.isArray(input))
142
+ return;
143
+ input.forEach((item, index) => {
144
+ const value = object(item, `standards[${index}]`, errors);
145
+ if (!value)
146
+ return;
147
+ requiredStringAt(value, "id", `standards[${index}].id`, errors);
148
+ requiredStringAt(value, "name", `standards[${index}].name`, errors);
149
+ requiredEnumAt(value, "status", ["mapped", "planned"], `standards[${index}].status`, errors);
150
+ requiredStringAt(value, "note", `standards[${index}].note`, errors);
151
+ });
73
152
  }
74
153
  function validateCorpusRecord(value, errors) {
75
154
  requiredConst(value, "schemaVersion", "1", errors);
@@ -161,3 +240,37 @@ function requiredEnum(input, key, values, errors) {
161
240
  errors.push(`${key} must be one of: ${values.join(", ")}.`);
162
241
  }
163
242
  }
243
+ function recordValue(input) {
244
+ return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
245
+ }
246
+ function requiredStringAt(input, key, path, errors) {
247
+ if (typeof input[key] !== "string" || input[key] === "")
248
+ errors.push(`${path} must be a non-empty string.`);
249
+ }
250
+ function requiredBooleanAt(input, key, path, errors) {
251
+ if (typeof input[key] !== "boolean")
252
+ errors.push(`${path} must be a boolean.`);
253
+ }
254
+ function requiredEnumAt(input, key, values, path, errors) {
255
+ if (typeof input[key] !== "string" || !values.includes(input[key]))
256
+ errors.push(`${path} must be one of: ${values.join(", ")}.`);
257
+ }
258
+ function requiredNumberRange(input, key, path, minimum, maximum, errors) {
259
+ const value = input[key];
260
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) {
261
+ errors.push(`${path} must be a finite number from ${minimum} to ${maximum}.`);
262
+ }
263
+ }
264
+ function requiredNonNegativeInteger(input, key, path, errors) {
265
+ const value = input[key];
266
+ if (!Number.isInteger(value) || value < 0)
267
+ errors.push(`${path} must be a non-negative integer.`);
268
+ }
269
+ function stringArray(input, path, errors) {
270
+ if (!Array.isArray(input) || input.some((item) => typeof item !== "string"))
271
+ errors.push(`${path} must be an array of strings.`);
272
+ }
273
+ function validateTimestamp(input, path, errors) {
274
+ if (typeof input === "string" && !Number.isFinite(Date.parse(input)))
275
+ errors.push(`${path} must be a valid date-time string.`);
276
+ }
package/dist/types.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export const AGENTCERT_EVIDENCE_SCHEMA_VERSION = "agentcert.evidence.v0.1";
2
2
  export const AGENTCERT_EVIDENCE_SCHEMA_SEMVER = "0.1.0";
3
+ export const AGENTCERT_ARTIFACT_MANIFEST_VERSION = "agentcert.artifact_manifest.v0.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcert",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Assurance release gates, runtime evidence, and regression CI for AI agents.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",