witnora 0.8.1 → 0.9.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.
Files changed (87) hide show
  1. package/README.md +275 -12
  2. package/dist/artifact-manifest.js +37 -0
  3. package/dist/artifact-validation.js +102 -0
  4. package/dist/badge.js +31 -0
  5. package/dist/browser-adapter.js +156 -0
  6. package/dist/bundle.js +100 -0
  7. package/dist/cli.js +971 -0
  8. package/dist/command-help.js +71 -0
  9. package/dist/companion-artifacts.js +170 -0
  10. package/dist/conformance.js +115 -0
  11. package/dist/control-plane.js +409 -0
  12. package/dist/corpus-governance.js +126 -0
  13. package/dist/corpus-store.js +239 -0
  14. package/dist/corpus.js +438 -0
  15. package/dist/credentials.js +127 -0
  16. package/dist/evidence-signing.js +85 -0
  17. package/dist/failure-review.js +188 -0
  18. package/dist/guided-setup.js +101 -0
  19. package/dist/index.js +16 -0
  20. package/dist/lab.js +323 -0
  21. package/dist/local-server.js +491 -0
  22. package/dist/monitor.js +100 -0
  23. package/dist/normalizers.js +265 -0
  24. package/dist/onboarding-templates.js +158 -0
  25. package/dist/release-gate.js +457 -0
  26. package/dist/report.js +165 -0
  27. package/dist/runner.js +349 -0
  28. package/dist/sandbox.js +424 -0
  29. package/dist/schema-validator.js +503 -0
  30. package/dist/try.js +135 -0
  31. package/dist/types.js +3 -0
  32. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
  33. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
  34. package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
  36. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
  37. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
  38. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
  39. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
  40. package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
  41. package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
  42. package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
  43. package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
  44. package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
  45. package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
  46. package/dist/vendor/onegent-runtime/policies.js +109 -0
  47. package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
  48. package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
  49. package/dist/vendor/onegent-runtime/risk.js +54 -0
  50. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
  51. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
  52. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
  53. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
  54. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
  55. package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
  56. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
  57. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
  58. package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
  59. package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
  60. package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
  61. package/dist/vendor/onegent-runtime/sdk.js +140 -0
  62. package/dist/vendor/onegent-runtime/service.d.ts +27 -0
  63. package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
  64. package/dist/vendor/onegent-runtime/service.js +423 -0
  65. package/dist/vendor/onegent-runtime/store.d.ts +16 -0
  66. package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
  67. package/dist/vendor/onegent-runtime/store.js +29 -0
  68. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
  69. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
  70. package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
  71. package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
  72. package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
  73. package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
  74. package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
  75. package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
  76. package/dist/vendor/onegent-runtime/trust-types.js +7 -0
  77. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
  78. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
  79. package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
  80. package/dist/vendor/onegent-runtime/types.d.ts +287 -0
  81. package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
  82. package/dist/vendor/onegent-runtime/types.js +1 -0
  83. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
  84. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
  85. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
  86. package/package.json +33 -13
  87. package/bin/witnora.js +0 -3
