@nexart/ai-execution 0.7.0 → 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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @nexart/ai-execution v0.7.0
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,7 +7,7 @@ Tamper-evident records and Certified Execution Records (CER) for AI operations.
7
7
  | Component | Version |
8
8
  |---|---|
9
9
  | Service | — |
10
- | SDK | 0.7.0 |
10
+ | SDK | 0.8.0 |
11
11
  | Protocol | 1.2.0 |
12
12
 
13
13
  ## Why Not Just Store Logs?
@@ -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.7.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 |
@@ -442,6 +442,49 @@ const result = validateProfile(bundle, 'AIEF_L4');
442
442
  // { ok: boolean, errors: string[] }
443
443
  ```
444
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
+
445
488
  ## Redaction Semantics (v0.7.0+)
446
489
 
447
490
  ### Pre-seal verifiable redaction
@@ -467,11 +510,43 @@ Each redacted field becomes `{ _redacted: true, hash: "sha256:..." }` where `has
467
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 |
468
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`. |
469
512
 
470
- ### Post-hoc redaction breaks integrity by design
513
+ ### Verifiable redacted export (post-seal, new bundle)
471
514
 
472
- `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, you must re-seal with `sealCer()`.
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
473
548
 
474
- **Rule of thumb:** redact before sealing if you need `verify()` to pass; use `sanitizeForStorage` only for data you do not need to verify later.
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.
475
550
 
476
551
  ## Canonical JSON Constraints
477
552
 
@@ -515,6 +590,7 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
515
590
  |---|---|
516
591
  | `RunBuilder` | Multi-step workflow builder with prevStepHash chaining |
517
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. |
518
594
 
519
595
  ### AIEF Interop (v0.7.0+)
520
596
 
@@ -525,6 +601,7 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
525
601
  | `hashToolOutput(value)` | Hash a tool output value: string → UTF-8 SHA-256; other → canonical JSON SHA-256 |
526
602
  | `makeToolEvent(params)` | Build a `ToolEvent` record for inclusion in `snapshot.toolCalls` |
527
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. |
528
605
  | `validateProfile(target, profile)` | Validate a snapshot or bundle against an AIEF strictness profile (does not affect hashing) |
529
606
 
530
607
  ### Attestation & Archive
@@ -607,7 +684,8 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
607
684
  | v0.4.2 | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
608
685
  | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
609
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 |
610
- | **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 |
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 |
611
689
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
612
690
 
613
691
  ## Releasing
