@nexart/ai-execution 0.6.0 → 0.8.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 +294 -10
- package/dist/index.cjs +401 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +303 -3
- package/dist/index.d.ts +303 -3
- package/dist/index.mjs +392 -2
- package/dist/index.mjs.map +1 -1
- package/dist/providers/anthropic.cjs +7 -1
- package/dist/providers/anthropic.cjs.map +1 -1
- package/dist/providers/anthropic.d.cts +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.mjs +7 -1
- package/dist/providers/anthropic.mjs.map +1 -1
- package/dist/providers/openai.cjs +7 -1
- package/dist/providers/openai.cjs.map +1 -1
- package/dist/providers/openai.d.cts +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.mjs +7 -1
- package/dist/providers/openai.mjs.map +1 -1
- package/dist/providers/wrap.cjs +7 -1
- package/dist/providers/wrap.cjs.map +1 -1
- package/dist/providers/wrap.d.cts +1 -1
- package/dist/providers/wrap.d.ts +1 -1
- package/dist/providers/wrap.mjs +7 -1
- package/dist/providers/wrap.mjs.map +1 -1
- package/dist/{types-Cgb52dTx.d.cts → types-CcqCDPrD.d.cts} +68 -1
- package/dist/{types-Cgb52dTx.d.ts → types-CcqCDPrD.d.ts} +68 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @nexart/ai-execution v0.
|
|
1
|
+
# @nexart/ai-execution v0.8.0
|
|
2
2
|
|
|
3
3
|
Tamper-evident records and Certified Execution Records (CER) for AI operations.
|
|
4
4
|
|
|
@@ -7,12 +7,12 @@ Tamper-evident records and Certified Execution Records (CER) for AI operations.
|
|
|
7
7
|
| Component | Version |
|
|
8
8
|
|---|---|
|
|
9
9
|
| Service | — |
|
|
10
|
-
| SDK | 0.
|
|
10
|
+
| SDK | 0.8.0 |
|
|
11
11
|
| Protocol | 1.2.0 |
|
|
12
12
|
|
|
13
13
|
## Why Not Just Store Logs?
|
|
14
14
|
|
|
15
|
-
Logs tell you what happened. CERs prove integrity. A log entry can be edited, truncated, or fabricated after the fact with no way to detect it. A CER bundle is cryptographically sealed: any modification —
|
|
15
|
+
Logs tell you what happened. CERs prove integrity. A log entry can be edited, truncated, or fabricated after the fact with no way to detect it. A CER bundle is cryptographically sealed: any modification to hashed fields — including input, output, parameters, and any recorded chain/tool evidence — invalidates the certificate hash. If you need to demonstrate to an auditor, regulator, or downstream system that a recorded execution has not been modified post-hoc, logs are insufficient. CERs provide the tamper-evident chain of custody that logs cannot. **CERs certify records, not model determinism or provider execution.**
|
|
16
16
|
|
|
17
17
|
## What This Does
|
|
18
18
|
|
|
@@ -105,7 +105,7 @@ const summary = run.finalize();
|
|
|
105
105
|
// { runId, stepCount: 2, steps: [...], finalStepHash: "sha256:..." }
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
### Attest to
|
|
108
|
+
### Attest to NexArt Attestation Node (optional)
|
|
109
109
|
|
|
110
110
|
```typescript
|
|
111
111
|
import { certifyDecision, attest } from '@nexart/ai-execution';
|
|
@@ -118,7 +118,7 @@ const proof = await attest(cer, {
|
|
|
118
118
|
console.log(proof.attestationId);
|
|
119
119
|
```
|
|
120
120
|
|
|
121
|
-
Attestation verifies internal integrity only. It does not re-run the model. The node confirms the bundle's hashes are consistent and
|
|
121
|
+
Attestation verifies internal integrity only. It does not re-run the model. The node confirms the bundle's hashes are consistent and returns an independently verifiable signed receipt (when the node is configured for signing).
|
|
122
122
|
|
|
123
123
|
### Archive (Export / Import)
|
|
124
124
|
|
|
@@ -147,7 +147,7 @@ const restored = importCer(json); // parse + verify (throws on tamper)
|
|
|
147
147
|
| `modelVersion` | Optional | `string \| null` | Defaults to `null` |
|
|
148
148
|
| `parameters.topP` | Optional | `number \| null` | Defaults to `null` |
|
|
149
149
|
| `parameters.seed` | Optional | `number \| null` | Defaults to `null` |
|
|
150
|
-
| `sdkVersion` | Optional | `string \| null` | Defaults to `"0.
|
|
150
|
+
| `sdkVersion` | Optional | `string \| null` | Defaults to `"0.8.0"` |
|
|
151
151
|
| `appId` | Optional | `string \| null` | Defaults to `null` |
|
|
152
152
|
| `runId` | Optional | `string \| null` | Workflow run ID |
|
|
153
153
|
| `stepId` | Optional | `string \| null` | Step identifier within a run |
|
|
@@ -155,6 +155,7 @@ const restored = importCer(json); // parse + verify (throws on tamper)
|
|
|
155
155
|
| `workflowId` | Optional | `string \| null` | Workflow template ID |
|
|
156
156
|
| `conversationId` | Optional | `string \| null` | Conversation/session ID |
|
|
157
157
|
| `prevStepHash` | Optional | `string \| null` | certificateHash of previous step |
|
|
158
|
+
| `toolCalls` | Optional | `ToolEvent[]` | v0.7.0+ tool/dependency evidence records. **Included in `certificateHash` when present.** See Level 4 section. |
|
|
158
159
|
|
|
159
160
|
Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `protocolVersion`, `executionSurface`, `inputHash`, `outputHash`.
|
|
160
161
|
|
|
@@ -166,14 +167,39 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
|
|
|
166
167
|
"certificateHash": "sha256:...",
|
|
167
168
|
"createdAt": "2026-02-12T00:00:00.000Z",
|
|
168
169
|
"version": "0.1",
|
|
169
|
-
"snapshot": { ... },
|
|
170
|
-
"meta": { "source": "my-app", "tags": ["production"] }
|
|
170
|
+
"snapshot": { "..." : "..." },
|
|
171
|
+
"meta": { "source": "my-app", "tags": ["production"] },
|
|
172
|
+
"declaration": {
|
|
173
|
+
"stabilitySchemeId": "nexart-cer-v1",
|
|
174
|
+
"protectedSetId": "ai.execution.v1.full",
|
|
175
|
+
"protectedFields": ["snapshot", "bundleType", "version", "createdAt"],
|
|
176
|
+
"notes": "optional free text"
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
```
|
|
173
180
|
|
|
174
181
|
### Certificate Hash Computation
|
|
175
182
|
|
|
176
|
-
The `certificateHash` is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: `{ bundleType, version, createdAt, snapshot }`. `meta`
|
|
183
|
+
The `certificateHash` is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: `{ bundleType, version, createdAt, snapshot }`. `meta` and `declaration` are excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
|
|
184
|
+
|
|
185
|
+
### Declaration Block (v0.7.0+)
|
|
186
|
+
|
|
187
|
+
The optional `declaration` field is a self-describing metadata block for AIEF-02 conformance. It carries `stabilitySchemeId`, `protectedSetId`, and `protectedFields` so that verifiers can confirm which fields are covered by the certificate hash without side-channel knowledge.
|
|
188
|
+
|
|
189
|
+
**`declaration` is excluded from `certificateHash` by design.** It is purely informational — mutating it does not invalidate the bundle. Pass it via `sealCer(snapshot, { declaration: { ... } })`.
|
|
190
|
+
|
|
191
|
+
Verifiers MUST treat `declaration` as advisory. For `cer.ai.execution.v1`, the protected set is defined by the bundleType semantics: `{ bundleType, version, createdAt, snapshot }` is what is hashed.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const bundle = sealCer(snapshot, {
|
|
195
|
+
declaration: {
|
|
196
|
+
stabilitySchemeId: 'nexart-cer-v1',
|
|
197
|
+
protectedSetId: 'ai.execution.v1.full',
|
|
198
|
+
protectedFields: ['snapshot', 'bundleType', 'version', 'createdAt'],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
// verifyCer(bundle).ok === true — declaration does not affect the result
|
|
202
|
+
```
|
|
177
203
|
|
|
178
204
|
## Attestation
|
|
179
205
|
|
|
@@ -285,6 +311,243 @@ const result = await verifyNodeReceiptSignature({
|
|
|
285
311
|
|
|
286
312
|
**Skip re-attestation:** use `hasAttestation(bundle)` to check if a bundle already includes attestation fields before calling `attest()` again.
|
|
287
313
|
|
|
314
|
+
## AIEF Interop (v0.7.0+)
|
|
315
|
+
|
|
316
|
+
`verifyAief(bundle)` is an adapter over the existing `verifyCer()` / `verify()`. It returns the exact output shape required by AIEF §9.1 for cross-vendor verifier interoperability. The internal `verify()` return value is unchanged — `verifyAief` is purely additive.
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { verifyAief } from '@nexart/ai-execution';
|
|
320
|
+
|
|
321
|
+
const result = verifyAief(bundle);
|
|
322
|
+
// {
|
|
323
|
+
// result: 'PASS' | 'FAIL',
|
|
324
|
+
// reason: string | null, // null on PASS; AIEF §9.2 reason string on FAIL
|
|
325
|
+
// checks: {
|
|
326
|
+
// schemaSupported: boolean, // false if SCHEMA_ERROR or CANONICALIZATION_ERROR
|
|
327
|
+
// integrityValid: boolean, // false if any hash mismatch
|
|
328
|
+
// protectedSetValid: boolean, // false if any hash mismatch (mirrors integrityValid for v1)
|
|
329
|
+
// chainValid: boolean, // false only if CHAIN_BREAK_DETECTED
|
|
330
|
+
// },
|
|
331
|
+
// notes?: string[], // failure detail strings when available
|
|
332
|
+
// }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**What we map / what we don't rename:**
|
|
336
|
+
|
|
337
|
+
| NexArt `CerVerifyCode` | AIEF `reason` |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `OK` | `ok` (reason is `null`) |
|
|
340
|
+
| `CERTIFICATE_HASH_MISMATCH` | `integrityProofMismatch` |
|
|
341
|
+
| `INPUT_HASH_MISMATCH` | `integrityProofMismatch` |
|
|
342
|
+
| `OUTPUT_HASH_MISMATCH` | `integrityProofMismatch` |
|
|
343
|
+
| `TOOL_OUTPUT_HASH_MISMATCH` | `integrityProofMismatch` |
|
|
344
|
+
| `SCHEMA_ERROR` | `unsupportedSchema` |
|
|
345
|
+
| `CANONICALIZATION_ERROR` | `malformedArtifact` |
|
|
346
|
+
| `INVALID_SHA256_FORMAT` | `malformedArtifact` |
|
|
347
|
+
| `UNKNOWN_ERROR` | `malformedArtifact` |
|
|
348
|
+
| `INCOMPLETE_ARTIFACT` | `incompleteArtifact` |
|
|
349
|
+
| `TOOL_EVIDENCE_MISSING` | `incompleteArtifact` |
|
|
350
|
+
| `CHAIN_BREAK_DETECTED` | `chainBreakDetected` |
|
|
351
|
+
| `VERIFICATION_MATERIAL_UNAVAILABLE` | `verificationMaterialUnavailable` |
|
|
352
|
+
| `ATTESTATION_MISSING` | `verificationMaterialUnavailable` |
|
|
353
|
+
| `ATTESTATION_KEY_NOT_FOUND` | `verificationMaterialUnavailable` |
|
|
354
|
+
| `ATTESTATION_KEY_FORMAT_UNSUPPORTED` | `verificationMaterialUnavailable` |
|
|
355
|
+
| `ATTESTATION_INVALID_SIGNATURE` | `signatureInvalid` |
|
|
356
|
+
|
|
357
|
+
The AIEF reason strings are not renamed or mapped to NexArt-specific vocabulary — they are passed through verbatim for AIEF conformance. Per AIEF §9.0 rule #7, `chainValid` is `true` when chain fields are absent from the snapshot.
|
|
358
|
+
|
|
359
|
+
`mapToAiefReason(code: string): string` is also exported if you need to convert a `CerVerifyCode` to an AIEF reason string directly. Unknown codes fall back to `"malformedArtifact"`.
|
|
360
|
+
|
|
361
|
+
## Level 4 (Optional): Chain + Tool Evidence (v0.7.0+)
|
|
362
|
+
|
|
363
|
+
> **If you do nothing, nothing changes.** All Level 4 features are additive and opt-in. Existing bundles without `toolCalls` verify identically.
|
|
364
|
+
|
|
365
|
+
### Tool Calls
|
|
366
|
+
|
|
367
|
+
Add a `toolCalls` array to the snapshot to record evidence of external tool or dependency invocations. When present, `toolCalls` is included in the `certificateHash` computation — tool evidence is part of the sealed record.
|
|
368
|
+
|
|
369
|
+
`toolCalls` provide tamper-evident evidence of what was recorded about a tool/dependency call. They do not prove the external tool actually executed unless the tool itself provides independent verifiable proof (e.g., its own signed receipt).
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { makeToolEvent, createSnapshot, sealCer } from '@nexart/ai-execution';
|
|
373
|
+
|
|
374
|
+
const webResult = await fetch('https://api.example.com/data');
|
|
375
|
+
const data = await webResult.json();
|
|
376
|
+
|
|
377
|
+
const toolEvent = makeToolEvent({
|
|
378
|
+
toolId: 'web-search',
|
|
379
|
+
output: data, // hashed automatically
|
|
380
|
+
input: { query: 'Q1 revenue' }, // optional; hashed if provided
|
|
381
|
+
evidenceRef: 'https://api.example.com/data', // optional URL/ID
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const snapshot = createSnapshot({
|
|
385
|
+
// ...
|
|
386
|
+
toolCalls: [toolEvent],
|
|
387
|
+
});
|
|
388
|
+
const bundle = sealCer(snapshot);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
`ToolEvent` shape:
|
|
392
|
+
|
|
393
|
+
| Field | Required | Description |
|
|
394
|
+
|---|---|---|
|
|
395
|
+
| `toolId` | **Yes** | Identifier of the tool/dependency called |
|
|
396
|
+
| `at` | **Yes** | ISO 8601 timestamp (defaults to `new Date()` in `makeToolEvent`) |
|
|
397
|
+
| `outputHash` | **Yes** | `sha256:<64hex>` of the tool output |
|
|
398
|
+
| `inputHash` | No | `sha256:<64hex>` of the tool input (optional) |
|
|
399
|
+
| `evidenceRef` | No | URL or external ID pointing to the raw evidence |
|
|
400
|
+
| `error` | No | Error message if the tool call failed |
|
|
401
|
+
|
|
402
|
+
`hashToolOutput(value)` hashes a tool output value: strings → SHA-256 of UTF-8 bytes; anything else → SHA-256 of canonical JSON bytes.
|
|
403
|
+
|
|
404
|
+
### Chain Verification
|
|
405
|
+
|
|
406
|
+
`verifyRunSummary(summary, bundles, opts?)` validates that a `RunBuilder` multi-step run forms an unbroken cryptographic chain. It detects insertion, deletion, and reordering of steps.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { RunBuilder, verifyRunSummary } from '@nexart/ai-execution';
|
|
410
|
+
|
|
411
|
+
const run = new RunBuilder({ runId: 'my-run' });
|
|
412
|
+
run.step({ /* step 0 */ });
|
|
413
|
+
run.step({ /* step 1 */ });
|
|
414
|
+
|
|
415
|
+
const summary = run.finalize();
|
|
416
|
+
const bundles = run.getBundles(); // or retrieve from storage
|
|
417
|
+
|
|
418
|
+
const result = verifyRunSummary(summary, bundles);
|
|
419
|
+
// { ok: boolean, code: CerVerifyCode, errors: string[], breakAt?: number }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`verifyRunSummary` returns `RunSummaryVerifyResult`:
|
|
423
|
+
- `ok: true` — full chain is valid
|
|
424
|
+
- `ok: false` with `INCOMPLETE_ARTIFACT` — step count mismatch
|
|
425
|
+
- `ok: false` with `CHAIN_BREAK_DETECTED` — stepIndex/prevStepHash/certificateHash mismatch; `breakAt` is the index of the first broken link
|
|
426
|
+
|
|
427
|
+
### Profiles (Opt-in Strictness)
|
|
428
|
+
|
|
429
|
+
`validateProfile(target, profile)` applies extra field-presence checks at creation time. It never affects `certificateHash` or `verifyCer()`.
|
|
430
|
+
|
|
431
|
+
| Profile | What it enforces |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `'flexible'` | No extra validation (default SDK behaviour) |
|
|
434
|
+
| `'AIEF_L2'` | AIEF-01 required fields: `executionId`, `timestamp`, `provider`, `model`, `input`, `output`, `inputHash`, `outputHash` |
|
|
435
|
+
| `'AIEF_L3'` | Same as `AIEF_L2` |
|
|
436
|
+
| `'AIEF_L4'` | AIEF_L3 + validates each `ToolEvent` in `toolCalls`; requires `prevStepHash` when `stepIndex > 0` |
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
import { validateProfile } from '@nexart/ai-execution';
|
|
440
|
+
|
|
441
|
+
const result = validateProfile(bundle, 'AIEF_L4');
|
|
442
|
+
// { ok: boolean, errors: string[] }
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
## Opinionated Run Helper
|
|
446
|
+
|
|
447
|
+
`certifyAndAttestRun(steps, options?)` combines RunBuilder step creation, optional per-step attestation, and finalization into a single call. It does not change `RunBuilder` semantics — it creates an internal RunBuilder and returns all artifacts together.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { certifyAndAttestRun, verifyRunSummary, attest } from '@nexart/ai-execution';
|
|
451
|
+
|
|
452
|
+
const { runSummary, stepBundles, receipts, finalStepHash } =
|
|
453
|
+
await certifyAndAttestRun(
|
|
454
|
+
[step0Params, step1Params, step2Params],
|
|
455
|
+
{
|
|
456
|
+
runId: 'analysis-run',
|
|
457
|
+
workflowId: 'data-pipeline',
|
|
458
|
+
// Optional: attest each step immediately after sealing
|
|
459
|
+
attestStep: (bundle) => attest(bundle, { nodeUrl, apiKey }),
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
verifyRunSummary(runSummary, stepBundles); // { ok: true }
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Return shape:**
|
|
467
|
+
|
|
468
|
+
| Field | Type | Description |
|
|
469
|
+
|---|---|---|
|
|
470
|
+
| `runSummary` | `RunSummary` | From `RunBuilder.finalize()` — stepCount, steps, finalStepHash |
|
|
471
|
+
| `stepBundles` | `CerAiExecutionBundle[]` | Sealed bundles in step order (index 0 = step 0) |
|
|
472
|
+
| `receipts` | `(AttestationReceipt \| null)[]` | Attestation receipts in step order; `null` if `attestStep` was not provided |
|
|
473
|
+
| `finalStepHash` | `string \| null` | Alias for `runSummary.finalStepHash` |
|
|
474
|
+
|
|
475
|
+
**Testing without network:** inject a mock `attestStep` to test the full flow without hitting a node:
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
const { runSummary, stepBundles, receipts } = await certifyAndAttestRun(steps, {
|
|
479
|
+
attestStep: async (bundle) => ({
|
|
480
|
+
attestationId: 'mock-' + bundle.certificateHash.slice(7, 15),
|
|
481
|
+
certificateHash: bundle.certificateHash,
|
|
482
|
+
nodeRuntimeHash: 'sha256:' + 'a'.repeat(64),
|
|
483
|
+
protocolVersion: '1.2.0',
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
## Redaction Semantics (v0.7.0+)
|
|
489
|
+
|
|
490
|
+
### Pre-seal verifiable redaction
|
|
491
|
+
|
|
492
|
+
`redactBeforeSeal(snapshot, policy)` replaces sensitive snapshot fields with stable envelopes **before** sealing. Because the `certificateHash` is computed over the already-redacted snapshot, the resulting bundle passes `verifyCer()` unchanged.
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { redactBeforeSeal, sealCer, verify } from '@nexart/ai-execution';
|
|
496
|
+
|
|
497
|
+
const redacted = redactBeforeSeal(snapshot, { paths: ['input', 'output'] });
|
|
498
|
+
const bundle = sealCer(redacted);
|
|
499
|
+
verify(bundle).ok; // true — hash matches the redacted snapshot
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Each redacted field becomes `{ _redacted: true, hash: "sha256:..." }` where `hash` is the SHA-256 of the original value. This lets authorized reviewers confirm what was there without accessing the raw content.
|
|
503
|
+
|
|
504
|
+
**Supported fields for pre-seal redaction:**
|
|
505
|
+
|
|
506
|
+
| Field | Supported | Notes |
|
|
507
|
+
|---|---|---|
|
|
508
|
+
| `input` | **Yes** | `inputHash` is recomputed from the envelope |
|
|
509
|
+
| `output` | **Yes** | `outputHash` is recomputed from the envelope |
|
|
510
|
+
| `toolCalls[n].output` *(via `ToolEvent.outputHash`)* | **Yes** | The `outputHash` already stores only the hash — the raw tool output need not be in the bundle at all |
|
|
511
|
+
| `prompt` and other schema-validated strings | **No** | `verifySnapshot` validates these as non-empty strings. Replacing with an object envelope causes `verifyCer()` to return `SCHEMA_ERROR`. |
|
|
512
|
+
|
|
513
|
+
### Verifiable redacted export (post-seal, new bundle)
|
|
514
|
+
|
|
515
|
+
`exportVerifiableRedacted(bundle, policy, options?)` is the right choice when you already have a sealed bundle and want to share a sanitized version that is still independently verifiable. It:
|
|
516
|
+
|
|
517
|
+
1. Applies `redactBeforeSeal()` to the original snapshot
|
|
518
|
+
2. Re-seals the redacted snapshot into a **new** bundle with a **new** `certificateHash`
|
|
519
|
+
3. Stores the original `certificateHash` in `meta.provenance.originalCertificateHash` as an informational cross-reference
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { certifyDecision, exportVerifiableRedacted, verify } from '@nexart/ai-execution';
|
|
523
|
+
|
|
524
|
+
const original = certifyDecision({ ... });
|
|
525
|
+
|
|
526
|
+
const { bundle, originalCertificateHash } = exportVerifiableRedacted(
|
|
527
|
+
original,
|
|
528
|
+
{ paths: ['input', 'output'] },
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
verify(bundle).ok; // true — new bundle verifies
|
|
532
|
+
bundle.certificateHash !== original.certificateHash; // true — different hash
|
|
533
|
+
bundle.meta.provenance.originalCertificateHash; // 'sha256:...' — reference only
|
|
534
|
+
bundle.snapshot.input; // { _redacted: true, hash: 'sha256:...' }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Provenance semantics:** `meta.provenance.originalCertificateHash` is reference metadata only. It is not part of the new `certificateHash` computation. Recipients of the new bundle cannot verify the original bundle's integrity from it — they can only confirm what hash the original had. If you need to prove the original's integrity, keep the original bundle available alongside the redacted one.
|
|
538
|
+
|
|
539
|
+
**Rule of thumb for choosing a redaction approach:**
|
|
540
|
+
|
|
541
|
+
| Approach | `verify()` passes | Original hash preserved | Notes |
|
|
542
|
+
|---|---|---|---|
|
|
543
|
+
| `redactBeforeSeal(snapshot, policy)` + `sealCer()` | ✅ | N/A — no original exists yet | Use when building a redacted bundle from scratch |
|
|
544
|
+
| `exportVerifiableRedacted(bundle, policy)` | ✅ | Reference only in `meta.provenance` | Use when you have a sealed bundle and need a shareable sanitized copy |
|
|
545
|
+
| `sanitizeForStorage(bundle, options)` | ❌ | N/A — hash broken by design | Use only for storage; do not pass result to `verify()` |
|
|
546
|
+
|
|
547
|
+
### Post-hoc redaction breaks integrity — by design
|
|
548
|
+
|
|
549
|
+
`sanitizeForStorage(bundle, options)` can replace arbitrary paths with a string placeholder, but it does **not** recompute any content hashes. This means `verifyCer()` will return `CERTIFICATE_HASH_MISMATCH` on the result. This is intentional: post-hoc redaction is a lossy, storage-only operation. If you need a verifiable record after storage-side redaction, use `exportVerifiableRedacted` instead.
|
|
550
|
+
|
|
288
551
|
## Canonical JSON Constraints
|
|
289
552
|
|
|
290
553
|
1. Object keys sorted lexicographically (Unicode codepoint order) at every nesting level.
|
|
@@ -326,6 +589,20 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
|
|
|
326
589
|
| Export | Description |
|
|
327
590
|
|---|---|
|
|
328
591
|
| `RunBuilder` | Multi-step workflow builder with prevStepHash chaining |
|
|
592
|
+
| `verifyRunSummary(summary, bundles, opts?)` | Verify that a RunSummary + step bundles form an unbroken cryptographic chain (v0.7.0+) |
|
|
593
|
+
| `certifyAndAttestRun(steps, options?)` | One-call: create RunBuilder internally, certify all steps, optionally attest each bundle, return `{ runSummary, stepBundles, receipts, finalStepHash }`. Injectable `attestStep` for mocking. |
|
|
594
|
+
|
|
595
|
+
### AIEF Interop (v0.7.0+)
|
|
596
|
+
|
|
597
|
+
| Function | Description |
|
|
598
|
+
|---|---|
|
|
599
|
+
| `verifyAief(bundle)` | Verify a CER bundle and return the exact AIEF §9.1 output shape |
|
|
600
|
+
| `mapToAiefReason(code)` | Convert a `CerVerifyCode` string to an AIEF §9.2 reason string |
|
|
601
|
+
| `hashToolOutput(value)` | Hash a tool output value: string → UTF-8 SHA-256; other → canonical JSON SHA-256 |
|
|
602
|
+
| `makeToolEvent(params)` | Build a `ToolEvent` record for inclusion in `snapshot.toolCalls` |
|
|
603
|
+
| `redactBeforeSeal(snapshot, policy)` | Pre-seal redaction: replace `input`/`output` with verifiable envelopes before sealing |
|
|
604
|
+
| `exportVerifiableRedacted(bundle, policy, options?)` | Post-seal: produce a new sealed bundle with redacted snapshot + `meta.provenance.originalCertificateHash`. `verify()` passes on the new bundle. Original is unchanged. |
|
|
605
|
+
| `validateProfile(target, profile)` | Validate a snapshot or bundle against an AIEF strictness profile (does not affect hashing) |
|
|
329
606
|
|
|
330
607
|
### Attestation & Archive
|
|
331
608
|
|
|
@@ -377,6 +654,11 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
|
|
|
377
654
|
| `ATTESTATION_KEY_NOT_FOUND` | kid not found in node keys document (v0.5.0+) |
|
|
378
655
|
| `ATTESTATION_INVALID_SIGNATURE` | Ed25519 signature did not verify (v0.5.0+) |
|
|
379
656
|
| `ATTESTATION_KEY_FORMAT_UNSUPPORTED` | Key cannot be decoded (v0.5.0+) |
|
|
657
|
+
| `CHAIN_BREAK_DETECTED` | `verifyRunSummary` detected a broken prevStepHash link or reordered step (v0.7.0+) |
|
|
658
|
+
| `INCOMPLETE_ARTIFACT` | Step count mismatch between RunSummary and provided bundles (v0.7.0+) |
|
|
659
|
+
| `VERIFICATION_MATERIAL_UNAVAILABLE` | Required verification material (keys, receipt) is absent (v0.7.0+) |
|
|
660
|
+
| `TOOL_EVIDENCE_MISSING` | Required tool call evidence absent in an AIEF_L4 context (v0.7.0+) |
|
|
661
|
+
| `TOOL_OUTPUT_HASH_MISMATCH` | A recorded `outputHash` in `toolCalls` does not match the provided tool output (v0.7.0+) |
|
|
380
662
|
|
|
381
663
|
Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR` > `INVALID_SHA256_FORMAT` > `CERTIFICATE_HASH_MISMATCH` > `INPUT_HASH_MISMATCH` > `OUTPUT_HASH_MISMATCH` > `SNAPSHOT_HASH_MISMATCH` > `UNKNOWN_ERROR`.
|
|
382
664
|
|
|
@@ -401,7 +683,9 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
|
|
|
401
683
|
| v0.4.1 | Verification reason codes (`CerVerifyCode`), `code` + `details` on `VerificationResult`, README provenance wording tightened |
|
|
402
684
|
| v0.4.2 | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
|
|
403
685
|
| v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
|
|
404
|
-
|
|
|
686
|
+
| v0.6.0 | Frictionless integration: `certifyDecisionFromProviderCall` (OpenAI/Anthropic/Gemini/Mistral/Bedrock drop-in); `sanitizeForStorage` + `sanitizeForStamp` redaction helpers; `createClient(defaults)` factory; regression fixture suite; all backward-compatible, no hash changes |
|
|
687
|
+
| v0.7.0 | AIEF alignment: `verifyAief()` (AIEF §9.1 adapter); `verifyRunSummary()` chain verifier; `makeToolEvent()` + `hashToolOutput()` + `snapshot.toolCalls` (AIEF-06); `BundleDeclaration` block (`stabilitySchemeId`, `protectedSetId`, `protectedFields`) excluded from `certificateHash`; `redactBeforeSeal()` pre-seal verifiable redaction; `validateProfile()` (flexible/AIEF_L2/L3/L4); 5 new `CerVerifyCode` entries; backward-compatible, no hash changes |
|
|
688
|
+
| **v0.8.0** | Helper APIs: `exportVerifiableRedacted()` (post-seal re-seal with redacted snapshot + provenance metadata); `certifyAndAttestRun()` (one-call multi-step certify + optional per-step attestation with injectable mock); test determinism fix (no time-dependent first-run failures); all v0.1–v0.7 bundles verify identically, no hashing or canonicalization changes |
|
|
405
689
|
| v1.0.0 | Planned: API stabilization, freeze public API surface |
|
|
406
690
|
|
|
407
691
|
## Releasing
|