@@ -0,0 +1,409 @@
1
+ import { createHash } from "node:crypto";
2
+ import { serializeHostedEvidenceBundle } from "./artifact-manifest.js";
3
+ import { MAX_REPORTED_COMPANION_ARTIFACT_SKIPS, } from "./companion-artifacts.js";
4
+ export class ControlPlaneRequestError extends Error {
5
+ status;
6
+ code;
7
+ requestId;
8
+ recovery;
9
+ constructor(message, status, code, requestId, recovery) {
10
+ super(message);
11
+ this.status = status;
12
+ this.code = code;
13
+ this.requestId = requestId;
14
+ this.recovery = recovery;
15
+ this.name = "ControlPlaneRequestError";
16
+ }
17
+ }
18
+ export async function verifyControlPlaneConnection(options) {
19
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
20
+ const request = options.fetch ?? fetch;
21
+ try {
22
+ const overview = await requestJson(request, `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}/overview`, {
23
+ headers: { authorization: `Bearer ${options.apiKey}` },
24
+ });
25
+ return {
26
+ projectId: overview.projectId,
27
+ runs: overview.summary.runs ?? 0,
28
+ evidence: overview.summary.evidence ?? 0,
29
+ };
30
+ }
31
+ catch (error) {
32
+ if (error instanceof ControlPlaneRequestError && error.status === 401) {
33
+ throw new Error("AgentCert API key was rejected. Create a new project API key and try again.");
34
+ }
35
+ if (error instanceof ControlPlaneRequestError && error.status === 403) {
36
+ throw new Error(`AgentCert API key cannot access project ${options.projectId}. Check the project ID and key scope.`);
37
+ }
38
+ if (error instanceof ControlPlaneRequestError && error.status === 404) {
39
+ throw new Error(`AgentCert project ${options.projectId} was not found.`);
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ export async function pushEvidenceToControlPlane(options) {
45
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
46
+ if (!baseUrl || !options.projectId || !options.apiKey) {
47
+ throw new Error("baseUrl, projectId, and apiKey are required to push evidence.");
48
+ }
49
+ const request = options.fetch ?? fetch;
50
+ const projectUrl = `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}`;
51
+ const companionArtifacts = options.companionArtifacts ?? [];
52
+ const hostedEvidence = options.companionArtifacts === undefined && options.bundle.artifactManifest
53
+ ? {
54
+ bundle: options.bundle,
55
+ bytes: new TextEncoder().encode(`${JSON.stringify(options.bundle, null, 2)}\n`),
56
+ }
57
+ : serializeHostedEvidenceBundle(options.bundle, companionArtifacts);
58
+ const bundle = hostedEvidence.bundle;
59
+ const externalId = options.externalId ?? bundle.runId;
60
+ const kind = hostedRunKind(bundle);
61
+ const headers = { authorization: `Bearer ${options.apiKey}` };
62
+ const trace = hostedTrace(options.projectId, externalId);
63
+ const observationEvents = buildHostedObservationEvents(bundle, trace);
64
+ const run = await requestJson(request, `${projectUrl}/runs`, {
65
+ method: "POST",
66
+ headers: { ...headers, "content-type": "application/json" },
67
+ body: JSON.stringify({
68
+ externalId,
69
+ kind,
70
+ schemaVersion: bundle.schemaVersion,
71
+ startedAt: bundle.generatedAt,
72
+ traceId: trace.traceId,
73
+ rootSpanId: trace.rootSpanId,
74
+ metadata: {
75
+ subject: bundle.subject,
76
+ products: bundle.summary.products,
77
+ },
78
+ assurance: options.assurance,
79
+ }),
80
+ });
81
+ await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/events`, {
82
+ method: "POST",
83
+ headers: { ...headers, "content-type": "application/json" },
84
+ body: JSON.stringify({
85
+ events: observationEvents,
86
+ }),
87
+ });
88
+ const query = new URLSearchParams({
89
+ fileName: options.fileName ?? "agentcert-evidence.json",
90
+ kind: "evidence_bundle",
91
+ schemaVersion: bundle.schemaVersion,
92
+ runId: run.id,
93
+ });
94
+ const evidence = await requestJson(request, `${projectUrl}/evidence?${query}`, {
95
+ method: "POST",
96
+ headers: { ...headers, "content-type": "application/json" },
97
+ body: new Uint8Array(hostedEvidence.bytes).buffer,
98
+ });
99
+ const skippedArtifacts = options.skippedCompanionArtifacts ?? [];
100
+ let artifactBytesUploaded = 0;
101
+ for (const artifact of companionArtifacts) {
102
+ const artifactQuery = new URLSearchParams({
103
+ fileName: artifact.fileName,
104
+ kind: artifact.kind,
105
+ schemaVersion: bundle.schemaVersion,
106
+ runId: run.id,
107
+ sourcePath: artifact.sourcePath,
108
+ });
109
+ await requestJson(request, `${projectUrl}/evidence?${artifactQuery}`, {
110
+ method: "POST",
111
+ headers: { ...headers, "content-type": artifact.contentType },
112
+ body: new Uint8Array(artifact.bytes).buffer,
113
+ });
114
+ artifactBytesUploaded += artifact.bytes.byteLength;
115
+ }
116
+ await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/events`, {
117
+ method: "POST",
118
+ headers: { ...headers, "content-type": "application/json" },
119
+ body: JSON.stringify({
120
+ events: [{
121
+ sequence: observationEvents.length,
122
+ type: "agentcert.evidence.uploaded",
123
+ actor: "agentcert-cli",
124
+ occurredAt: bundle.generatedAt,
125
+ payload: {
126
+ evidenceId: evidence.id,
127
+ bundleSchemaVersion: bundle.schemaVersion,
128
+ uploadedCount: companionArtifacts.length,
129
+ skippedCount: skippedArtifacts.length,
130
+ uploadedBytes: artifactBytesUploaded,
131
+ skipped: skippedArtifacts
132
+ .slice(0, MAX_REPORTED_COMPANION_ARTIFACT_SKIPS)
133
+ .map(({ sourcePath, reason }) => ({ sourcePath, reason })),
134
+ skippedDetailsTruncated: skippedArtifacts.length > MAX_REPORTED_COMPANION_ARTIFACT_SKIPS,
135
+ },
136
+ traceId: trace.traceId,
137
+ spanId: stableSpanId(`${trace.traceId}:evidence-uploaded`),
138
+ parentSpanId: trace.rootSpanId,
139
+ }],
140
+ }),
141
+ });
142
+ const firstDivergence = bundle.evidence.find((item) => item.severity === "critical" || item.severity === "high")?.message;
143
+ await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/complete`, {
144
+ method: "POST",
145
+ headers: { ...headers, "content-type": "application/json" },
146
+ body: JSON.stringify({
147
+ status: bundle.verdict.passed ? "passed" : "failed",
148
+ score: bundle.verdict.score,
149
+ summary: `${bundle.subject.name}: ${bundle.verdict.level}`,
150
+ firstDivergence,
151
+ completedAt: bundle.generatedAt,
152
+ metadata: {
153
+ evidenceId: evidence.id,
154
+ evidenceSchemaVersion: bundle.schemaVersion,
155
+ artifactManifestVersion: bundle.artifactManifest?.schemaVersion,
156
+ companionArtifactsUploaded: companionArtifacts.length,
157
+ companionArtifactsSkipped: skippedArtifacts.length,
158
+ companionArtifactBytesUploaded: artifactBytesUploaded,
159
+ },
160
+ }),
161
+ });
162
+ const continuousAssuranceHealth = options.assurance && options.verifyContinuousAssurance
163
+ ? await loadContinuousAssuranceHealth({
164
+ baseUrl,
165
+ projectId: options.projectId,
166
+ apiKey: options.apiKey,
167
+ runId: run.id,
168
+ expectedCaseId: options.assurance.caseId,
169
+ expectedScopeFingerprintSha256: scopeFingerprint(options.assurance.scope),
170
+ fetch: request,
171
+ })
172
+ : undefined;
173
+ return {
174
+ runId: run.id,
175
+ evidenceId: evidence.id,
176
+ externalId,
177
+ artifactsUploaded: companionArtifacts.length,
178
+ artifactsSkipped: skippedArtifacts.length,
179
+ artifactBytesUploaded,
180
+ ...(continuousAssuranceHealth ? { continuousAssuranceHealth } : {}),
181
+ };
182
+ }
183
+ export async function loadContinuousAssuranceHealth(options) {
184
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
185
+ const request = options.fetch ?? fetch;
186
+ const analysis = await requestJson(request, `${baseUrl}/v1/projects/${encodeURIComponent(options.projectId)}/runs/${encodeURIComponent(options.runId)}/analysis`, { headers: { authorization: `Bearer ${options.apiKey}` } });
187
+ const run = object(analysis.run);
188
+ const metadata = object(run.metadata);
189
+ const binding = object(metadata.continuousAssurance);
190
+ const reconciliation = object(binding.reconciliation);
191
+ const completeness = object(analysis.evidenceCompleteness);
192
+ const manifest = object(completeness.reconciliation);
193
+ const runStatus = text(run.status) ?? "unknown";
194
+ const nextStatus = assuranceStatus(reconciliation.nextStatus);
195
+ const evidenceStatus = evidenceCompletenessStatus(completeness.status);
196
+ const diagnostics = [];
197
+ if (!text(binding.caseId))
198
+ diagnostics.push(diagnostic("assurance_binding_missing", "Hosted run has no continuous assurance binding.", "Regenerate the kit from an issued assurance case and rerun the workflow."));
199
+ if (options.expectedCaseId && binding.caseId !== options.expectedCaseId)
200
+ diagnostics.push(diagnostic("assurance_case_mismatch", "Hosted run is bound to a different assurance case.", "Use the unmodified generated workflow and case ID."));
201
+ if (options.expectedScopeFingerprintSha256 && binding.scopeFingerprintSha256 !== options.expectedScopeFingerprintSha256)
202
+ diagnostics.push(diagnostic("scope_fingerprint_mismatch", "Hosted scope fingerprint does not match the generated scope file.", "Restore the generated scope file or start a formal revalidation."));
203
+ if (reconciliation.authoritative !== true)
204
+ diagnostics.push(diagnostic("non_authoritative_trigger", "The Hosted reconciliation is prospective, not authoritative.", "Run the release or nightly workflow; pull requests cannot establish CURRENT."));
205
+ if (reconciliation.outcome !== "current" || nextStatus !== "CURRENT")
206
+ diagnostics.push(diagnostic("assurance_not_current", `Hosted reconciliation ended in ${nextStatus}.`, "Review changed scope components or failed tests, then revalidate before release."));
207
+ if (runStatus !== "passed")
208
+ diagnostics.push(diagnostic("run_not_passed", `Hosted run status is ${runStatus}.`, "Open the run trace, fix the first divergence, and rerun the same generated kit."));
209
+ if (evidenceStatus !== "complete")
210
+ diagnostics.push(diagnostic("evidence_incomplete", `Hosted evidence is ${evidenceStatus}.`, "Upload every declared companion artifact and verify manifest hashes before retrying."));
211
+ return {
212
+ schemaVersion: "agentcert.continuous_assurance_health.v0.1",
213
+ healthy: diagnostics.length === 0,
214
+ status: nextStatus,
215
+ checkedAt: new Date().toISOString(),
216
+ run: { id: text(run.id) ?? options.runId, externalId: text(run.externalId) ?? "unknown", status: runStatus },
217
+ assurance: {
218
+ caseId: text(binding.caseId),
219
+ trigger: assuranceTrigger(binding.trigger),
220
+ scopeFingerprintSha256: text(binding.scopeFingerprintSha256),
221
+ authoritative: boolean(reconciliation.authoritative),
222
+ outcome: text(reconciliation.outcome),
223
+ firstAuthoritativeCurrentAt: text(reconciliation.firstAuthoritativeCurrentAt),
224
+ timeToFirstCurrentMs: finiteNumber(reconciliation.timeToFirstCurrentMs),
225
+ },
226
+ evidence: {
227
+ status: evidenceStatus,
228
+ declared: finiteNumber(manifest.declared) ?? 0,
229
+ matched: finiteNumber(manifest.matched) ?? 0,
230
+ reasons: stringArray(completeness.reasons),
231
+ },
232
+ diagnostics,
233
+ };
234
+ }
235
+ export function requireContinuousAssuranceCurrent(health) {
236
+ if (health.healthy)
237
+ return;
238
+ const details = health.diagnostics.map((item) => `${item.code}: ${item.message} ${item.recovery}`).join("\n");
239
+ throw new Error(`Hosted continuous assurance did not reach CURRENT.\n${details}`);
240
+ }
241
+ function hostedRunKind(bundle) {
242
+ const products = new Set(bundle.summary.products);
243
+ if (products.size > 1)
244
+ return "release_gate";
245
+ if (products.has("mcpbench"))
246
+ return "mcpbench";
247
+ if (products.has("tripwire-ci"))
248
+ return "tripwire";
249
+ if (products.has("onegent-runtime"))
250
+ return "runtime";
251
+ return "custom";
252
+ }
253
+ function hostedTrace(projectId, externalId) {
254
+ const traceId = createHash("sha256").update(`agentcert:${projectId}:${externalId}`).digest("hex").slice(0, 32);
255
+ return { traceId, rootSpanId: stableSpanId(`${traceId}:root`) };
256
+ }
257
+ function buildHostedObservationEvents(bundle, trace) {
258
+ const events = [{
259
+ sequence: 0,
260
+ type: "agentcert.run.started",
261
+ actor: "agentcert-cli",
262
+ occurredAt: bundle.generatedAt,
263
+ payload: redactedPayload({ subject: bundle.subject, products: bundle.summary.products, schemaVersion: bundle.schemaVersion }),
264
+ traceId: trace.traceId,
265
+ spanId: trace.rootSpanId,
266
+ }];
267
+ for (const [resultIndex, result] of bundle.results.entries()) {
268
+ if (events.length >= 478)
269
+ break;
270
+ const resultSpanId = stableSpanId(`${trace.traceId}:result:${resultIndex}:${result.runId}`);
271
+ events.push({
272
+ sequence: events.length,
273
+ type: "agentcert.product.result",
274
+ actor: result.product,
275
+ occurredAt: result.timestamp || bundle.generatedAt,
276
+ payload: redactedPayload({ product: result.product, phase: result.phase, passed: result.passed, score: result.score, certLevel: result.certLevel, summary: result.summary, sourceRunId: result.runId }),
277
+ traceId: trace.traceId,
278
+ spanId: resultSpanId,
279
+ parentSpanId: trace.rootSpanId,
280
+ });
281
+ for (const [findingIndex, finding] of result.evidence.entries()) {
282
+ if (events.length >= 478)
283
+ break;
284
+ events.push({
285
+ sequence: events.length,
286
+ type: findingEventType(result.product, finding.kind),
287
+ actor: result.product,
288
+ occurredAt: result.timestamp || bundle.generatedAt,
289
+ payload: redactedPayload({
290
+ findingId: finding.id,
291
+ kind: finding.kind,
292
+ severity: finding.severity,
293
+ message: finding.message,
294
+ source: finding.source,
295
+ artifactPath: finding.artifactPath,
296
+ suggestedFix: finding.suggestedFix,
297
+ metadata: finding.metadata,
298
+ passed: !new Set(["critical", "high"]).has(finding.severity),
299
+ }),
300
+ traceId: trace.traceId,
301
+ spanId: stableSpanId(`${trace.traceId}:finding:${resultIndex}:${findingIndex}:${finding.id}`),
302
+ parentSpanId: resultSpanId,
303
+ });
304
+ }
305
+ }
306
+ events.push({
307
+ sequence: events.length,
308
+ type: "agentcert.run.evaluated",
309
+ actor: "agentcert-cli",
310
+ occurredAt: bundle.generatedAt,
311
+ payload: redactedPayload({ verdict: bundle.verdict, summary: bundle.summary, evidenceStrength: bundle.evidenceStrength?.level }),
312
+ traceId: trace.traceId,
313
+ spanId: stableSpanId(`${trace.traceId}:evaluated`),
314
+ parentSpanId: trace.rootSpanId,
315
+ });
316
+ return events;
317
+ }
318
+ function findingEventType(product, kind) {
319
+ if (product === "tripwire-ci")
320
+ return "tripwire.fault.assertion";
321
+ if (product === "mcpbench")
322
+ return /policy|permission|security|violation/i.test(kind) ? "mcpbench.policy.violation" : "mcpbench.assertion";
323
+ if (product === "onegent-runtime") {
324
+ if (/approval/i.test(kind))
325
+ return "onegent.approval.decision";
326
+ if (/outcome|verification/i.test(kind))
327
+ return "onegent.outcome.verification";
328
+ if (/policy|mandate|authorization/i.test(kind))
329
+ return "onegent.policy.decision";
330
+ return "onegent.action.observation";
331
+ }
332
+ return "agentcert.finding";
333
+ }
334
+ function stableSpanId(seed) {
335
+ return createHash("sha256").update(seed).digest("hex").slice(0, 16);
336
+ }
337
+ function redactedPayload(value, depth = 0) {
338
+ if (depth > 4)
339
+ return "[TRUNCATED]";
340
+ if (typeof value === "string")
341
+ return value.length > 1_000 ? `${value.slice(0, 1_000)}...[TRUNCATED]` : value;
342
+ if (typeof value === "number" || typeof value === "boolean" || value === null)
343
+ return value;
344
+ if (Array.isArray(value))
345
+ return value.slice(0, 50).map((item) => redactedPayload(item, depth + 1));
346
+ if (value && typeof value === "object") {
347
+ const result = {};
348
+ for (const [key, item] of Object.entries(value).slice(0, 50)) {
349
+ result[key] = /(authorization|api.?key|token|secret|password|credential)/i.test(key) ? "[REDACTED]" : redactedPayload(item, depth + 1);
350
+ }
351
+ return result;
352
+ }
353
+ return undefined;
354
+ }
355
+ function scopeFingerprint(scope) {
356
+ return createHash("sha256").update(canonicalJson(scope)).digest("hex");
357
+ }
358
+ function canonicalJson(value) {
359
+ if (Array.isArray(value))
360
+ return `[${value.map(canonicalJson).join(",")}]`;
361
+ if (value && typeof value === "object") {
362
+ const source = value;
363
+ return `{${Object.keys(source).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(source[key])}`).join(",")}}`;
364
+ }
365
+ return JSON.stringify(value) ?? "null";
366
+ }
367
+ function object(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }
368
+ function text(value) { return typeof value === "string" && value ? value : undefined; }
369
+ function boolean(value) { return typeof value === "boolean" ? value : undefined; }
370
+ function finiteNumber(value) { return typeof value === "number" && Number.isFinite(value) ? value : undefined; }
371
+ function stringArray(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; }
372
+ function assuranceStatus(value) {
373
+ return value === "CURRENT" || value === "REVALIDATION_REQUIRED" || value === "SUSPENDED" || value === "EXPIRED" ? value : "UNKNOWN";
374
+ }
375
+ function evidenceCompletenessStatus(value) {
376
+ return value === "complete" || value === "partial" || value === "rejected" ? value : "unknown";
377
+ }
378
+ function assuranceTrigger(value) {
379
+ return value === "pull_request" || value === "release" || value === "nightly" ? value : undefined;
380
+ }
381
+ function diagnostic(code, message, recovery) { return { code, message, recovery }; }
382
+ async function requestJson(request, url, init) {
383
+ let response;
384
+ try {
385
+ response = await request(url, { ...init, signal: init.signal ?? AbortSignal.timeout(30_000) });
386
+ }
387
+ catch (error) {
388
+ const message = error instanceof Error ? error.message : String(error);
389
+ throw new ControlPlaneRequestError(`AgentCert control plane request failed: ${message}`);
390
+ }
391
+ const text = await response.text();
392
+ let value = {};
393
+ if (text) {
394
+ try {
395
+ value = JSON.parse(text);
396
+ }
397
+ catch {
398
+ if (!response.ok)
399
+ throw new ControlPlaneRequestError(`AgentCert control plane returned HTTP ${response.status}.`, response.status);
400
+ throw new ControlPlaneRequestError("AgentCert control plane returned invalid JSON.", response.status);
401
+ }
402
+ }
403
+ if (!response.ok) {
404
+ throw new ControlPlaneRequestError([typeof value.error === "string" ? value.error : `AgentCert control plane returned HTTP ${response.status}.`,
405
+ typeof value.recovery === "string" ? value.recovery : undefined,
406
+ typeof value.requestId === "string" ? `Request ID: ${value.requestId}.` : undefined].filter(Boolean).join(" "), response.status, typeof value.code === "string" ? value.code : undefined, typeof value.requestId === "string" ? value.requestId : response.headers.get("x-request-id") ?? undefined, typeof value.recovery === "string" ? value.recovery : undefined);
407
+ }
408
+ return value;
409
+ }
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto";
2
+ import { basename } from "node:path";
3
+ const SECRET_PATTERNS = [
4
+ /\bsk-(?:proj-|live_|test_)?[A-Za-z0-9_-]{16,}\b/g,
5
+ /\brk_(?:live|test)_[A-Za-z0-9]{12,}\b/g,
6
+ /\bnpm_[A-Za-z0-9]{16,}\b/g,
7
+ /\bac_(?:live|test)_[A-Za-z0-9_-]{12,}\b/g,
8
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi,
9
+ ];
10
+ const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
11
+ export function governCorpusRecords(records, input) {
12
+ if (input.consent === "denied")
13
+ throw new Error("Corpus consent is denied; no record was retained.");
14
+ if (!input.consentSource.trim())
15
+ throw new Error("Corpus consent source is required.");
16
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
17
+ return records.map((record) => {
18
+ const [redacted, replacements] = redactValue(structuredClone(record), input.consent === "anonymous");
19
+ const governed = redacted;
20
+ if (input.consent === "anonymous") {
21
+ const identity = pseudonym(record.subject);
22
+ governed.subject = `anonymous-${identity}`;
23
+ governed.agentName = `anonymous-agent-${identity}`;
24
+ governed.sourcePath = basename(record.sourcePath.replaceAll("\\", "/"));
25
+ governed.artifacts = Object.fromEntries(Object.entries(governed.artifacts).map(([key, value]) => [key, basename(value.replaceAll("\\", "/"))]));
26
+ }
27
+ governed.governance = {
28
+ schemaVersion: "agentcert.corpus_governance.v0.1",
29
+ consent: input.consent,
30
+ consentSource: redactText(input.consentSource, input.consent === "anonymous")[0],
31
+ consentRecordedAt: recordedAt,
32
+ provenance: {
33
+ sourcePath: input.consent === "anonymous" ? basename(record.sourcePath.replaceAll("\\", "/")) : record.sourcePath,
34
+ sourceRecordSha256: createHash("sha256").update(JSON.stringify(record)).digest("hex"),
35
+ collectedAt: record.ingestedAt,
36
+ },
37
+ redaction: { policyVersion: "agentcert.redaction.v0.1", replacements, secretScanPassed: !containsSecret(JSON.stringify(governed)) },
38
+ };
39
+ if (!governed.governance.redaction.secretScanPassed)
40
+ throw new Error(`Corpus record ${record.id} still contains a recognized secret after redaction.`);
41
+ return governed;
42
+ });
43
+ }
44
+ export function exportGovernedCorpus(records, allowed = ["public", "anonymous"]) {
45
+ const allow = new Set(allowed);
46
+ return records
47
+ .filter((record) => record.governance && allow.has(record.governance.consent))
48
+ .map((record) => {
49
+ const [redacted, replacements] = redactValue(structuredClone(record), record.governance.consent === "anonymous");
50
+ const exported = redacted;
51
+ if (record.governance.consent === "anonymous") {
52
+ const identity = record.subject.startsWith("anonymous-") ? record.subject.slice("anonymous-".length) : pseudonym(record.subject);
53
+ exported.subject = `anonymous-${identity}`;
54
+ exported.agentName = `anonymous-agent-${identity}`;
55
+ exported.sourcePath = basename(record.sourcePath.replaceAll("\\", "/"));
56
+ exported.artifacts = Object.fromEntries(Object.entries(exported.artifacts).map(([key, value]) => [key, basename(value.replaceAll("\\", "/"))]));
57
+ }
58
+ exported.governance = {
59
+ ...record.governance,
60
+ redaction: {
61
+ ...record.governance.redaction,
62
+ replacements: record.governance.redaction.replacements + replacements,
63
+ secretScanPassed: !containsSecret(JSON.stringify(exported)),
64
+ },
65
+ };
66
+ if (!exported.governance.redaction.secretScanPassed)
67
+ throw new Error(`Corpus record ${record.id} failed the export secret scan.`);
68
+ return exported;
69
+ });
70
+ }
71
+ export function deleteCorpusRecords(records, ids, reason, deletedAt = new Date().toISOString()) {
72
+ if (!reason.trim())
73
+ throw new Error("Corpus deletion requires a reason.");
74
+ const requested = new Set(ids);
75
+ const deleted = records.filter((record) => requested.has(record.id));
76
+ return {
77
+ retained: records.filter((record) => !requested.has(record.id)),
78
+ tombstones: deleted.map((record) => ({
79
+ schemaVersion: "agentcert.corpus_deletion.v0.1",
80
+ recordIdSha256: createHash("sha256").update(record.id).digest("hex"),
81
+ reason: redactText(reason, true)[0],
82
+ deletedAt,
83
+ })),
84
+ };
85
+ }
86
+ export function parseCorpusConsent(value) {
87
+ const consent = value ?? "private";
88
+ if (consent === "private" || consent === "anonymous" || consent === "public" || consent === "denied")
89
+ return consent;
90
+ throw new Error(`Unsupported corpus consent "${consent}". Use private, anonymous, public, or denied.`);
91
+ }
92
+ function redactValue(value, anonymous) {
93
+ if (typeof value === "string")
94
+ return redactText(value, anonymous);
95
+ if (Array.isArray(value)) {
96
+ let replacements = 0;
97
+ const output = value.map((item) => { const [next, count] = redactValue(item, anonymous); replacements += count; return next; });
98
+ return [output, replacements];
99
+ }
100
+ if (value && typeof value === "object") {
101
+ let replacements = 0;
102
+ const output = {};
103
+ for (const [key, item] of Object.entries(value)) {
104
+ const [next, count] = redactValue(item, anonymous);
105
+ output[key] = next;
106
+ replacements += count;
107
+ }
108
+ return [output, replacements];
109
+ }
110
+ return [value, 0];
111
+ }
112
+ function redactText(value, anonymous) {
113
+ let output = value;
114
+ let replacements = 0;
115
+ for (const pattern of SECRET_PATTERNS)
116
+ output = output.replace(pattern, () => { replacements += 1; return "[REDACTED_SECRET]"; });
117
+ if (anonymous)
118
+ output = output.replace(EMAIL_PATTERN, () => { replacements += 1; return "[REDACTED_EMAIL]"; });
119
+ return [output, replacements];
120
+ }
121
+ function containsSecret(value) {
122
+ return SECRET_PATTERNS.some((pattern) => { pattern.lastIndex = 0; return pattern.test(value); });
123
+ }
124
+ function pseudonym(value) {
125
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
126
+ }