package/dist/index.cjs CHANGED
@@ -33,10 +33,12 @@ __export(src_exports, {
33
33
  CerAttestationError: () => CerAttestationError,
34
34
  CerVerificationError: () => CerVerificationError,
35
35
  CerVerifyCode: () => CerVerifyCode,
36
+ ReasonCode: () => ReasonCode,
36
37
  RunBuilder: () => RunBuilder,
37
38
  attest: () => attest,
38
39
  attestIfNeeded: () => attestIfNeeded,
39
40
  certifyAndAttestDecision: () => certifyAndAttestDecision,
41
+ certifyAndAttestRun: () => certifyAndAttestRun,
40
42
  certifyDecision: () => certifyDecision,
41
43
  certifyDecisionFromProviderCall: () => certifyDecisionFromProviderCall,
42
44
  computeInputHash: () => computeInputHash,
@@ -44,6 +46,7 @@ __export(src_exports, {
44
46
  createClient: () => createClient,
45
47
  createSnapshot: () => createSnapshot,
46
48
  exportCer: () => exportCer,
49
+ exportVerifiableRedacted: () => exportVerifiableRedacted,
47
50
  fetchNodeKeys: () => fetchNodeKeys,
48
51
  getAttestationReceipt: () => getAttestationReceipt,
49
52
  hasAttestation: () => hasAttestation,
@@ -63,6 +66,7 @@ __export(src_exports, {
63
66
  toCanonicalJson: () => toCanonicalJson,
64
67
  validateProfile: () => validateProfile,
65
68
  verify: () => verifyCer,
69
+ verifyAiCerBundleDetailed: () => verifyAiCerBundleDetailed,
66
70
  verifyAief: () => verifyAief,
67
71
  verifyBundleAttestation: () => verifyBundleAttestation,
68
72
  verifyCer: () => verifyCer,
@@ -189,7 +193,7 @@ function computeOutputHash(output) {
189
193
  }
190
194
 
191
195
  // src/snapshot.ts
192
- var PACKAGE_VERSION = "0.7.0";
196
+ var PACKAGE_VERSION = "0.8.0";
193
197
  function validateParameters(params) {
194
198
  const errors = [];
195
199
  if (typeof params.temperature !== "number" || !Number.isFinite(params.temperature)) {
@@ -457,7 +461,7 @@ function certifyDecision(params) {
457
461
  conversationId: params.conversationId,
458
462
  prevStepHash: params.prevStepHash
459
463
  });
460
- return sealCer(snapshot, { meta: params.meta });
464
+ return sealCer(snapshot, { createdAt: params.createdAt, meta: params.meta });
461
465
  }
462
466
 
463
467
  // src/run.ts
@@ -2485,15 +2489,150 @@ function validateProfile(target, profile) {
2485
2489
  }
2486
2490
  return { ok: errors.length === 0, errors };
2487
2491
  }
2492
+
2493
+ // src/exportRedacted.ts
2494
+ function exportVerifiableRedacted(bundle, policy, options) {
2495
+ const createdAt = options?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
2496
+ const redactedSnapshot = redactBeforeSeal(bundle.snapshot, policy);
2497
+ const provenance = {
2498
+ originalCertificateHash: bundle.certificateHash,
2499
+ redactionPolicy: { paths: [...policy.paths] },
2500
+ redactedAt: createdAt
2501
+ };
2502
+ const mergedMeta = {
2503
+ ...bundle.meta,
2504
+ provenance
2505
+ };
2506
+ const newBundle = sealCer(redactedSnapshot, {
2507
+ createdAt,
2508
+ meta: mergedMeta,
2509
+ declaration: bundle.declaration
2510
+ });
2511
+ return {
2512
+ bundle: newBundle,
2513
+ originalCertificateHash: bundle.certificateHash
2514
+ };
2515
+ }
2516
+
2517
+ // src/runHelper.ts
2518
+ async function certifyAndAttestRun(steps, options) {
2519
+ const run = new RunBuilder({
2520
+ runId: options?.runId,
2521
+ workflowId: options?.workflowId,
2522
+ conversationId: options?.conversationId,
2523
+ appId: options?.appId
2524
+ });
2525
+ const stepBundles = [];
2526
+ const receipts = [];
2527
+ for (const stepParams of steps) {
2528
+ const bundle = run.step(stepParams);
2529
+ stepBundles.push(bundle);
2530
+ if (options?.attestStep) {
2531
+ const receipt = await options.attestStep(bundle);
2532
+ receipts.push(receipt);
2533
+ } else {
2534
+ receipts.push(null);
2535
+ }
2536
+ }
2537
+ const runSummary = run.finalize();
2538
+ return {
2539
+ runSummary,
2540
+ stepBundles,
2541
+ receipts,
2542
+ finalStepHash: runSummary.finalStepHash
2543
+ };
2544
+ }
2545
+
2546
+ // src/cerProtocol.ts
2547
+ var ReasonCode = {
2548
+ BUNDLE_HASH_MISMATCH: "BUNDLE_HASH_MISMATCH",
2549
+ NODE_SIGNATURE_INVALID: "NODE_SIGNATURE_INVALID",
2550
+ NODE_SIGNATURE_MISSING: "NODE_SIGNATURE_MISSING",
2551
+ RECEIPT_HASH_MISMATCH: "RECEIPT_HASH_MISMATCH",
2552
+ SCHEMA_VERSION_UNSUPPORTED: "SCHEMA_VERSION_UNSUPPORTED",
2553
+ RECORD_NOT_FOUND: "RECORD_NOT_FOUND",
2554
+ BUNDLE_CORRUPTED: "BUNDLE_CORRUPTED"
2555
+ };
2556
+
2557
+ // src/verifyDetailed.ts
2558
+ function mapCerCodeToReasonCodes(cerCode) {
2559
+ switch (cerCode) {
2560
+ case CerVerifyCode.CERTIFICATE_HASH_MISMATCH:
2561
+ case CerVerifyCode.SNAPSHOT_HASH_MISMATCH:
2562
+ case CerVerifyCode.INPUT_HASH_MISMATCH:
2563
+ case CerVerifyCode.OUTPUT_HASH_MISMATCH:
2564
+ return [ReasonCode.BUNDLE_HASH_MISMATCH];
2565
+ case CerVerifyCode.SCHEMA_ERROR:
2566
+ return [ReasonCode.SCHEMA_VERSION_UNSUPPORTED];
2567
+ case CerVerifyCode.CANONICALIZATION_ERROR:
2568
+ case CerVerifyCode.UNKNOWN_ERROR:
2569
+ case CerVerifyCode.INVALID_SHA256_FORMAT:
2570
+ return [ReasonCode.BUNDLE_CORRUPTED];
2571
+ case CerVerifyCode.ATTESTATION_INVALID_SIGNATURE:
2572
+ case CerVerifyCode.ATTESTATION_KEY_FORMAT_UNSUPPORTED:
2573
+ case CerVerifyCode.ATTESTATION_KEY_NOT_FOUND:
2574
+ return [ReasonCode.NODE_SIGNATURE_INVALID];
2575
+ case CerVerifyCode.ATTESTATION_MISSING:
2576
+ return [ReasonCode.NODE_SIGNATURE_MISSING];
2577
+ default:
2578
+ return [];
2579
+ }
2580
+ }
2581
+ function verifyAiCerBundleDetailed(bundle) {
2582
+ const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2583
+ const verifier = "@nexart/ai-execution";
2584
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
2585
+ return {
2586
+ status: "FAILED",
2587
+ checks: { bundleIntegrity: "FAIL", nodeSignature: "SKIPPED", receiptConsistency: "SKIPPED" },
2588
+ reasonCodes: [ReasonCode.BUNDLE_CORRUPTED],
2589
+ certificateHash: "",
2590
+ bundleType: "",
2591
+ verifiedAt,
2592
+ verifier
2593
+ };
2594
+ }
2595
+ const b = bundle;
2596
+ const bundleType = typeof b["bundleType"] === "string" ? b["bundleType"] : "";
2597
+ const certificateHash = typeof b["certificateHash"] === "string" ? b["certificateHash"] : "";
2598
+ if (bundleType !== "cer.ai.execution.v1") {
2599
+ return {
2600
+ status: "FAILED",
2601
+ checks: { bundleIntegrity: "FAIL", nodeSignature: "SKIPPED", receiptConsistency: "SKIPPED" },
2602
+ reasonCodes: [ReasonCode.SCHEMA_VERSION_UNSUPPORTED],
2603
+ certificateHash,
2604
+ bundleType,
2605
+ verifiedAt,
2606
+ verifier
2607
+ };
2608
+ }
2609
+ const cerResult = verifyCer(bundle);
2610
+ const bundleIntegrity = cerResult.ok ? "PASS" : "FAIL";
2611
+ const attestationPresent = hasAttestation(bundle);
2612
+ const nodeSignature = attestationPresent ? "PASS" : "SKIPPED";
2613
+ const receiptConsistency = attestationPresent ? "PASS" : "SKIPPED";
2614
+ const reasonCodes = cerResult.ok ? [] : mapCerCodeToReasonCodes(cerResult.code);
2615
+ return {
2616
+ status: cerResult.ok ? "VERIFIED" : "FAILED",
2617
+ checks: { bundleIntegrity, nodeSignature, receiptConsistency },
2618
+ reasonCodes,
2619
+ certificateHash,
2620
+ bundleType,
2621
+ verifiedAt,
2622
+ verifier
2623
+ };
2624
+ }
2488
2625
  // Annotate the CommonJS export names for ESM import in node:
2489
2626
  0 && (module.exports = {
2490
2627
  CerAttestationError,
2491
2628
  CerVerificationError,
2492
2629
  CerVerifyCode,
2630
+ ReasonCode,
2493
2631
  RunBuilder,
2494
2632
  attest,
2495
2633
  attestIfNeeded,
2496
2634
  certifyAndAttestDecision,
2635
+ certifyAndAttestRun,
2497
2636
  certifyDecision,
2498
2637
  certifyDecisionFromProviderCall,
2499
2638
  computeInputHash,
@@ -2501,6 +2640,7 @@ function validateProfile(target, profile) {
2501
2640
  createClient,
2502
2641
  createSnapshot,
2503
2642
  exportCer,
2643
+ exportVerifiableRedacted,
2504
2644
  fetchNodeKeys,
2505
2645
  getAttestationReceipt,
2506
2646
  hasAttestation,
@@ -2520,6 +2660,7 @@ function validateProfile(target, profile) {
2520
2660
  toCanonicalJson,
2521
2661
  validateProfile,
2522
2662
  verify,
2663
+ verifyAiCerBundleDetailed,
2523
2664
  verifyAief,
2524
2665
  verifyBundleAttestation,
2525
2666
  verifyCer,