@nexart/ai-execution 0.6.0 → 0.7.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.6.0
1
+ # @nexart/ai-execution v0.7.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.6.0 |
10
+ | SDK | 0.7.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 — to the input, output, parameters, or ordering — 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.**
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 Canonical Node
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 records it in the proof ledger.
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.4.2"` |
150
+ | `sdkVersion` | Optional | `string \| null` | Defaults to `"0.7.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` is excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
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,168 @@ 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
+ ## Redaction Semantics (v0.7.0+)
446
+
447
+ ### Pre-seal verifiable redaction
448
+
449
+ `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.
450
+
451
+ ```typescript
452
+ import { redactBeforeSeal, sealCer, verify } from '@nexart/ai-execution';
453
+
454
+ const redacted = redactBeforeSeal(snapshot, { paths: ['input', 'output'] });
455
+ const bundle = sealCer(redacted);
456
+ verify(bundle).ok; // true — hash matches the redacted snapshot
457
+ ```
458
+
459
+ 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.
460
+
461
+ **Supported fields for pre-seal redaction:**
462
+
463
+ | Field | Supported | Notes |
464
+ |---|---|---|
465
+ | `input` | **Yes** | `inputHash` is recomputed from the envelope |
466
+ | `output` | **Yes** | `outputHash` is recomputed from the envelope |
467
+ | `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
+ | `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
+
470
+ ### Post-hoc redaction breaks integrity — by design
471
+
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()`.
473
+
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.
475
+
288
476
  ## Canonical JSON Constraints
289
477
 
290
478
  1. Object keys sorted lexicographically (Unicode codepoint order) at every nesting level.
@@ -326,6 +514,18 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
326
514
  | Export | Description |
327
515
  |---|---|
328
516
  | `RunBuilder` | Multi-step workflow builder with prevStepHash chaining |
517
+ | `verifyRunSummary(summary, bundles, opts?)` | Verify that a RunSummary + step bundles form an unbroken cryptographic chain (v0.7.0+) |
518
+
519
+ ### AIEF Interop (v0.7.0+)
520
+
521
+ | Function | Description |
522
+ |---|---|
523
+ | `verifyAief(bundle)` | Verify a CER bundle and return the exact AIEF §9.1 output shape |
524
+ | `mapToAiefReason(code)` | Convert a `CerVerifyCode` string to an AIEF §9.2 reason string |
525
+ | `hashToolOutput(value)` | Hash a tool output value: string → UTF-8 SHA-256; other → canonical JSON SHA-256 |
526
+ | `makeToolEvent(params)` | Build a `ToolEvent` record for inclusion in `snapshot.toolCalls` |
527
+ | `redactBeforeSeal(snapshot, policy)` | Pre-seal redaction: replace `input`/`output` with verifiable envelopes before sealing |
528
+ | `validateProfile(target, profile)` | Validate a snapshot or bundle against an AIEF strictness profile (does not affect hashing) |
329
529
 
330
530
  ### Attestation & Archive
331
531
 
@@ -377,6 +577,11 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
377
577
  | `ATTESTATION_KEY_NOT_FOUND` | kid not found in node keys document (v0.5.0+) |
378
578
  | `ATTESTATION_INVALID_SIGNATURE` | Ed25519 signature did not verify (v0.5.0+) |
379
579
  | `ATTESTATION_KEY_FORMAT_UNSUPPORTED` | Key cannot be decoded (v0.5.0+) |
580
+ | `CHAIN_BREAK_DETECTED` | `verifyRunSummary` detected a broken prevStepHash link or reordered step (v0.7.0+) |
581
+ | `INCOMPLETE_ARTIFACT` | Step count mismatch between RunSummary and provided bundles (v0.7.0+) |
582
+ | `VERIFICATION_MATERIAL_UNAVAILABLE` | Required verification material (keys, receipt) is absent (v0.7.0+) |
583
+ | `TOOL_EVIDENCE_MISSING` | Required tool call evidence absent in an AIEF_L4 context (v0.7.0+) |
584
+ | `TOOL_OUTPUT_HASH_MISMATCH` | A recorded `outputHash` in `toolCalls` does not match the provided tool output (v0.7.0+) |
380
585
 
381
586
  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
587
 
@@ -401,7 +606,8 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
401
606
  | v0.4.1 | Verification reason codes (`CerVerifyCode`), `code` + `details` on `VerificationResult`, README provenance wording tightened |
402
607
  | v0.4.2 | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
403
608
  | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
404
- | **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 |
609
+ | 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 |
405
611
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
406
612
 
407
613
  ## Releasing