agentcert 0.2.6 → 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/dist/cli.js +32 -3
- package/dist/command-help.js +6 -0
- package/dist/conformance.js +115 -0
- package/dist/index.js +1 -0
- package/dist/schema-validator.js +114 -1
- package/package.json +1 -1
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@
|
|
915
|
-
- uses: actions/setup-node@
|
|
942
|
+
- uses: actions/checkout@v7
|
|
943
|
+
- uses: actions/setup-node@v6
|
|
916
944
|
with:
|
|
917
945
|
node-version: "20"
|
|
918
946
|
|
|
919
|
-
-
|
|
947
|
+
- id: agentcert
|
|
948
|
+
uses: Kakarottoooo/agentcert/actions/tripwire@v0
|
|
920
949
|
with:
|
|
921
950
|
config: tripwire.yml
|
|
922
951
|
out: .tripwire/latest
|
package/dist/command-help.js
CHANGED
|
@@ -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
|
+
}
|
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";
|
package/dist/schema-validator.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|