@nexart/ai-execution 0.9.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
@@ -41,9 +41,11 @@ __export(src_exports, {
41
41
  certifyAndAttestRun: () => certifyAndAttestRun,
42
42
  certifyDecision: () => certifyDecision,
43
43
  certifyDecisionFromProviderCall: () => certifyDecisionFromProviderCall,
44
+ certifyLangChainRun: () => certifyLangChainRun,
44
45
  computeInputHash: () => computeInputHash,
45
46
  computeOutputHash: () => computeOutputHash,
46
47
  createClient: () => createClient,
48
+ createLangChainCer: () => createLangChainCer,
47
49
  createSnapshot: () => createSnapshot,
48
50
  exportCer: () => exportCer,
49
51
  exportVerifiableRedacted: () => exportVerifiableRedacted,
@@ -2622,6 +2624,99 @@ function verifyAiCerBundleDetailed(bundle) {
2622
2624
  verifier
2623
2625
  };
2624
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
+ }
2625
2720
  // Annotate the CommonJS export names for ESM import in node:
2626
2721
  0 && (module.exports = {
2627
2722
  CerAttestationError,
@@ -2635,9 +2730,11 @@ function verifyAiCerBundleDetailed(bundle) {
2635
2730
  certifyAndAttestRun,
2636
2731
  certifyDecision,
2637
2732
  certifyDecisionFromProviderCall,
2733
+ certifyLangChainRun,
2638
2734
  computeInputHash,
2639
2735
  computeOutputHash,
2640
2736
  createClient,
2737
+ createLangChainCer,
2641
2738
  createSnapshot,
2642
2739
  exportCer,
2643
2740
  exportVerifiableRedacted,