@nexart/ai-execution 0.5.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,10 +1,18 @@
1
- # @nexart/ai-execution v0.5.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
 
5
+ ## Version Information
6
+
7
+ | Component | Version |
8
+ |---|---|
9
+ | Service | — |
10
+ | SDK | 0.7.0 |
11
+ | Protocol | 1.2.0 |
12
+
5
13
  ## Why Not Just Store Logs?
6
14
 
7
- 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.**
8
16
 
9
17
  ## What This Does
10
18
 
@@ -97,7 +105,7 @@ const summary = run.finalize();
97
105
  // { runId, stepCount: 2, steps: [...], finalStepHash: "sha256:..." }
98
106
  ```
99
107
 
100
- ### Attest to Canonical Node
108
+ ### Attest to NexArt Attestation Node (optional)
101
109
 
102
110
  ```typescript
103
111
  import { certifyDecision, attest } from '@nexart/ai-execution';
@@ -110,7 +118,7 @@ const proof = await attest(cer, {
110
118
  console.log(proof.attestationId);
111
119
  ```
112
120
 
113
- 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).
114
122
 
115
123
  ### Archive (Export / Import)
116
124
 
@@ -139,7 +147,7 @@ const restored = importCer(json); // parse + verify (throws on tamper)
139
147
  | `modelVersion` | Optional | `string \| null` | Defaults to `null` |
140
148
  | `parameters.topP` | Optional | `number \| null` | Defaults to `null` |
141
149
  | `parameters.seed` | Optional | `number \| null` | Defaults to `null` |
142
- | `sdkVersion` | Optional | `string \| null` | Defaults to `"0.4.2"` |
150
+ | `sdkVersion` | Optional | `string \| null` | Defaults to `"0.7.0"` |
143
151
  | `appId` | Optional | `string \| null` | Defaults to `null` |
144
152
  | `runId` | Optional | `string \| null` | Workflow run ID |
145
153
  | `stepId` | Optional | `string \| null` | Step identifier within a run |
@@ -147,6 +155,7 @@ const restored = importCer(json); // parse + verify (throws on tamper)
147
155
  | `workflowId` | Optional | `string \| null` | Workflow template ID |
148
156
  | `conversationId` | Optional | `string \| null` | Conversation/session ID |
149
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. |
150
159
 
151
160
  Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `protocolVersion`, `executionSurface`, `inputHash`, `outputHash`.
152
161
 
@@ -158,14 +167,39 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
158
167
  "certificateHash": "sha256:...",
159
168
  "createdAt": "2026-02-12T00:00:00.000Z",
160
169
  "version": "0.1",
161
- "snapshot": { ... },
162
- "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
+ }
163
178
  }
164
179
  ```
165
180
 
166
181
  ### Certificate Hash Computation
167
182
 
168
- 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
+ ```
169
203
 
170
204
  ## Attestation
171
205
 
@@ -277,6 +311,168 @@ const result = await verifyNodeReceiptSignature({
277
311
 
278
312
  **Skip re-attestation:** use `hasAttestation(bundle)` to check if a bundle already includes attestation fields before calling `attest()` again.
279
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
+
280
476
  ## Canonical JSON Constraints
281
477
 
282
478
  1. Object keys sorted lexicographically (Unicode codepoint order) at every nesting level.
@@ -318,6 +514,18 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
318
514
  | Export | Description |
319
515
  |---|---|
320
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) |
321
529
 
322
530
  ### Attestation & Archive
323
531
 
@@ -332,10 +540,24 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
332
540
  | `selectNodeKey(doc, kid?)` | Select a key from a `NodeKeysDocument` by kid or activeKid (v0.5.0+) |
333
541
  | `verifyBundleAttestation(bundle, options)` | One-call offline attestation verification (v0.5.0+) |
334
542
  | `sanitizeForAttestation(bundle)` | Remove `undefined` keys, reject BigInt/functions/symbols |
543
+ | `sanitizeForStorage(bundle, options?)` | Sanitize for DB storage with optional path-based redaction; does not recompute hashes (v0.6.0+) |
544
+ | `sanitizeForStamp(bundle)` | Extract attestable core (no meta); does not recompute hashes (v0.6.0+) |
335
545
  | `hasAttestation(bundle)` | Check if bundle already has attestation fields |
336
546
  | `exportCer(bundle)` | Serialize to canonical JSON string |
337
547
  | `importCer(json)` | Parse + verify from JSON string |
338
548
 
549
+ ### Provider Drop-in (v0.6.0+)
550
+
551
+ | Function | Description |
552
+ |---|---|
553
+ | `certifyDecisionFromProviderCall(params)` | One-function wrapper: extracts prompt/input/output/params from raw provider request+response and returns `{ ok, bundle }` or `{ ok: false, code: 'SCHEMA_ERROR', reason }`. Supports OpenAI, Anthropic, Gemini, Mistral, Bedrock, and generic shapes. |
554
+
555
+ ### Opinionated Client (v0.6.0+)
556
+
557
+ | Export | Description |
558
+ |---|---|
559
+ | `createClient(defaults)` | Returns a `NexArtClient` with bound defaults (`appId`, `workflowId`, `nodeUrl`, `apiKey`, `tags`, `source`). Methods: `certifyDecision`, `certifyAndAttestDecision`, `verify`, `verifyBundleAttestation`. Defaults do not affect bundle hashing. |
560
+
339
561
  ### Reason Codes
340
562
 
341
563
  `CerVerifyCode` — stable string-union constant exported from the package root:
@@ -355,6 +577,11 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
355
577
  | `ATTESTATION_KEY_NOT_FOUND` | kid not found in node keys document (v0.5.0+) |
356
578
  | `ATTESTATION_INVALID_SIGNATURE` | Ed25519 signature did not verify (v0.5.0+) |
357
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+) |
358
585
 
359
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`.
360
587
 
@@ -378,7 +605,9 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
378
605
  | v0.4.0 | Dual ESM/CJS build, `sanitizeForAttestation`, `hasAttestation`, auto-sanitize in `attest()`, fixed `ERR_PACKAGE_PATH_NOT_EXPORTED` |
379
606
  | v0.4.1 | Verification reason codes (`CerVerifyCode`), `code` + `details` on `VerificationResult`, README provenance wording tightened |
380
607
  | v0.4.2 | `AttestationReceipt`, `getAttestationReceipt`, `certifyAndAttestDecision`, `attestIfNeeded` |
381
- | **v0.5.0** | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
608
+ | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
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 |
382
611
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
383
612
 
384
613
  ## Releasing