agentcert 0.6.0 → 0.6.1
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 +7 -1
- package/dist/cli.js +19 -2
- package/dist/command-help.js +2 -0
- package/dist/control-plane.js +231 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,9 @@ case by declaring the exact reviewed scope:
|
|
|
64
64
|
npx agentcert run --config agentcert.config.json --push \
|
|
65
65
|
--assurance-case "$AGENTCERT_ASSURANCE_CASE_ID" \
|
|
66
66
|
--assurance-scope agentcert.assurance-scope.json \
|
|
67
|
-
--assurance-trigger auto
|
|
67
|
+
--assurance-trigger auto \
|
|
68
|
+
--require-current auto \
|
|
69
|
+
--continuous-health-out .agentcert/canary/generated-kit-health.json
|
|
68
70
|
```
|
|
69
71
|
|
|
70
72
|
Validate the scope before CI uses it:
|
|
@@ -79,6 +81,10 @@ npx agentcert schema validate \
|
|
|
79
81
|
and other GitHub runs as release checks. Authoritative failure or scope drift
|
|
80
82
|
sets the Hosted contract to `REVALIDATION_REQUIRED`; only an independently
|
|
81
83
|
issued successor case establishes a new `CURRENT` baseline.
|
|
84
|
+
Release and nightly checks fail unless Hosted returns `CURRENT`. The optional
|
|
85
|
+
health output is redacted and captures the Hosted run/evidence identifiers,
|
|
86
|
+
evidence completeness, freshness transition, and install-to-CURRENT timing for
|
|
87
|
+
external canaries and operational dashboards.
|
|
82
88
|
|
|
83
89
|
Add `--push` to `agentcert run` to run locally and upload the resulting bundle
|
|
84
90
|
in one command. By default, both commands also upload local files referenced by
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +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 { deleteCorpusRecords, exportGovernedCorpus, governCorpusRecords, parseCorpusConsent } from "./corpus-governance.js";
|
|
12
|
-
import { pushEvidenceToControlPlane, verifyControlPlaneConnection } from "./control-plane.js";
|
|
12
|
+
import { pushEvidenceToControlPlane, requireContinuousAssuranceCurrent, verifyControlPlaneConnection } from "./control-plane.js";
|
|
13
13
|
import { runEvidenceConformance } from "./conformance.js";
|
|
14
14
|
import { DEFAULT_AGENTCERT_SERVER, resolveConnection, saveConnection } from "./credentials.js";
|
|
15
15
|
import { applyFailureReviews, appendFailureReview, createFailureReview, findFailurePattern, parseFailureReviewStatus, parseFailureType, parseReviewConfidence, readFailureReviews, } from "./failure-review.js";
|
|
@@ -601,6 +601,11 @@ async function pushHostedEvidence(bundle, bytes, fileName) {
|
|
|
601
601
|
const companions = readBoolFlag("--no-artifacts")
|
|
602
602
|
? undefined
|
|
603
603
|
: await collectCompanionArtifacts(bundle, resolve(readFlag("--artifact-root") ?? process.cwd()));
|
|
604
|
+
const assurance = await readContinuousAssuranceBinding();
|
|
605
|
+
const healthOut = readFlag("--continuous-health-out");
|
|
606
|
+
const requireCurrent = readBoolFlag("--require-current");
|
|
607
|
+
if (requireCurrent && !assurance)
|
|
608
|
+
throw new Error("--require-current needs --assurance-case and --assurance-scope.");
|
|
604
609
|
const result = await pushEvidenceToControlPlane({
|
|
605
610
|
baseUrl: connection.server,
|
|
606
611
|
projectId: connection.projectId,
|
|
@@ -611,7 +616,8 @@ async function pushHostedEvidence(bundle, bytes, fileName) {
|
|
|
611
616
|
externalId: readFlag("--external-id"),
|
|
612
617
|
companionArtifacts: companions?.artifacts,
|
|
613
618
|
skippedCompanionArtifacts: companions?.skipped,
|
|
614
|
-
assurance
|
|
619
|
+
assurance,
|
|
620
|
+
verifyContinuousAssurance: Boolean(assurance && (healthOut || requireCurrent)),
|
|
615
621
|
});
|
|
616
622
|
process.stdout.write(`Hosted run: ${result.runId}\nHosted evidence: ${result.evidenceId}\n`);
|
|
617
623
|
if (companions) {
|
|
@@ -623,6 +629,17 @@ async function pushHostedEvidence(bundle, bytes, fileName) {
|
|
|
623
629
|
process.stderr.write(`${companions.skipped.length - MAX_REPORTED_COMPANION_ARTIFACT_SKIPS} additional skipped artifacts omitted.\n`);
|
|
624
630
|
}
|
|
625
631
|
}
|
|
632
|
+
if (result.continuousAssuranceHealth) {
|
|
633
|
+
process.stdout.write(`Hosted assurance: ${result.continuousAssuranceHealth.status} (${result.continuousAssuranceHealth.healthy ? "healthy" : "attention required"})\n`);
|
|
634
|
+
if (healthOut) {
|
|
635
|
+
const outputPath = resolve(healthOut);
|
|
636
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
637
|
+
await writeFile(outputPath, `${JSON.stringify(result.continuousAssuranceHealth, null, 2)}\n`, "utf8");
|
|
638
|
+
process.stdout.write(`Continuous assurance health: ${outputPath}\n`);
|
|
639
|
+
}
|
|
640
|
+
if (requireCurrent)
|
|
641
|
+
requireContinuousAssuranceCurrent(result.continuousAssuranceHealth);
|
|
642
|
+
}
|
|
626
643
|
}
|
|
627
644
|
async function readContinuousAssuranceBinding() {
|
|
628
645
|
const caseId = readFlag("--assurance-case");
|
package/dist/command-help.js
CHANGED
|
@@ -37,6 +37,8 @@ Options:
|
|
|
37
37
|
--assurance-case <id> Issued assurance case to reconcile
|
|
38
38
|
--assurance-scope <p> Declared agent/model/prompt/tools/policy/suite scope JSON
|
|
39
39
|
--assurance-trigger <t> auto, pull_request, release, or nightly (default: auto)
|
|
40
|
+
--require-current Fail unless Hosted confirms authoritative CURRENT and complete evidence
|
|
41
|
+
--continuous-health-out <path> Write the redacted Hosted health contract
|
|
40
42
|
--artifact-root <dir> Allowed root for companion artifacts (default: current directory)
|
|
41
43
|
--no-artifacts Upload only the evidence bundle
|
|
42
44
|
--help, -h Show this help
|
package/dist/control-plane.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { serializeHostedEvidenceBundle } from "./artifact-manifest.js";
|
|
2
3
|
import { MAX_REPORTED_COMPANION_ARTIFACT_SKIPS, } from "./companion-artifacts.js";
|
|
3
4
|
export class ControlPlaneRequestError extends Error {
|
|
@@ -58,6 +59,8 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
58
59
|
const externalId = options.externalId ?? bundle.runId;
|
|
59
60
|
const kind = hostedRunKind(bundle);
|
|
60
61
|
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
62
|
+
const trace = hostedTrace(options.projectId, externalId);
|
|
63
|
+
const observationEvents = buildHostedObservationEvents(bundle, trace);
|
|
61
64
|
const run = await requestJson(request, `${projectUrl}/runs`, {
|
|
62
65
|
method: "POST",
|
|
63
66
|
headers: { ...headers, "content-type": "application/json" },
|
|
@@ -66,6 +69,8 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
66
69
|
kind,
|
|
67
70
|
schemaVersion: bundle.schemaVersion,
|
|
68
71
|
startedAt: bundle.generatedAt,
|
|
72
|
+
traceId: trace.traceId,
|
|
73
|
+
rootSpanId: trace.rootSpanId,
|
|
69
74
|
metadata: {
|
|
70
75
|
subject: bundle.subject,
|
|
71
76
|
products: bundle.summary.products,
|
|
@@ -77,18 +82,7 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
77
82
|
method: "POST",
|
|
78
83
|
headers: { ...headers, "content-type": "application/json" },
|
|
79
84
|
body: JSON.stringify({
|
|
80
|
-
events:
|
|
81
|
-
sequence: 0,
|
|
82
|
-
type: "agentcert.evidence.created",
|
|
83
|
-
actor: "agentcert-cli",
|
|
84
|
-
occurredAt: bundle.generatedAt,
|
|
85
|
-
payload: {
|
|
86
|
-
verdict: bundle.verdict,
|
|
87
|
-
totalEvidence: bundle.summary.totalEvidence,
|
|
88
|
-
criticalEvidence: bundle.summary.criticalEvidence,
|
|
89
|
-
highEvidence: bundle.summary.highEvidence,
|
|
90
|
-
},
|
|
91
|
-
}],
|
|
85
|
+
events: observationEvents,
|
|
92
86
|
}),
|
|
93
87
|
});
|
|
94
88
|
const query = new URLSearchParams({
|
|
@@ -119,29 +113,32 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
119
113
|
});
|
|
120
114
|
artifactBytesUploaded += artifact.bytes.byteLength;
|
|
121
115
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
});
|
|
145
142
|
const firstDivergence = bundle.evidence.find((item) => item.severity === "critical" || item.severity === "high")?.message;
|
|
146
143
|
await requestJson(request, `${projectUrl}/runs/${encodeURIComponent(run.id)}/complete`, {
|
|
147
144
|
method: "POST",
|
|
@@ -162,6 +159,17 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
162
159
|
},
|
|
163
160
|
}),
|
|
164
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;
|
|
165
173
|
return {
|
|
166
174
|
runId: run.id,
|
|
167
175
|
evidenceId: evidence.id,
|
|
@@ -169,8 +177,67 @@ export async function pushEvidenceToControlPlane(options) {
|
|
|
169
177
|
artifactsUploaded: companionArtifacts.length,
|
|
170
178
|
artifactsSkipped: skippedArtifacts.length,
|
|
171
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,
|
|
172
233
|
};
|
|
173
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
|
+
}
|
|
174
241
|
function hostedRunKind(bundle) {
|
|
175
242
|
const products = new Set(bundle.summary.products);
|
|
176
243
|
if (products.size > 1)
|
|
@@ -183,6 +250,135 @@ function hostedRunKind(bundle) {
|
|
|
183
250
|
return "runtime";
|
|
184
251
|
return "custom";
|
|
185
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 }; }
|
|
186
382
|
async function requestJson(request, url, init) {
|
|
187
383
|
let response;
|
|
188
384
|
try {
|