agentcert 0.1.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 +84 -0
- package/dist/badge.js +31 -0
- package/dist/bundle.js +80 -0
- package/dist/cli.js +553 -0
- package/dist/corpus-store.js +241 -0
- package/dist/corpus.js +435 -0
- package/dist/failure-review.js +188 -0
- package/dist/index.js +10 -0
- package/dist/lab.js +323 -0
- package/dist/local-server.js +491 -0
- package/dist/monitor.js +100 -0
- package/dist/normalizers.js +193 -0
- package/dist/report.js +148 -0
- package/dist/runner.js +341 -0
- package/dist/schema-validator.js +150 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function normalizeMcpBenchResult(input, artifactPath) {
|
|
3
|
+
const value = asRecord(input);
|
|
4
|
+
const violations = Array.isArray(value.violations) ? value.violations : [];
|
|
5
|
+
const scorerResults = Array.isArray(value.scorer_results) ? value.scorer_results : [];
|
|
6
|
+
const runId = stringValue(value.run_id) ?? stableRunId("mcpbench", artifactPath, input);
|
|
7
|
+
const passed = Boolean(value.passed);
|
|
8
|
+
const score = numberValue(value.total_score) ?? 0;
|
|
9
|
+
const evidence = violations.map((violation, index) => {
|
|
10
|
+
const record = asRecord(violation);
|
|
11
|
+
return {
|
|
12
|
+
id: stringValue(record.id) ?? `mcpbench_violation_${index + 1}`,
|
|
13
|
+
kind: stringValue(record.kind) ?? "policy_violation",
|
|
14
|
+
severity: severityValue(record.severity) ?? "high",
|
|
15
|
+
message: stringValue(record.message) ?? "MCPBench policy violation.",
|
|
16
|
+
source: "mcpbench",
|
|
17
|
+
artifactPath,
|
|
18
|
+
metadata: record,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
for (const scorer of scorerResults) {
|
|
22
|
+
const record = asRecord(scorer);
|
|
23
|
+
if (record.passed === false) {
|
|
24
|
+
evidence.push({
|
|
25
|
+
id: `mcpbench_scorer_${evidence.length + 1}`,
|
|
26
|
+
kind: "assertion_result",
|
|
27
|
+
severity: "medium",
|
|
28
|
+
message: `${stringValue(record.name) ?? "scorer"} failed.`,
|
|
29
|
+
source: "mcpbench",
|
|
30
|
+
artifactPath,
|
|
31
|
+
metadata: record,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: "1",
|
|
37
|
+
product: "mcpbench",
|
|
38
|
+
runId,
|
|
39
|
+
timestamp: stringValue(value.completed_at) ?? new Date().toISOString(),
|
|
40
|
+
phase: "pre-release",
|
|
41
|
+
score,
|
|
42
|
+
passed,
|
|
43
|
+
certLevel: stringValue(value.cert_level),
|
|
44
|
+
summary: passed ? "MCPBench completed without blocking violations." : "MCPBench found blocking evidence.",
|
|
45
|
+
artifacts: recordOfStrings(value.artifact_paths, { results: artifactPath }),
|
|
46
|
+
evidence,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export function normalizeTripwireResult(input, artifactPath) {
|
|
50
|
+
const value = asRecord(input);
|
|
51
|
+
const summary = asRecord(value.summary);
|
|
52
|
+
const gate = asRecord(value.gate);
|
|
53
|
+
const runs = Array.isArray(value.runs) ? value.runs : [];
|
|
54
|
+
const overallScore = normalizeScore(numberValue(summary.overallScore) ?? 0);
|
|
55
|
+
const passed = Boolean(gate.passed);
|
|
56
|
+
const evidence = [];
|
|
57
|
+
for (const run of runs) {
|
|
58
|
+
const record = asRecord(run);
|
|
59
|
+
const assertions = Array.isArray(record.assertions) ? record.assertions : [];
|
|
60
|
+
for (const assertion of assertions) {
|
|
61
|
+
const assertionRecord = asRecord(assertion);
|
|
62
|
+
if (assertionRecord.pass === false) {
|
|
63
|
+
evidence.push({
|
|
64
|
+
id: `tripwire_assertion_${evidence.length + 1}`,
|
|
65
|
+
kind: "assertion_result",
|
|
66
|
+
severity: "high",
|
|
67
|
+
message: stringValue(assertionRecord.message) ?? "Tripwire assertion failed.",
|
|
68
|
+
source: "tripwire-ci",
|
|
69
|
+
artifactPath: stringValue(record.tracePath) ?? artifactPath,
|
|
70
|
+
metadata: {
|
|
71
|
+
scenarioName: record.scenarioName,
|
|
72
|
+
faultName: record.faultName,
|
|
73
|
+
assertion: assertionRecord,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
schemaVersion: "1",
|
|
81
|
+
product: "tripwire-ci",
|
|
82
|
+
runId: stableRunId("tripwire", artifactPath, input),
|
|
83
|
+
timestamp: stringValue(value.timestamp) ?? new Date().toISOString(),
|
|
84
|
+
phase: "pre-release",
|
|
85
|
+
score: overallScore,
|
|
86
|
+
passed,
|
|
87
|
+
summary: passed ? "Tripwire CI gate passed." : "Tripwire CI gate failed.",
|
|
88
|
+
artifacts: { result: artifactPath, outDir: stringValue(value.outDir) ?? "" },
|
|
89
|
+
evidence,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function normalizeOnegentAuditPacket(input, artifactPath) {
|
|
93
|
+
const value = asRecord(input);
|
|
94
|
+
const action = asRecord(value.actionIntent);
|
|
95
|
+
const risk = asRecord(value.riskAssessment);
|
|
96
|
+
const approval = asRecord(value.approvalRequest);
|
|
97
|
+
const verification = asRecord(value.verificationResult);
|
|
98
|
+
const auditEvents = Array.isArray(value.auditEvents) ? value.auditEvents : [];
|
|
99
|
+
const verificationSuccess = verification.success === true;
|
|
100
|
+
const approved = approval.status === "APPROVED" || approval.status === undefined;
|
|
101
|
+
const passed = verificationSuccess && approved;
|
|
102
|
+
const riskLevel = stringValue(risk.riskLevel) ?? "UNKNOWN";
|
|
103
|
+
const evidence = [
|
|
104
|
+
{
|
|
105
|
+
id: "onegent_risk_assessment",
|
|
106
|
+
kind: "runtime_risk_assessment",
|
|
107
|
+
severity: riskLevel === "CRITICAL" ? "critical" : riskLevel === "HIGH" ? "high" : "medium",
|
|
108
|
+
message: `Runtime action risk assessed as ${riskLevel}.`,
|
|
109
|
+
source: "onegent-runtime",
|
|
110
|
+
artifactPath,
|
|
111
|
+
metadata: risk,
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
if (approval.status) {
|
|
115
|
+
evidence.push({
|
|
116
|
+
id: "onegent_approval",
|
|
117
|
+
kind: "approval_record",
|
|
118
|
+
severity: approval.status === "APPROVED" ? "info" : "high",
|
|
119
|
+
message: `Human approval status: ${approval.status}.`,
|
|
120
|
+
source: "onegent-runtime",
|
|
121
|
+
artifactPath,
|
|
122
|
+
metadata: approval,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
evidence.push({
|
|
126
|
+
id: "onegent_verification",
|
|
127
|
+
kind: "runtime_verification",
|
|
128
|
+
severity: verificationSuccess ? "info" : "critical",
|
|
129
|
+
message: verificationSuccess
|
|
130
|
+
? "Observed runtime state matched expected state."
|
|
131
|
+
: "Observed runtime state did not match expected state.",
|
|
132
|
+
source: "onegent-runtime",
|
|
133
|
+
artifactPath,
|
|
134
|
+
metadata: verification,
|
|
135
|
+
});
|
|
136
|
+
for (const event of auditEvents) {
|
|
137
|
+
const record = asRecord(event);
|
|
138
|
+
evidence.push({
|
|
139
|
+
id: stringValue(record.id) ?? `onegent_audit_${evidence.length + 1}`,
|
|
140
|
+
kind: "audit_event",
|
|
141
|
+
severity: "info",
|
|
142
|
+
message: stringValue(record.message) ?? stringValue(record.eventType) ?? "Onegent audit event.",
|
|
143
|
+
source: "onegent-runtime",
|
|
144
|
+
artifactPath,
|
|
145
|
+
metadata: record,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
schemaVersion: "1",
|
|
150
|
+
product: "onegent-runtime",
|
|
151
|
+
runId: stringValue(action.id) ?? stableRunId("onegent", artifactPath, input),
|
|
152
|
+
timestamp: stringValue(verification.createdAt) ?? new Date().toISOString(),
|
|
153
|
+
phase: "runtime",
|
|
154
|
+
score: passed ? 100 : 0,
|
|
155
|
+
passed,
|
|
156
|
+
summary: passed
|
|
157
|
+
? "Runtime action was approved, mock-executed, verified, and audited."
|
|
158
|
+
: "Runtime action did not complete the approval and verification gate.",
|
|
159
|
+
artifacts: { auditPacket: artifactPath },
|
|
160
|
+
evidence,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function normalizeScore(value) {
|
|
164
|
+
return value <= 1 ? Math.round(value * 100) : Math.round(value);
|
|
165
|
+
}
|
|
166
|
+
function stableRunId(prefix, artifactPath, input) {
|
|
167
|
+
const hash = createHash("sha256").update(`${artifactPath}:${JSON.stringify(input)}`).digest("hex").slice(0, 12);
|
|
168
|
+
return `${prefix}_${hash}`;
|
|
169
|
+
}
|
|
170
|
+
function recordOfStrings(input, fallback) {
|
|
171
|
+
const record = asRecord(input);
|
|
172
|
+
const output = { ...fallback };
|
|
173
|
+
for (const [key, value] of Object.entries(record)) {
|
|
174
|
+
if (typeof value === "string") {
|
|
175
|
+
output[key] = value;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return output;
|
|
179
|
+
}
|
|
180
|
+
function asRecord(input) {
|
|
181
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : {};
|
|
182
|
+
}
|
|
183
|
+
function stringValue(input) {
|
|
184
|
+
return typeof input === "string" && input.length > 0 ? input : undefined;
|
|
185
|
+
}
|
|
186
|
+
function numberValue(input) {
|
|
187
|
+
return typeof input === "number" && Number.isFinite(input) ? input : undefined;
|
|
188
|
+
}
|
|
189
|
+
function severityValue(input) {
|
|
190
|
+
return input === "critical" || input === "high" || input === "medium" || input === "low" || input === "info"
|
|
191
|
+
? input
|
|
192
|
+
: undefined;
|
|
193
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export function renderMarkdownReport(bundle) {
|
|
2
|
+
const lines = [
|
|
3
|
+
"# AgentCert Evidence Report",
|
|
4
|
+
"",
|
|
5
|
+
`Subject: ${bundle.subject.name}`,
|
|
6
|
+
`Generated: ${bundle.generatedAt}`,
|
|
7
|
+
`Verdict: ${bundle.verdict.passed ? "PASS" : "FAIL"}`,
|
|
8
|
+
`Score: ${bundle.verdict.score}`,
|
|
9
|
+
`Level: ${bundle.verdict.level}`,
|
|
10
|
+
"",
|
|
11
|
+
"## Results",
|
|
12
|
+
"",
|
|
13
|
+
];
|
|
14
|
+
for (const result of bundle.results) {
|
|
15
|
+
lines.push(`- ${result.product}: ${result.passed ? "PASS" : "FAIL"} (${result.score}/100, ${result.phase})`, ` ${result.summary ?? ""}`.trimEnd());
|
|
16
|
+
}
|
|
17
|
+
lines.push("", "## Evidence", "");
|
|
18
|
+
if (bundle.evidence.length === 0) {
|
|
19
|
+
lines.push("No blocking evidence recorded.");
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
for (const evidence of bundle.evidence) {
|
|
23
|
+
lines.push(`- [${evidence.severity}] ${evidence.kind}: ${evidence.message}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
lines.push("", "## Standards Mapping", "");
|
|
27
|
+
for (const standard of bundle.standards) {
|
|
28
|
+
lines.push(`- ${standard.name}: ${standard.note}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push("", "## Artifacts", "");
|
|
31
|
+
for (const [name, path] of Object.entries(bundle.artifacts)) {
|
|
32
|
+
lines.push(`- ${name}: \`${path}\``);
|
|
33
|
+
}
|
|
34
|
+
return `${lines.join("\n")}\n`;
|
|
35
|
+
}
|
|
36
|
+
export function renderHtmlReport(bundle) {
|
|
37
|
+
const resultCards = bundle.results
|
|
38
|
+
.map((result) => `<article class="card ${result.passed ? "pass" : "fail"}">
|
|
39
|
+
<span>${escapeHtml(result.phase)}</span>
|
|
40
|
+
<h2>${escapeHtml(result.product)}</h2>
|
|
41
|
+
<strong>${result.passed ? "PASS" : "FAIL"} ${result.score}/100</strong>
|
|
42
|
+
<p>${escapeHtml(result.summary ?? "No summary provided.")}</p>
|
|
43
|
+
</article>`)
|
|
44
|
+
.join("");
|
|
45
|
+
const evidenceRows = bundle.evidence.length === 0
|
|
46
|
+
? `<tr><td colspan="4">No blocking evidence recorded.</td></tr>`
|
|
47
|
+
: bundle.evidence
|
|
48
|
+
.map((evidence) => `<tr>
|
|
49
|
+
<td><span class="severity ${escapeHtml(evidence.severity)}">${escapeHtml(evidence.severity)}</span></td>
|
|
50
|
+
<td>${escapeHtml(evidence.kind)}</td>
|
|
51
|
+
<td>${escapeHtml(evidence.message)}</td>
|
|
52
|
+
<td>${evidence.artifactPath ? artifactLink(evidence.artifactPath) : "-"}</td>
|
|
53
|
+
</tr>`)
|
|
54
|
+
.join("");
|
|
55
|
+
const artifactRows = Object.entries(bundle.artifacts)
|
|
56
|
+
.map(([name, path]) => `<tr><td>${escapeHtml(name)}</td><td>${artifactLink(path)}</td></tr>`)
|
|
57
|
+
.join("");
|
|
58
|
+
const standardItems = bundle.standards
|
|
59
|
+
.map((standard) => `<li><strong>${escapeHtml(standard.name)}</strong><span>${escapeHtml(standard.note)}</span></li>`)
|
|
60
|
+
.join("");
|
|
61
|
+
return `<!doctype html>
|
|
62
|
+
<html lang="en">
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="utf-8">
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
66
|
+
<title>AgentCert Evidence Report</title>
|
|
67
|
+
<style>
|
|
68
|
+
:root { color-scheme: light; --ink: #102033; --muted: #5d6978; --line: #d9e0e8; --bg: #f6f8fb; --panel: #fff; --pass: #087f5b; --fail: #c92a2a; }
|
|
69
|
+
body { margin: 0; font: 15px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); }
|
|
70
|
+
main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 32px 0 48px; }
|
|
71
|
+
header { display: grid; gap: 16px; margin-bottom: 24px; }
|
|
72
|
+
h1 { margin: 0; font-size: clamp(32px, 6vw, 62px); line-height: 0.95; }
|
|
73
|
+
h2 { margin: 0 0 10px; font-size: 18px; }
|
|
74
|
+
p { color: var(--muted); margin: 0; }
|
|
75
|
+
.summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin: 24px 0; }
|
|
76
|
+
.metric, .card, section { background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 18px; }
|
|
77
|
+
.metric span, .card span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 700; }
|
|
78
|
+
.metric strong { display: block; margin-top: 8px; font-size: 28px; }
|
|
79
|
+
.verdict-pass { color: var(--pass); }
|
|
80
|
+
.verdict-fail { color: var(--fail); }
|
|
81
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
|
|
82
|
+
.card { border-left: 5px solid var(--line); }
|
|
83
|
+
.card.pass { border-left-color: var(--pass); }
|
|
84
|
+
.card.fail { border-left-color: var(--fail); }
|
|
85
|
+
.card strong { display: block; margin: 8px 0; font-size: 20px; }
|
|
86
|
+
section { margin-top: 16px; overflow-x: auto; }
|
|
87
|
+
table { width: 100%; border-collapse: collapse; min-width: 720px; }
|
|
88
|
+
th, td { padding: 10px 8px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
89
|
+
th { font-size: 12px; text-transform: uppercase; color: var(--muted); }
|
|
90
|
+
a { color: #0b63ce; text-decoration: none; font-weight: 650; }
|
|
91
|
+
ul { margin: 0; padding-left: 20px; }
|
|
92
|
+
li { margin: 8px 0; }
|
|
93
|
+
li span { display: block; color: var(--muted); }
|
|
94
|
+
.severity { display: inline-block; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 800; color: #fff; background: #687385; }
|
|
95
|
+
.severity.critical, .severity.high { background: var(--fail); }
|
|
96
|
+
.severity.medium { background: #b35c00; }
|
|
97
|
+
.severity.low, .severity.info { background: #24745a; }
|
|
98
|
+
@media (max-width: 760px) { .summary { grid-template-columns: 1fr 1fr; } main { width: min(100% - 24px, 1120px); padding-top: 20px; } }
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<main>
|
|
103
|
+
<header>
|
|
104
|
+
<p>AgentCert Evidence Report</p>
|
|
105
|
+
<h1>${escapeHtml(bundle.subject.name)}</h1>
|
|
106
|
+
<p>Generated ${escapeHtml(bundle.generatedAt)}. Portable evidence bundle ${escapeHtml(bundle.runId)}.</p>
|
|
107
|
+
</header>
|
|
108
|
+
<div class="summary">
|
|
109
|
+
<div class="metric"><span>Verdict</span><strong class="${bundle.verdict.passed ? "verdict-pass" : "verdict-fail"}">${bundle.verdict.passed ? "PASS" : "FAIL"}</strong></div>
|
|
110
|
+
<div class="metric"><span>Score</span><strong>${bundle.verdict.score}/100</strong></div>
|
|
111
|
+
<div class="metric"><span>Level</span><strong>${escapeHtml(bundle.verdict.level)}</strong></div>
|
|
112
|
+
<div class="metric"><span>Evidence</span><strong>${bundle.summary.totalEvidence}</strong></div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="cards">${resultCards}</div>
|
|
115
|
+
<section>
|
|
116
|
+
<h2>Evidence</h2>
|
|
117
|
+
<table>
|
|
118
|
+
<thead><tr><th>Severity</th><th>Kind</th><th>Message</th><th>Artifact</th></tr></thead>
|
|
119
|
+
<tbody>${evidenceRows}</tbody>
|
|
120
|
+
</table>
|
|
121
|
+
</section>
|
|
122
|
+
<section>
|
|
123
|
+
<h2>Artifacts</h2>
|
|
124
|
+
<table>
|
|
125
|
+
<thead><tr><th>Name</th><th>Path</th></tr></thead>
|
|
126
|
+
<tbody>${artifactRows || `<tr><td colspan="2">No artifacts recorded.</td></tr>`}</tbody>
|
|
127
|
+
</table>
|
|
128
|
+
</section>
|
|
129
|
+
<section>
|
|
130
|
+
<h2>Standards Mapping</h2>
|
|
131
|
+
<ul>${standardItems}</ul>
|
|
132
|
+
</section>
|
|
133
|
+
</main>
|
|
134
|
+
</body>
|
|
135
|
+
</html>
|
|
136
|
+
`;
|
|
137
|
+
}
|
|
138
|
+
function artifactLink(path) {
|
|
139
|
+
const escaped = escapeHtml(path);
|
|
140
|
+
return `<a href="${escapeHtml(path)}">${escaped}</a>`;
|
|
141
|
+
}
|
|
142
|
+
function escapeHtml(value) {
|
|
143
|
+
return String(value)
|
|
144
|
+
.replaceAll("&", "&")
|
|
145
|
+
.replaceAll("<", "<")
|
|
146
|
+
.replaceAll(">", ">")
|
|
147
|
+
.replaceAll('"', """);
|
|
148
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { renderAgentCertBadge } from "./badge.js";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { buildEvidenceBundle } from "./bundle.js";
|
|
7
|
+
import { recordsFromAgentCertResult, renderCorpusSummary, summarizeCorpus, writeReviewedFailureDataset, } from "./corpus.js";
|
|
8
|
+
import { openCorpusStore, parseCorpusStoreKind } from "./corpus-store.js";
|
|
9
|
+
import { applyFailureReviews, readFailureReviews } from "./failure-review.js";
|
|
10
|
+
import { buildMonitorSnapshot, writeMonitorSnapshot } from "./monitor.js";
|
|
11
|
+
import { normalizeMcpBenchResult, normalizeOnegentAuditPacket, normalizeTripwireResult } from "./normalizers.js";
|
|
12
|
+
import { renderHtmlReport, renderMarkdownReport } from "./report.js";
|
|
13
|
+
const JOB_KEYS = ["mcpbench", "tripwire", "onegent"];
|
|
14
|
+
export function publicDemoRunProfile() {
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: "1",
|
|
17
|
+
subject: {
|
|
18
|
+
name: "agentcert-public-demo",
|
|
19
|
+
type: "agent",
|
|
20
|
+
},
|
|
21
|
+
artifacts: {
|
|
22
|
+
mcpbench: "public-demo/lifecycle-evidence/mcpbench-passing/results.json",
|
|
23
|
+
tripwire: "public-demo/browser-agent-robustness/evidence/tripwire-public-demo/tripwire-result.json",
|
|
24
|
+
onegent: "public-demo/lifecycle-evidence/onegent-procurement/audit-packet.json",
|
|
25
|
+
},
|
|
26
|
+
outputDir: "public-demo/browser-agent-robustness/evidence/agentcert-public-demo",
|
|
27
|
+
run: {
|
|
28
|
+
corpus: {
|
|
29
|
+
path: "public-demo/browser-agent-robustness/evidence/agentcert-corpus.jsonl",
|
|
30
|
+
reviewsPath: "public-demo/browser-agent-robustness/evidence/failure-reviews.jsonl",
|
|
31
|
+
replace: true,
|
|
32
|
+
},
|
|
33
|
+
monitor: {
|
|
34
|
+
outputs: [
|
|
35
|
+
"packages/agentcert-dashboard/public/data/monitor.json",
|
|
36
|
+
"public-demo/agentcert-monitor/data/monitor.json",
|
|
37
|
+
],
|
|
38
|
+
detailUrl: "../browser-agent-robustness/",
|
|
39
|
+
},
|
|
40
|
+
dataset: {
|
|
41
|
+
reviewedOutputs: ["public-demo/browser-agent-robustness/evidence/reviewed-failure-dataset.jsonl"],
|
|
42
|
+
},
|
|
43
|
+
gate: {
|
|
44
|
+
failOnVerdict: false,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export async function loadRunProfile(profileName, configPath) {
|
|
50
|
+
if (profileName === "public-demo") {
|
|
51
|
+
return publicDemoRunProfile();
|
|
52
|
+
}
|
|
53
|
+
if (profileName && profileName !== "public-demo") {
|
|
54
|
+
throw new Error(`Unknown AgentCert run profile "${profileName}". Available profiles: public-demo.`);
|
|
55
|
+
}
|
|
56
|
+
if (configPath) {
|
|
57
|
+
return (await readJson(configPath));
|
|
58
|
+
}
|
|
59
|
+
const defaultConfigPath = "agentcert.config.json";
|
|
60
|
+
try {
|
|
61
|
+
await access(resolve(defaultConfigPath));
|
|
62
|
+
return (await readJson(defaultConfigPath));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("agentcert run requires --profile public-demo, --config <path>, explicit artifact flags, or agentcert.config.json in the current directory.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function profileFromArtifactFlags(overrides) {
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: "1",
|
|
71
|
+
subject: {
|
|
72
|
+
name: overrides.subject ?? "agentcert-subject",
|
|
73
|
+
type: "agent",
|
|
74
|
+
},
|
|
75
|
+
artifacts: {
|
|
76
|
+
mcpbench: overrides.mcpbench,
|
|
77
|
+
tripwire: overrides.tripwire,
|
|
78
|
+
onegent: overrides.onegent,
|
|
79
|
+
},
|
|
80
|
+
outputDir: overrides.outDir ?? ".agentcert/latest",
|
|
81
|
+
run: {
|
|
82
|
+
corpus: {
|
|
83
|
+
path: overrides.corpusPath ?? ".agentcert/corpus/corpus.jsonl",
|
|
84
|
+
replace: overrides.replaceCorpus ?? false,
|
|
85
|
+
},
|
|
86
|
+
monitor: {
|
|
87
|
+
outputs: [overrides.monitorOut ?? ".agentcert/latest/monitor.json"],
|
|
88
|
+
},
|
|
89
|
+
dataset: {
|
|
90
|
+
reviewedOutputs: [overrides.reviewedDatasetOut ?? ".agentcert/latest/reviewed-failure-dataset.jsonl"],
|
|
91
|
+
},
|
|
92
|
+
gate: {
|
|
93
|
+
failOnVerdict: overrides.failOnVerdict ?? false,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export function applyRunOverrides(profile, overrides = {}) {
|
|
99
|
+
const next = {
|
|
100
|
+
...profile,
|
|
101
|
+
subject: {
|
|
102
|
+
...profile.subject,
|
|
103
|
+
name: overrides.subject ?? profile.subject.name,
|
|
104
|
+
},
|
|
105
|
+
artifacts: {
|
|
106
|
+
...profile.artifacts,
|
|
107
|
+
...(overrides.mcpbench ? { mcpbench: overrides.mcpbench } : {}),
|
|
108
|
+
...(overrides.tripwire ? { tripwire: overrides.tripwire } : {}),
|
|
109
|
+
...(overrides.onegent ? { onegent: overrides.onegent } : {}),
|
|
110
|
+
},
|
|
111
|
+
outputDir: overrides.outDir ?? profile.outputDir,
|
|
112
|
+
run: {
|
|
113
|
+
...profile.run,
|
|
114
|
+
corpus: {
|
|
115
|
+
...profile.run?.corpus,
|
|
116
|
+
...(overrides.corpusPath ? { path: overrides.corpusPath } : {}),
|
|
117
|
+
...(overrides.reviewsPath ? { reviewsPath: overrides.reviewsPath } : {}),
|
|
118
|
+
...(overrides.replaceCorpus === undefined ? {} : { replace: overrides.replaceCorpus }),
|
|
119
|
+
},
|
|
120
|
+
monitor: {
|
|
121
|
+
...profile.run?.monitor,
|
|
122
|
+
...(overrides.monitorOut ? { outputs: [overrides.monitorOut] } : {}),
|
|
123
|
+
},
|
|
124
|
+
dataset: {
|
|
125
|
+
...profile.run?.dataset,
|
|
126
|
+
...(overrides.reviewedDatasetOut ? { reviewedOutputs: [overrides.reviewedDatasetOut] } : {}),
|
|
127
|
+
},
|
|
128
|
+
gate: {
|
|
129
|
+
...profile.run?.gate,
|
|
130
|
+
...(overrides.failOnVerdict === undefined ? {} : { failOnVerdict: overrides.failOnVerdict }),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
export async function runAgentCertProfile(profileInput, options = {}) {
|
|
137
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
138
|
+
const profile = applyRunOverrides(profileInput, options.overrides);
|
|
139
|
+
const steps = [];
|
|
140
|
+
const loaded = [];
|
|
141
|
+
for (const key of JOB_KEYS) {
|
|
142
|
+
const job = jobFor(profile, key);
|
|
143
|
+
if (!job.artifact && !job.command) {
|
|
144
|
+
steps.push({ id: key, status: "skipped", message: "No command or artifact configured." });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (job.command && options.runCommands !== false) {
|
|
148
|
+
const step = await runJobCommand(key, job.command, cwd, options.commandStdio ?? "inherit");
|
|
149
|
+
steps.push(step);
|
|
150
|
+
if (step.status === "failed" && !job.allowCommandFailure) {
|
|
151
|
+
throw new Error(`${key} command failed with exit code ${step.exitCode ?? "unknown"}.`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!job.artifact) {
|
|
155
|
+
if (job.required !== false) {
|
|
156
|
+
throw new Error(`${key} did not define an artifact path.`);
|
|
157
|
+
}
|
|
158
|
+
steps.push({ id: key, status: "skipped", message: "No artifact configured." });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const raw = await readJson(job.artifact, cwd);
|
|
162
|
+
loaded.push({ key, path: job.artifact, raw, result: normalizeArtifact(key, raw, job.artifact) });
|
|
163
|
+
steps.push({ id: key, status: "passed", artifactPath: job.artifact });
|
|
164
|
+
}
|
|
165
|
+
if (loaded.length === 0) {
|
|
166
|
+
throw new Error("No AgentCert artifacts were loaded. Configure at least one MCPBench, Tripwire, or Onegent artifact.");
|
|
167
|
+
}
|
|
168
|
+
const bundle = buildEvidenceBundle(loaded.map((artifact) => artifact.result), profile.subject.name, profile.subject.type);
|
|
169
|
+
const reviewsPath = profile.run?.corpus?.reviewsPath;
|
|
170
|
+
const reviews = await readFailureReviews(reviewsPath ? resolve(cwd, reviewsPath) : undefined);
|
|
171
|
+
const records = applyFailureReviews(loaded.flatMap(({ result, path, raw }) => recordsFromAgentCertResult(result, path, profile.subject.name, raw)), reviews);
|
|
172
|
+
const outputs = { monitor: [], reviewedDataset: [] };
|
|
173
|
+
const reportEnabled = profile.run?.report?.enabled ?? true;
|
|
174
|
+
const reportDir = profile.run?.report?.outDir ?? profile.outputDir;
|
|
175
|
+
if (reportEnabled && reportDir) {
|
|
176
|
+
const resolvedReportDir = resolve(cwd, reportDir);
|
|
177
|
+
await mkdir(resolvedReportDir, { recursive: true });
|
|
178
|
+
await writeFile(`${resolvedReportDir}/agentcert-evidence.json`, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
179
|
+
await writeFile(`${resolvedReportDir}/agentcert-report.md`, renderMarkdownReport(bundle));
|
|
180
|
+
await writeFile(`${resolvedReportDir}/agentcert-report.html`, renderHtmlReport(bundle));
|
|
181
|
+
await writeFile(`${resolvedReportDir}/badge.svg`, renderAgentCertBadge(bundle));
|
|
182
|
+
steps.push({ id: "report", status: "passed", artifactPath: reportDir });
|
|
183
|
+
outputs.reportDir = reportDir;
|
|
184
|
+
outputs.evidenceBundle = artifactPath(reportDir, "agentcert-evidence.json");
|
|
185
|
+
outputs.markdownReport = artifactPath(reportDir, "agentcert-report.md");
|
|
186
|
+
outputs.htmlReport = artifactPath(reportDir, "agentcert-report.html");
|
|
187
|
+
outputs.badge = artifactPath(reportDir, "badge.svg");
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
steps.push({ id: "report", status: "skipped", message: "Report output disabled." });
|
|
191
|
+
}
|
|
192
|
+
const corpusOptions = corpusStoreOptions(profile, cwd);
|
|
193
|
+
const store = await openCorpusStore(corpusOptions);
|
|
194
|
+
try {
|
|
195
|
+
await store.append(records, { replace: profile.run?.corpus?.replace ?? false });
|
|
196
|
+
const allRecords = await store.readAll();
|
|
197
|
+
steps.push({ id: "corpus", status: "passed", artifactPath: profile.run?.corpus?.path ?? corpusOptions.jsonlPath });
|
|
198
|
+
outputs.corpus = profile.run?.corpus?.path ?? corpusOptions.jsonlPath ?? store.description;
|
|
199
|
+
const monitorOutputs = monitorOutputsFor(profile);
|
|
200
|
+
for (const outPath of monitorOutputs) {
|
|
201
|
+
const snapshot = buildMonitorSnapshot(allRecords, {
|
|
202
|
+
subject: profile.run?.monitor?.subject ?? profile.subject.name,
|
|
203
|
+
detailUrl: profile.run?.monitor?.detailUrl,
|
|
204
|
+
});
|
|
205
|
+
await writeMonitorSnapshot(resolve(cwd, outPath), snapshot);
|
|
206
|
+
outputs.monitor.push(outPath);
|
|
207
|
+
}
|
|
208
|
+
const reviewedDatasetOutputs = reviewedDatasetOutputsFor(profile);
|
|
209
|
+
for (const outPath of reviewedDatasetOutputs) {
|
|
210
|
+
await writeReviewedFailureDataset(resolve(cwd, outPath), allRecords);
|
|
211
|
+
outputs.reviewedDataset.push(outPath);
|
|
212
|
+
}
|
|
213
|
+
steps.push({
|
|
214
|
+
id: "dataset",
|
|
215
|
+
status: reviewedDatasetOutputs.length > 0 ? "passed" : "skipped",
|
|
216
|
+
artifactPath: reviewedDatasetOutputs.join(", "),
|
|
217
|
+
message: reviewedDatasetOutputs.length > 0 ? undefined : "No reviewed dataset output configured.",
|
|
218
|
+
});
|
|
219
|
+
steps.push({
|
|
220
|
+
id: "monitor",
|
|
221
|
+
status: monitorOutputs.length > 0 ? "passed" : "skipped",
|
|
222
|
+
artifactPath: monitorOutputs.join(", "),
|
|
223
|
+
message: monitorOutputs.length > 0 ? undefined : "No monitor output configured.",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await store.close();
|
|
228
|
+
}
|
|
229
|
+
const manifestPath = profile.run?.manifest?.out ?? (profile.outputDir ? join(profile.outputDir, "agentcert-run-manifest.json") : undefined);
|
|
230
|
+
const manifest = {
|
|
231
|
+
schemaVersion: "1",
|
|
232
|
+
kind: "agentcert.run_manifest",
|
|
233
|
+
runId: stableRunId(profile, bundle),
|
|
234
|
+
generatedAt: new Date().toISOString(),
|
|
235
|
+
subject: profile.subject,
|
|
236
|
+
steps,
|
|
237
|
+
outputs,
|
|
238
|
+
verdict: bundle.verdict,
|
|
239
|
+
summary: bundle.summary,
|
|
240
|
+
};
|
|
241
|
+
if (manifestPath) {
|
|
242
|
+
const resolvedManifestPath = resolve(cwd, manifestPath);
|
|
243
|
+
await mkdir(dirname(resolvedManifestPath), { recursive: true });
|
|
244
|
+
manifest.outputs.manifest = normalizeArtifactPath(manifestPath);
|
|
245
|
+
await writeFile(resolvedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
246
|
+
}
|
|
247
|
+
const failOnVerdict = profile.run?.gate?.failOnVerdict ?? false;
|
|
248
|
+
return {
|
|
249
|
+
exitCode: failOnVerdict && !bundle.verdict.passed ? 1 : 0,
|
|
250
|
+
bundle,
|
|
251
|
+
records,
|
|
252
|
+
manifest,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export function renderRunSummary(outcome) {
|
|
256
|
+
const lines = [
|
|
257
|
+
"# AgentCert Run",
|
|
258
|
+
"",
|
|
259
|
+
`Subject: ${outcome.manifest.subject.name}`,
|
|
260
|
+
`Verdict: ${outcome.bundle.verdict.passed ? "PASS" : "FAIL"} (${outcome.bundle.verdict.score}/100)`,
|
|
261
|
+
`Products: ${outcome.bundle.summary.products.join(", ")}`,
|
|
262
|
+
`Records: ${outcome.records.length}`,
|
|
263
|
+
"",
|
|
264
|
+
"## Outputs",
|
|
265
|
+
...Object.entries(outcome.manifest.outputs)
|
|
266
|
+
.filter(([, value]) => (Array.isArray(value) ? value.length > 0 : Boolean(value)))
|
|
267
|
+
.map(([key, value]) => `- ${key}: ${Array.isArray(value) ? value.join(", ") : value}`),
|
|
268
|
+
"",
|
|
269
|
+
"## Corpus Summary",
|
|
270
|
+
renderCorpusSummary(summarizeCorpus(outcome.records)).trim(),
|
|
271
|
+
];
|
|
272
|
+
return `${lines.join("\n")}\n`;
|
|
273
|
+
}
|
|
274
|
+
async function runJobCommand(id, command, cwd, stdio) {
|
|
275
|
+
const startedAt = Date.now();
|
|
276
|
+
const exitCode = await new Promise((resolveExit, reject) => {
|
|
277
|
+
const child = spawn(command, { cwd, shell: true, stdio });
|
|
278
|
+
child.on("error", reject);
|
|
279
|
+
child.on("exit", resolveExit);
|
|
280
|
+
});
|
|
281
|
+
const durationMs = Date.now() - startedAt;
|
|
282
|
+
return {
|
|
283
|
+
id,
|
|
284
|
+
command,
|
|
285
|
+
durationMs,
|
|
286
|
+
exitCode: exitCode ?? undefined,
|
|
287
|
+
status: exitCode === 0 ? "passed" : "failed",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
function jobFor(profile, key) {
|
|
291
|
+
const explicit = profile.run?.jobs?.[key] ?? {};
|
|
292
|
+
return {
|
|
293
|
+
required: true,
|
|
294
|
+
...explicit,
|
|
295
|
+
artifact: explicit.artifact ?? profile.artifacts[key],
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function normalizeArtifact(key, raw, artifactPath) {
|
|
299
|
+
if (key === "mcpbench")
|
|
300
|
+
return normalizeMcpBenchResult(raw, artifactPath);
|
|
301
|
+
if (key === "tripwire")
|
|
302
|
+
return normalizeTripwireResult(raw, artifactPath);
|
|
303
|
+
return normalizeOnegentAuditPacket(raw, artifactPath);
|
|
304
|
+
}
|
|
305
|
+
function corpusStoreOptions(profile, cwd) {
|
|
306
|
+
const corpus = profile.run?.corpus ?? {};
|
|
307
|
+
return {
|
|
308
|
+
kind: parseCorpusStoreKind(corpus.store),
|
|
309
|
+
jsonlPath: corpus.path ? resolve(cwd, corpus.path) : resolve(cwd, ".agentcert/corpus/corpus.jsonl"),
|
|
310
|
+
sqlitePath: corpus.sqlitePath ? resolve(cwd, corpus.sqlitePath) : resolve(cwd, ".agentcert/corpus/agentcert.sqlite"),
|
|
311
|
+
databaseUrl: corpus.databaseUrl ?? process.env.AGENTCERT_DATABASE_URL ?? process.env.DATABASE_URL,
|
|
312
|
+
tableName: corpus.tableName,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function monitorOutputsFor(profile) {
|
|
316
|
+
const monitor = profile.run?.monitor;
|
|
317
|
+
return [...(monitor?.outputs ?? []), ...(monitor?.out ? [monitor.out] : [])];
|
|
318
|
+
}
|
|
319
|
+
function reviewedDatasetOutputsFor(profile) {
|
|
320
|
+
const dataset = profile.run?.dataset;
|
|
321
|
+
return [...(dataset?.reviewedOutputs ?? []), ...(dataset?.reviewedOut ? [dataset.reviewedOut] : [])];
|
|
322
|
+
}
|
|
323
|
+
function artifactPath(base, file) {
|
|
324
|
+
return normalizeArtifactPath(`${base.replace(/[\\/]+$/, "")}/${file}`);
|
|
325
|
+
}
|
|
326
|
+
function normalizeArtifactPath(path) {
|
|
327
|
+
return path.replace(/\\/g, "/");
|
|
328
|
+
}
|
|
329
|
+
async function readJson(path, cwd = process.cwd()) {
|
|
330
|
+
const fullPath = resolve(cwd, path);
|
|
331
|
+
await access(fullPath);
|
|
332
|
+
const raw = await readFile(fullPath, "utf8");
|
|
333
|
+
return JSON.parse(raw);
|
|
334
|
+
}
|
|
335
|
+
function stableRunId(profile, bundle) {
|
|
336
|
+
const hash = createHash("sha256")
|
|
337
|
+
.update(`${profile.subject.name}:${bundle.runId}:${JSON.stringify(bundle.summary)}`)
|
|
338
|
+
.digest("hex")
|
|
339
|
+
.slice(0, 12);
|
|
340
|
+
return `agentcert_run_${hash}`;
|
|
341
|
+
}
|