@nexart/ai-execution 0.8.0 → 0.10.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.8.0
1
+ # @nexart/ai-execution v0.10.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.8.0 |
10
+ | SDK | 0.10.0 |
11
11
  | Protocol | 1.2.0 |
12
12
 
13
13
  ## Why Not Just Store Logs?
@@ -672,6 +672,168 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
672
672
  | `runAnthropicExecution` | `@nexart/ai-execution/providers/anthropic` |
673
673
  | `wrapProvider` | `@nexart/ai-execution/providers/wrap` |
674
674
 
675
+ ## LangChain Integration
676
+
677
+ `@nexart/ai-execution` v0.10.0 includes a minimal LangChain helper surface. Certify the final input and output of any LangChain chain, agent, or runnable as a tamper-evident CER — no LangChain package dependency required.
678
+
679
+ `certifyLangChainRun` operates in two modes depending on whether `nodeUrl` and `apiKey` are supplied:
680
+
681
+ | Mode | How to call | Returns |
682
+ |---|---|---|
683
+ | **Local** (default) | No `nodeUrl` / `apiKey` | `LangChainCerResult` (sync) |
684
+ | **Node-attested** | `nodeUrl` + `apiKey` on input | `Promise<LangChainAttestedResult>` |
685
+
686
+ ### Mode 1 — Local CER creation (synchronous, no network)
687
+
688
+ `createLangChainCer` is always local. `certifyLangChainRun` without `nodeUrl`/`apiKey` is identical.
689
+
690
+ ```ts
691
+ import { createLangChainCer, certifyLangChainRun } from '@nexart/ai-execution';
692
+ // or: import { ... } from '@nexart/ai-execution/langchain';
693
+
694
+ // Using createLangChainCer — always explicit about local-only behaviour
695
+ const { bundle, certificateHash, executionId } = createLangChainCer({
696
+ executionId: 'run-001', // optional — UUID generated if omitted
697
+ provider: 'openai',
698
+ model: 'gpt-4o-mini',
699
+ input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
700
+ output: { text: 'Paris.' },
701
+ metadata: { appId: 'my-app', projectId: 'docs-bot' },
702
+ parameters: { temperature: 0, maxTokens: 200, topP: null, seed: null },
703
+ createdAt: new Date().toISOString(), // pin for deterministic hash
704
+ });
705
+
706
+ console.log(certificateHash); // sha256:...
707
+
708
+ // certifyLangChainRun without nodeUrl/apiKey: identical, sync
709
+ const { bundle: b2 } = certifyLangChainRun({
710
+ provider: 'openai', model: 'gpt-4o-mini',
711
+ input: { messages: [{ role: 'user', content: 'Summarise this.' }] },
712
+ output: { text: 'Summary...' },
713
+ });
714
+ ```
715
+
716
+ ### Mode 2 — Node-attested certification (async)
717
+
718
+ Add `nodeUrl` and `apiKey` to the same input to route through the existing `certifyAndAttestDecision()` path. The function returns a `Promise<LangChainAttestedResult>` with the `receipt` from the NexArt node.
719
+
720
+ ```ts
721
+ import { certifyLangChainRun } from '@nexart/ai-execution';
722
+
723
+ const result = await certifyLangChainRun({
724
+ executionId: 'run-001',
725
+ provider: 'openai',
726
+ model: 'gpt-4o-mini',
727
+ input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
728
+ output: { text: 'Paris.' },
729
+ metadata: { appId: 'my-app', projectId: 'docs-bot' },
730
+ createdAt: new Date().toISOString(),
731
+ nodeUrl: 'https://node.nexart.io', // ← triggers attested mode
732
+ apiKey: process.env.NEXART_API_KEY!, // ← required with nodeUrl
733
+ });
734
+
735
+ console.log(result.certificateHash); // sha256:... (same semantics as local)
736
+ console.log(result.attested); // true
737
+ console.log(result.receipt);
738
+ // {
739
+ // attestationId: "nxa_attest_...",
740
+ // certificateHash: "sha256:...",
741
+ // nodeRuntimeHash: "sha256:...",
742
+ // protocolVersion: "1.2.0"
743
+ // }
744
+ ```
745
+
746
+ `result.bundle` passes `verifyCer()` identically to local mode — the `certificateHash` covers only the CER content, not the receipt fields.
747
+
748
+ ### Three helpers
749
+
750
+ | Helper | Returns | Network |
751
+ |---|---|---|
752
+ | `createLangChainCer(input)` | `LangChainCerResult` (sync) | Never |
753
+ | `certifyLangChainRun(input)` without `nodeUrl`/`apiKey` | `LangChainCerResult` (sync) | Never |
754
+ | `certifyLangChainRun({ ...input, nodeUrl, apiKey })` | `Promise<LangChainAttestedResult>` | Yes — NexArt node |
755
+
756
+ ### Result types
757
+
758
+ ```ts
759
+ interface LangChainCerResult {
760
+ bundle: CerAiExecutionBundle;
761
+ certificateHash: string;
762
+ executionId: string;
763
+ }
764
+
765
+ interface LangChainAttestedResult extends LangChainCerResult {
766
+ receipt: AttestationReceipt;
767
+ attested: true;
768
+ }
769
+ ```
770
+
771
+ ### Input normalization
772
+
773
+ | Input shape | Stored in CER as |
774
+ |---|---|
775
+ | `string` | `string` (pass-through) |
776
+ | Plain object `{}` | `Record<string, unknown>` (pass-through) |
777
+ | Array `[]` | `{ items: [...] }` |
778
+ | Anything else | `String(value)` |
779
+
780
+ ### Prompt extraction
781
+
782
+ Resolved in this order: `metadata.prompt` → `input.messages[0].content` → `input.prompt` → `input.text` → `"[LangChain run]"`.
783
+
784
+ ### Metadata mapping
785
+
786
+ | Metadata key | Mapped to |
787
+ |---|---|
788
+ | `appId` | `snapshot.appId` |
789
+ | `runId` | `snapshot.runId` |
790
+ | `workflowId` | `snapshot.workflowId` |
791
+ | `conversationId` | `snapshot.conversationId` |
792
+ | `prompt` | Used as the CER `prompt` field |
793
+ | All other keys | `bundle.meta.tags` (as `"key:value"` strings) |
794
+
795
+ ### Verification
796
+
797
+ Bundles from all three helpers are fully protocol-aligned:
798
+
799
+ ```ts
800
+ import { verifyCer, verifyAiCerBundleDetailed } from '@nexart/ai-execution';
801
+
802
+ const basic = verifyCer(bundle);
803
+ // { ok: true, code: 'OK' }
804
+
805
+ const detailed = verifyAiCerBundleDetailed(bundle);
806
+ // { status: 'VERIFIED', checks: { bundleIntegrity: 'PASS', nodeSignature: 'SKIPPED' }, ... }
807
+ ```
808
+
809
+ ### Testing the attested path without a network
810
+
811
+ Use the injectable `_attestFn` test option (same pattern as `certifyAndAttestRun`'s `attestStep`):
812
+
813
+ ```ts
814
+ import type { AttestDecisionFn } from '@nexart/ai-execution';
815
+
816
+ const mockAttest: AttestDecisionFn = async (params, _opts) => {
817
+ const { certifyDecision } = await import('@nexart/ai-execution');
818
+ const bundle = certifyDecision(params);
819
+ return { bundle, receipt: { attestationId: 'mock-123', certificateHash: bundle.certificateHash,
820
+ nodeRuntimeHash: 'sha256:' + 'beef'.repeat(16), protocolVersion: '1.2.0' } };
821
+ };
822
+
823
+ const result = await certifyLangChainRun(
824
+ { ...input, nodeUrl: 'https://node.nexart.io', apiKey: 'nxa_test' },
825
+ { _attestFn: mockAttest },
826
+ );
827
+ ```
828
+
829
+ ### V3 roadmap (not yet implemented)
830
+
831
+ - `BaseCallbackHandler` integration for automatic mid-chain CER capture at `onLLMEnd`
832
+ - LangSmith trace correlation via `metadata.runId` → `snapshot.conversationId`
833
+ - Separate `@nexart/langchain` package once the callback surface is proven in production
834
+
835
+ ---
836
+
675
837
  ## Version History
676
838
 
677
839
  | Version | Description |
@@ -685,7 +847,9 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
685
847
  | v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
686
848
  | 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
849
  | 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 |
850
+ | 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; all v0.1–v0.7 bundles verify identically |
851
+ | v0.9.0 | CER Protocol types: `CerVerificationResult`, `ReasonCode`, `CheckStatus`; `verifyAiCerBundleDetailed()`; `CertifyDecisionParams.createdAt` wired through; determinism bug fix |
852
+ | **v0.10.0** | LangChain integration: `createLangChainCer()` (sync/local); `certifyLangChainRun()` dual-mode — local (sync) or node-attested (`Promise<LangChainAttestedResult>` when `nodeUrl`+`apiKey` supplied); `LangChainAttestedResult`, `AttestDecisionFn`; injectable `_attestFn` for test mocking; `@nexart/ai-execution/langchain` subpath; 47 tests (was 34); all prior bundles verify identically |
689
853
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
690
854
 
691
855
  ## Releasing
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __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,
@@ -40,9 +41,11 @@ __export(src_exports, {
40
41
  certifyAndAttestRun: () => certifyAndAttestRun,
41
42
  certifyDecision: () => certifyDecision,
42
43
  certifyDecisionFromProviderCall: () => certifyDecisionFromProviderCall,
44
+ certifyLangChainRun: () => certifyLangChainRun,
43
45
  computeInputHash: () => computeInputHash,
44
46
  computeOutputHash: () => computeOutputHash,
45
47
  createClient: () => createClient,
48
+ createLangChainCer: () => createLangChainCer,
46
49
  createSnapshot: () => createSnapshot,
47
50
  exportCer: () => exportCer,
48
51
  exportVerifiableRedacted: () => exportVerifiableRedacted,
@@ -65,6 +68,7 @@ __export(src_exports, {
65
68
  toCanonicalJson: () => toCanonicalJson,
66
69
  validateProfile: () => validateProfile,
67
70
  verify: () => verifyCer,
71
+ verifyAiCerBundleDetailed: () => verifyAiCerBundleDetailed,
68
72
  verifyAief: () => verifyAief,
69
73
  verifyBundleAttestation: () => verifyBundleAttestation,
70
74
  verifyCer: () => verifyCer,
@@ -459,7 +463,7 @@ function certifyDecision(params) {
459
463
  conversationId: params.conversationId,
460
464
  prevStepHash: params.prevStepHash
461
465
  });
462
- return sealCer(snapshot, { meta: params.meta });
466
+ return sealCer(snapshot, { createdAt: params.createdAt, meta: params.meta });
463
467
  }
464
468
 
465
469
  // src/run.ts
@@ -2540,11 +2544,185 @@ async function certifyAndAttestRun(steps, options) {
2540
2544
  finalStepHash: runSummary.finalStepHash
2541
2545
  };
2542
2546
  }
2547
+
2548
+ // src/cerProtocol.ts
2549
+ var ReasonCode = {
2550
+ BUNDLE_HASH_MISMATCH: "BUNDLE_HASH_MISMATCH",
2551
+ NODE_SIGNATURE_INVALID: "NODE_SIGNATURE_INVALID",
2552
+ NODE_SIGNATURE_MISSING: "NODE_SIGNATURE_MISSING",
2553
+ RECEIPT_HASH_MISMATCH: "RECEIPT_HASH_MISMATCH",
2554
+ SCHEMA_VERSION_UNSUPPORTED: "SCHEMA_VERSION_UNSUPPORTED",
2555
+ RECORD_NOT_FOUND: "RECORD_NOT_FOUND",
2556
+ BUNDLE_CORRUPTED: "BUNDLE_CORRUPTED"
2557
+ };
2558
+
2559
+ // src/verifyDetailed.ts
2560
+ function mapCerCodeToReasonCodes(cerCode) {
2561
+ switch (cerCode) {
2562
+ case CerVerifyCode.CERTIFICATE_HASH_MISMATCH:
2563
+ case CerVerifyCode.SNAPSHOT_HASH_MISMATCH:
2564
+ case CerVerifyCode.INPUT_HASH_MISMATCH:
2565
+ case CerVerifyCode.OUTPUT_HASH_MISMATCH:
2566
+ return [ReasonCode.BUNDLE_HASH_MISMATCH];
2567
+ case CerVerifyCode.SCHEMA_ERROR:
2568
+ return [ReasonCode.SCHEMA_VERSION_UNSUPPORTED];
2569
+ case CerVerifyCode.CANONICALIZATION_ERROR:
2570
+ case CerVerifyCode.UNKNOWN_ERROR:
2571
+ case CerVerifyCode.INVALID_SHA256_FORMAT:
2572
+ return [ReasonCode.BUNDLE_CORRUPTED];
2573
+ case CerVerifyCode.ATTESTATION_INVALID_SIGNATURE:
2574
+ case CerVerifyCode.ATTESTATION_KEY_FORMAT_UNSUPPORTED:
2575
+ case CerVerifyCode.ATTESTATION_KEY_NOT_FOUND:
2576
+ return [ReasonCode.NODE_SIGNATURE_INVALID];
2577
+ case CerVerifyCode.ATTESTATION_MISSING:
2578
+ return [ReasonCode.NODE_SIGNATURE_MISSING];
2579
+ default:
2580
+ return [];
2581
+ }
2582
+ }
2583
+ function verifyAiCerBundleDetailed(bundle) {
2584
+ const verifiedAt = (/* @__PURE__ */ new Date()).toISOString();
2585
+ const verifier = "@nexart/ai-execution";
2586
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
2587
+ return {
2588
+ status: "FAILED",
2589
+ checks: { bundleIntegrity: "FAIL", nodeSignature: "SKIPPED", receiptConsistency: "SKIPPED" },
2590
+ reasonCodes: [ReasonCode.BUNDLE_CORRUPTED],
2591
+ certificateHash: "",
2592
+ bundleType: "",
2593
+ verifiedAt,
2594
+ verifier
2595
+ };
2596
+ }
2597
+ const b = bundle;
2598
+ const bundleType = typeof b["bundleType"] === "string" ? b["bundleType"] : "";
2599
+ const certificateHash = typeof b["certificateHash"] === "string" ? b["certificateHash"] : "";
2600
+ if (bundleType !== "cer.ai.execution.v1") {
2601
+ return {
2602
+ status: "FAILED",
2603
+ checks: { bundleIntegrity: "FAIL", nodeSignature: "SKIPPED", receiptConsistency: "SKIPPED" },
2604
+ reasonCodes: [ReasonCode.SCHEMA_VERSION_UNSUPPORTED],
2605
+ certificateHash,
2606
+ bundleType,
2607
+ verifiedAt,
2608
+ verifier
2609
+ };
2610
+ }
2611
+ const cerResult = verifyCer(bundle);
2612
+ const bundleIntegrity = cerResult.ok ? "PASS" : "FAIL";
2613
+ const attestationPresent = hasAttestation(bundle);
2614
+ const nodeSignature = attestationPresent ? "PASS" : "SKIPPED";
2615
+ const receiptConsistency = attestationPresent ? "PASS" : "SKIPPED";
2616
+ const reasonCodes = cerResult.ok ? [] : mapCerCodeToReasonCodes(cerResult.code);
2617
+ return {
2618
+ status: cerResult.ok ? "VERIFIED" : "FAILED",
2619
+ checks: { bundleIntegrity, nodeSignature, receiptConsistency },
2620
+ reasonCodes,
2621
+ certificateHash,
2622
+ bundleType,
2623
+ verifiedAt,
2624
+ verifier
2625
+ };
2626
+ }
2627
+
2628
+ // src/langchain.ts
2629
+ var crypto6 = __toESM(require("crypto"), 1);
2630
+ function normalizeCerValue(value) {
2631
+ if (typeof value === "string") return value;
2632
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
2633
+ return value;
2634
+ }
2635
+ if (Array.isArray(value)) return { items: value };
2636
+ return String(value);
2637
+ }
2638
+ function extractPrompt(input, metadata) {
2639
+ if (typeof metadata?.prompt === "string" && metadata.prompt.length > 0) {
2640
+ return metadata.prompt;
2641
+ }
2642
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
2643
+ const obj = input;
2644
+ if (Array.isArray(obj.messages) && obj.messages.length > 0) {
2645
+ const first = obj.messages[0];
2646
+ if (typeof first?.content === "string" && first.content.length > 0) {
2647
+ return first.content;
2648
+ }
2649
+ }
2650
+ if (typeof obj.prompt === "string" && obj.prompt.length > 0) return obj.prompt;
2651
+ if (typeof obj.text === "string" && obj.text.length > 0) return obj.text;
2652
+ }
2653
+ if (typeof input === "string" && input.length > 0) return input;
2654
+ return "[LangChain run]";
2655
+ }
2656
+ function buildMeta(metadata) {
2657
+ if (!metadata || Object.keys(metadata).length === 0) return void 0;
2658
+ const reserved = /* @__PURE__ */ new Set(["prompt", "appId", "runId", "workflowId", "conversationId"]);
2659
+ const extraTags = [];
2660
+ for (const [k, v] of Object.entries(metadata)) {
2661
+ if (!reserved.has(k)) {
2662
+ extraTags.push(typeof v === "string" ? `${k}:${v}` : `${k}:${JSON.stringify(v)}`);
2663
+ }
2664
+ }
2665
+ return extraTags.length > 0 ? { tags: extraTags } : void 0;
2666
+ }
2667
+ function resolveParameters(params) {
2668
+ return {
2669
+ temperature: params?.temperature ?? 0,
2670
+ maxTokens: params?.maxTokens ?? 0,
2671
+ topP: params?.topP ?? null,
2672
+ seed: params?.seed ?? null
2673
+ };
2674
+ }
2675
+ function buildCertifyParams(input, executionId) {
2676
+ return {
2677
+ executionId,
2678
+ timestamp: input.timestamp,
2679
+ createdAt: input.createdAt,
2680
+ provider: input.provider,
2681
+ model: input.model,
2682
+ modelVersion: input.modelVersion ?? null,
2683
+ prompt: extractPrompt(input.input, input.metadata),
2684
+ input: normalizeCerValue(input.input),
2685
+ output: normalizeCerValue(input.output),
2686
+ parameters: resolveParameters(input.parameters),
2687
+ appId: typeof input.metadata?.appId === "string" ? input.metadata.appId : null,
2688
+ runId: typeof input.metadata?.runId === "string" ? input.metadata.runId : void 0,
2689
+ workflowId: typeof input.metadata?.workflowId === "string" ? input.metadata.workflowId : void 0,
2690
+ conversationId: typeof input.metadata?.conversationId === "string" ? input.metadata.conversationId : void 0,
2691
+ meta: buildMeta(input.metadata)
2692
+ };
2693
+ }
2694
+ function createLangChainCer(input) {
2695
+ const executionId = input.executionId ?? crypto6.randomUUID();
2696
+ const bundle = certifyDecision(buildCertifyParams(input, executionId));
2697
+ return {
2698
+ bundle,
2699
+ certificateHash: bundle.certificateHash,
2700
+ executionId: bundle.snapshot.executionId
2701
+ };
2702
+ }
2703
+ function certifyLangChainRun(input, _options) {
2704
+ if (input.nodeUrl && input.apiKey) {
2705
+ const executionId = input.executionId ?? crypto6.randomUUID();
2706
+ const certifyParams = buildCertifyParams({ ...input, executionId }, executionId);
2707
+ const attestFn = _options?._attestFn ?? certifyAndAttestDecision;
2708
+ return attestFn(certifyParams, { nodeUrl: input.nodeUrl, apiKey: input.apiKey }).then(
2709
+ ({ bundle, receipt }) => ({
2710
+ bundle,
2711
+ receipt,
2712
+ certificateHash: bundle.certificateHash,
2713
+ executionId: bundle.snapshot.executionId,
2714
+ attested: true
2715
+ })
2716
+ );
2717
+ }
2718
+ return createLangChainCer(input);
2719
+ }
2543
2720
  // Annotate the CommonJS export names for ESM import in node:
2544
2721
  0 && (module.exports = {
2545
2722
  CerAttestationError,
2546
2723
  CerVerificationError,
2547
2724
  CerVerifyCode,
2725
+ ReasonCode,
2548
2726
  RunBuilder,
2549
2727
  attest,
2550
2728
  attestIfNeeded,
@@ -2552,9 +2730,11 @@ async function certifyAndAttestRun(steps, options) {
2552
2730
  certifyAndAttestRun,
2553
2731
  certifyDecision,
2554
2732
  certifyDecisionFromProviderCall,
2733
+ certifyLangChainRun,
2555
2734
  computeInputHash,
2556
2735
  computeOutputHash,
2557
2736
  createClient,
2737
+ createLangChainCer,
2558
2738
  createSnapshot,
2559
2739
  exportCer,
2560
2740
  exportVerifiableRedacted,
@@ -2577,6 +2757,7 @@ async function certifyAndAttestRun(steps, options) {
2577
2757
  toCanonicalJson,
2578
2758
  validateProfile,
2579
2759
  verify,
2760
+ verifyAiCerBundleDetailed,
2580
2761
  verifyAief,
2581
2762
  verifyBundleAttestation,
2582
2763
  verifyCer,