@nexart/ai-execution 0.9.0 → 0.11.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 +277 -5
- package/dist/index.cjs +118 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.mjs +116 -1
- package/dist/index.mjs.map +1 -1
- package/dist/langchain.cjs +496 -0
- package/dist/langchain.cjs.map +1 -0
- package/dist/langchain.d.cts +194 -0
- package/dist/langchain.d.ts +194 -0
- package/dist/langchain.mjs +460 -0
- package/dist/langchain.mjs.map +1 -0
- package/dist/providers/anthropic.cjs +11 -0
- package/dist/providers/anthropic.cjs.map +1 -1
- package/dist/providers/anthropic.d.cts +1 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.mjs +11 -0
- package/dist/providers/anthropic.mjs.map +1 -1
- package/dist/providers/openai.cjs +11 -0
- package/dist/providers/openai.cjs.map +1 -1
- package/dist/providers/openai.d.cts +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.mjs +11 -0
- package/dist/providers/openai.mjs.map +1 -1
- package/dist/providers/wrap.cjs +11 -0
- package/dist/providers/wrap.cjs.map +1 -1
- package/dist/providers/wrap.d.cts +1 -1
- package/dist/providers/wrap.d.ts +1 -1
- package/dist/providers/wrap.mjs +11 -0
- package/dist/providers/wrap.mjs.map +1 -1
- package/dist/{types-C5t12OK8.d.cts → types-C_M2xSWK.d.cts} +49 -1
- package/dist/{types-C5t12OK8.d.ts → types-C_M2xSWK.d.ts} +49 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @nexart/ai-execution v0.
|
|
1
|
+
# @nexart/ai-execution v0.11.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.
|
|
10
|
+
| SDK | 0.11.0 |
|
|
11
11
|
| Protocol | 1.2.0 |
|
|
12
12
|
|
|
13
13
|
## Why Not Just Store Logs?
|
|
@@ -22,6 +22,7 @@ This package creates integrity records for AI executions. Every time you call an
|
|
|
22
22
|
- What you got back (output)
|
|
23
23
|
- The exact parameters used (temperature, model, etc.)
|
|
24
24
|
- SHA-256 hashes of everything for tamper detection
|
|
25
|
+
- Optionally: upstream context signals from other systems, sealed into the same certificate hash (v0.10.0+)
|
|
25
26
|
|
|
26
27
|
These records can be verified offline to detect any post-hoc modification and prove integrity of the recorded execution.
|
|
27
28
|
|
|
@@ -168,6 +169,13 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
|
|
|
168
169
|
"createdAt": "2026-02-12T00:00:00.000Z",
|
|
169
170
|
"version": "0.1",
|
|
170
171
|
"snapshot": { "..." : "..." },
|
|
172
|
+
"context": {
|
|
173
|
+
"signals": [
|
|
174
|
+
{ "type": "approval", "source": "github-actions", "step": 0,
|
|
175
|
+
"timestamp": "2026-02-12T00:00:00.000Z", "actor": "alice",
|
|
176
|
+
"status": "ok", "payload": { "pr": 42 } }
|
|
177
|
+
]
|
|
178
|
+
},
|
|
171
179
|
"meta": { "source": "my-app", "tags": ["production"] },
|
|
172
180
|
"declaration": {
|
|
173
181
|
"stabilitySchemeId": "nexart-cer-v1",
|
|
@@ -178,9 +186,13 @@ Auto-generated fields (set by `createSnapshot`, do not set manually): `type`, `p
|
|
|
178
186
|
}
|
|
179
187
|
```
|
|
180
188
|
|
|
189
|
+
`context` is **optional** and only appears when signals are supplied. Bundles without `context` are structurally identical to all prior versions.
|
|
190
|
+
|
|
181
191
|
### Certificate Hash Computation
|
|
182
192
|
|
|
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.
|
|
193
|
+
The `certificateHash` is SHA-256 of the UTF-8 bytes of the canonical JSON of exactly: `{ bundleType, version, createdAt, snapshot }` — or, when context signals are present: `{ bundleType, version, createdAt, snapshot, context }`. `meta` and `declaration` are always excluded. Key-ordering is recursive. This computation is identical across all SDK versions.
|
|
194
|
+
|
|
195
|
+
**Context and backward compatibility:** when no signals are provided (or an empty array is passed), `context` is omitted from the hash payload entirely. The resulting `certificateHash` is byte-for-byte identical to a pre-v0.10.0 bundle with the same snapshot. Passing signals is strictly additive — it can only extend the hash, never break it for existing callers.
|
|
184
196
|
|
|
185
197
|
### Declaration Block (v0.7.0+)
|
|
186
198
|
|
|
@@ -582,7 +594,7 @@ Fixtures at `fixtures/vectors/` and `fixtures/golden/`. Cross-language implement
|
|
|
582
594
|
| `verifySnapshot(snapshot)` | Verify snapshot hashes and structure |
|
|
583
595
|
| `sealCer(snapshot, options?)` | Seal snapshot into CER bundle |
|
|
584
596
|
| `verify(bundle)` / `verifyCer(bundle)` | Verify CER bundle |
|
|
585
|
-
| `certifyDecision(params)` | One-call: createSnapshot + sealCer |
|
|
597
|
+
| `certifyDecision(params)` | One-call: createSnapshot + sealCer. Accepts optional `signals?: CerContextSignal[]` — included in `certificateHash` when present (v0.10.0+) |
|
|
586
598
|
|
|
587
599
|
### Workflow
|
|
588
600
|
|
|
@@ -672,6 +684,263 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
|
|
|
672
684
|
| `runAnthropicExecution` | `@nexart/ai-execution/providers/anthropic` |
|
|
673
685
|
| `wrapProvider` | `@nexart/ai-execution/providers/wrap` |
|
|
674
686
|
|
|
687
|
+
## LangChain Integration
|
|
688
|
+
|
|
689
|
+
`@nexart/ai-execution` includes a minimal LangChain helper surface (introduced v0.10.0, current v0.11.0). Certify the final input and output of any LangChain chain, agent, or runnable as a tamper-evident CER — no LangChain package dependency required.
|
|
690
|
+
|
|
691
|
+
`certifyLangChainRun` operates in two modes depending on whether `nodeUrl` and `apiKey` are supplied:
|
|
692
|
+
|
|
693
|
+
| Mode | How to call | Returns |
|
|
694
|
+
|---|---|---|
|
|
695
|
+
| **Local** (default) | No `nodeUrl` / `apiKey` | `LangChainCerResult` (sync) |
|
|
696
|
+
| **Node-attested** | `nodeUrl` + `apiKey` on input | `Promise<LangChainAttestedResult>` |
|
|
697
|
+
|
|
698
|
+
### Mode 1 — Local CER creation (synchronous, no network)
|
|
699
|
+
|
|
700
|
+
`createLangChainCer` is always local. `certifyLangChainRun` without `nodeUrl`/`apiKey` is identical.
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
import { createLangChainCer, certifyLangChainRun } from '@nexart/ai-execution';
|
|
704
|
+
// or: import { ... } from '@nexart/ai-execution/langchain';
|
|
705
|
+
|
|
706
|
+
// Using createLangChainCer — always explicit about local-only behaviour
|
|
707
|
+
const { bundle, certificateHash, executionId } = createLangChainCer({
|
|
708
|
+
executionId: 'run-001', // optional — UUID generated if omitted
|
|
709
|
+
provider: 'openai',
|
|
710
|
+
model: 'gpt-4o-mini',
|
|
711
|
+
input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
|
|
712
|
+
output: { text: 'Paris.' },
|
|
713
|
+
metadata: { appId: 'my-app', projectId: 'docs-bot' },
|
|
714
|
+
parameters: { temperature: 0, maxTokens: 200, topP: null, seed: null },
|
|
715
|
+
createdAt: new Date().toISOString(), // pin for deterministic hash
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
console.log(certificateHash); // sha256:...
|
|
719
|
+
|
|
720
|
+
// certifyLangChainRun without nodeUrl/apiKey: identical, sync
|
|
721
|
+
const { bundle: b2 } = certifyLangChainRun({
|
|
722
|
+
provider: 'openai', model: 'gpt-4o-mini',
|
|
723
|
+
input: { messages: [{ role: 'user', content: 'Summarise this.' }] },
|
|
724
|
+
output: { text: 'Summary...' },
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Mode 2 — Node-attested certification (async)
|
|
729
|
+
|
|
730
|
+
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.
|
|
731
|
+
|
|
732
|
+
```ts
|
|
733
|
+
import { certifyLangChainRun } from '@nexart/ai-execution';
|
|
734
|
+
|
|
735
|
+
const result = await certifyLangChainRun({
|
|
736
|
+
executionId: 'run-001',
|
|
737
|
+
provider: 'openai',
|
|
738
|
+
model: 'gpt-4o-mini',
|
|
739
|
+
input: { messages: [{ role: 'user', content: 'What is the capital of France?' }] },
|
|
740
|
+
output: { text: 'Paris.' },
|
|
741
|
+
metadata: { appId: 'my-app', projectId: 'docs-bot' },
|
|
742
|
+
createdAt: new Date().toISOString(),
|
|
743
|
+
nodeUrl: 'https://node.nexart.io', // ← triggers attested mode
|
|
744
|
+
apiKey: process.env.NEXART_API_KEY!, // ← required with nodeUrl
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
console.log(result.certificateHash); // sha256:... (same semantics as local)
|
|
748
|
+
console.log(result.attested); // true
|
|
749
|
+
console.log(result.receipt);
|
|
750
|
+
// {
|
|
751
|
+
// attestationId: "nxa_attest_...",
|
|
752
|
+
// certificateHash: "sha256:...",
|
|
753
|
+
// nodeRuntimeHash: "sha256:...",
|
|
754
|
+
// protocolVersion: "1.2.0"
|
|
755
|
+
// }
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
`result.bundle` passes `verifyCer()` identically to local mode — the `certificateHash` covers only the CER content, not the receipt fields.
|
|
759
|
+
|
|
760
|
+
### Three helpers
|
|
761
|
+
|
|
762
|
+
All three helpers accept an optional `signals?: CerContextSignal[]` field on their input. When provided, signals are sealed into `bundle.context` and included in `certificateHash`. Omitting signals produces a hash identical to a bundle without signals.
|
|
763
|
+
|
|
764
|
+
| Helper | Returns | Network |
|
|
765
|
+
|---|---|---|
|
|
766
|
+
| `createLangChainCer(input)` | `LangChainCerResult` (sync) | Never |
|
|
767
|
+
| `certifyLangChainRun(input)` without `nodeUrl`/`apiKey` | `LangChainCerResult` (sync) | Never |
|
|
768
|
+
| `certifyLangChainRun({ ...input, nodeUrl, apiKey })` | `Promise<LangChainAttestedResult>` | Yes — NexArt node |
|
|
769
|
+
|
|
770
|
+
### Result types
|
|
771
|
+
|
|
772
|
+
```ts
|
|
773
|
+
interface LangChainCerResult {
|
|
774
|
+
bundle: CerAiExecutionBundle;
|
|
775
|
+
certificateHash: string;
|
|
776
|
+
executionId: string;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
interface LangChainAttestedResult extends LangChainCerResult {
|
|
780
|
+
receipt: AttestationReceipt;
|
|
781
|
+
attested: true;
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Input normalization
|
|
786
|
+
|
|
787
|
+
| Input shape | Stored in CER as |
|
|
788
|
+
|---|---|
|
|
789
|
+
| `string` | `string` (pass-through) |
|
|
790
|
+
| Plain object `{}` | `Record<string, unknown>` (pass-through) |
|
|
791
|
+
| Array `[]` | `{ items: [...] }` |
|
|
792
|
+
| Anything else | `String(value)` |
|
|
793
|
+
|
|
794
|
+
### Prompt extraction
|
|
795
|
+
|
|
796
|
+
Resolved in this order: `metadata.prompt` → `input.messages[0].content` → `input.prompt` → `input.text` → `"[LangChain run]"`.
|
|
797
|
+
|
|
798
|
+
### Metadata mapping
|
|
799
|
+
|
|
800
|
+
| Metadata key | Mapped to |
|
|
801
|
+
|---|---|
|
|
802
|
+
| `appId` | `snapshot.appId` |
|
|
803
|
+
| `runId` | `snapshot.runId` |
|
|
804
|
+
| `workflowId` | `snapshot.workflowId` |
|
|
805
|
+
| `conversationId` | `snapshot.conversationId` |
|
|
806
|
+
| `prompt` | Used as the CER `prompt` field |
|
|
807
|
+
| All other keys | `bundle.meta.tags` (as `"key:value"` strings) |
|
|
808
|
+
|
|
809
|
+
### Verification
|
|
810
|
+
|
|
811
|
+
Bundles from all three helpers are fully protocol-aligned:
|
|
812
|
+
|
|
813
|
+
```ts
|
|
814
|
+
import { verifyCer, verifyAiCerBundleDetailed } from '@nexart/ai-execution';
|
|
815
|
+
|
|
816
|
+
const basic = verifyCer(bundle);
|
|
817
|
+
// { ok: true, code: 'OK' }
|
|
818
|
+
|
|
819
|
+
const detailed = verifyAiCerBundleDetailed(bundle);
|
|
820
|
+
// { status: 'VERIFIED', checks: { bundleIntegrity: 'PASS', nodeSignature: 'SKIPPED' }, ... }
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
### Testing the attested path without a network
|
|
824
|
+
|
|
825
|
+
Use the injectable `_attestFn` test option (same pattern as `certifyAndAttestRun`'s `attestStep`):
|
|
826
|
+
|
|
827
|
+
```ts
|
|
828
|
+
import type { AttestDecisionFn } from '@nexart/ai-execution';
|
|
829
|
+
|
|
830
|
+
const mockAttest: AttestDecisionFn = async (params, _opts) => {
|
|
831
|
+
const { certifyDecision } = await import('@nexart/ai-execution');
|
|
832
|
+
const bundle = certifyDecision(params);
|
|
833
|
+
return { bundle, receipt: { attestationId: 'mock-123', certificateHash: bundle.certificateHash,
|
|
834
|
+
nodeRuntimeHash: 'sha256:' + 'beef'.repeat(16), protocolVersion: '1.2.0' } };
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const result = await certifyLangChainRun(
|
|
838
|
+
{ ...input, nodeUrl: 'https://node.nexart.io', apiKey: 'nxa_test' },
|
|
839
|
+
{ _attestFn: mockAttest },
|
|
840
|
+
);
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### V3 roadmap (not yet implemented)
|
|
844
|
+
|
|
845
|
+
- `BaseCallbackHandler` integration for automatic mid-chain CER capture at `onLLMEnd`
|
|
846
|
+
- LangSmith trace correlation via `metadata.runId` → `snapshot.conversationId`
|
|
847
|
+
- Separate `@nexart/langchain` package once the callback surface is proven in production
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## Context Signals (v0.10.0+)
|
|
852
|
+
|
|
853
|
+
CERs can optionally include **context signals** — structured, protocol-agnostic records of upstream events that were present when the AI execution ran. Signals are evidence-only: they have no effect on how the AI call is made, what parameters are used, or how the output is processed. Their only role is to be bound into the `certificateHash` alongside the execution record so that any post-hoc modification to either the signals or the snapshot is detectable.
|
|
854
|
+
|
|
855
|
+
**What signals are:**
|
|
856
|
+
- Records of things that happened before or alongside the AI call (approvals, deploys, audits, tool invocations, review outcomes, etc.)
|
|
857
|
+
- Protocol-agnostic — `type`, `source`, `actor`, and `payload` are all free-form strings and objects
|
|
858
|
+
- Included in `certificateHash` only when present — omitting signals produces a hash identical to pre-v0.10.0
|
|
859
|
+
|
|
860
|
+
**What signals are not:**
|
|
861
|
+
- Governance enforcement — NexArt does not interpret signal content or enforce policy based on it
|
|
862
|
+
- Execution dependencies — signals do not gate or modify the AI call in any way
|
|
863
|
+
- A replacement for `toolCalls` — `toolCalls` records tool invocations *within* a run; signals record upstream context *around* a run
|
|
864
|
+
|
|
865
|
+
### Quick example
|
|
866
|
+
|
|
867
|
+
```ts
|
|
868
|
+
import { createSignalCollector } from '@nexart/signals';
|
|
869
|
+
import { certifyDecision, verifyCer } from '@nexart/ai-execution';
|
|
870
|
+
|
|
871
|
+
// 1. Collect upstream signals during your pipeline run
|
|
872
|
+
const collector = createSignalCollector({ defaultSource: 'github-actions' });
|
|
873
|
+
collector.add({ type: 'approval', actor: 'alice', status: 'ok', payload: { pr: 42 } });
|
|
874
|
+
collector.add({ type: 'deploy', actor: 'ci-bot', status: 'ok', payload: { env: 'prod' } });
|
|
875
|
+
const { signals } = collector.export();
|
|
876
|
+
|
|
877
|
+
// 2. Certify the AI execution with signals bound as context evidence
|
|
878
|
+
const bundle = certifyDecision({
|
|
879
|
+
provider: 'openai',
|
|
880
|
+
model: 'gpt-4o-mini',
|
|
881
|
+
prompt: 'Summarise.',
|
|
882
|
+
input: userQuery,
|
|
883
|
+
output: llmResponse,
|
|
884
|
+
parameters: { temperature: 0, maxTokens: 512, topP: null, seed: null },
|
|
885
|
+
signals, // ← included in certificateHash; omit to get identical hash as before
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
verifyCer(bundle).ok; // true
|
|
889
|
+
console.log(bundle.context?.signals.length); // 2 — signals are sealed in the bundle
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
`NexArtSignal[]` from `@nexart/signals` is structurally identical to `CerContextSignal[]` — no casting or conversion needed. `@nexart/ai-execution` has no hard dependency on `@nexart/signals`; any object matching the `CerContextSignal` shape works.
|
|
893
|
+
|
|
894
|
+
### Signals also work on the LangChain path
|
|
895
|
+
|
|
896
|
+
```ts
|
|
897
|
+
import { certifyLangChainRun } from '@nexart/ai-execution';
|
|
898
|
+
|
|
899
|
+
const { bundle } = certifyLangChainRun({
|
|
900
|
+
provider: 'openai',
|
|
901
|
+
model: 'gpt-4o-mini',
|
|
902
|
+
input: { messages: [...] },
|
|
903
|
+
output: { text: '...' },
|
|
904
|
+
signals, // ← same field, same semantics
|
|
905
|
+
});
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
### CerContextSignal shape
|
|
909
|
+
|
|
910
|
+
Every `CerContextSignal` has the following fields. All are always present in the stored bundle — no undefined values:
|
|
911
|
+
|
|
912
|
+
| Field | Type | Default | Description |
|
|
913
|
+
|---|---|---|---|
|
|
914
|
+
| `type` | `string` | required | Signal category — free-form (e.g. `"approval"`, `"deploy"`, `"audit"`) |
|
|
915
|
+
| `source` | `string` | required | Upstream system — free-form (e.g. `"github-actions"`, `"linear"`) |
|
|
916
|
+
| `step` | `number` | `0` | Position in sequence — used for ordering within the `context.signals` array |
|
|
917
|
+
| `timestamp` | `string` | current time | ISO 8601 |
|
|
918
|
+
| `actor` | `string` | `"unknown"` | Who produced the signal — free-form |
|
|
919
|
+
| `status` | `string` | `"ok"` | Outcome — free-form (e.g. `"ok"`, `"error"`, `"pending"`) |
|
|
920
|
+
| `payload` | `Record<string, unknown>` | `{}` | Opaque upstream data — NexArt does not interpret this |
|
|
921
|
+
|
|
922
|
+
Signal ordering within the array is part of the sealed hash. Reordering signals produces a different `certificateHash`.
|
|
923
|
+
|
|
924
|
+
### Tamper detection
|
|
925
|
+
|
|
926
|
+
`verifyCer()` always reconstructs `context` from the stored `bundle.context.signals` when recomputing the hash. Any mutation — changing a signal field, adding or removing signals, or injecting a `context` block into a no-signals bundle — produces a `CERTIFICATE_HASH_MISMATCH` result:
|
|
927
|
+
|
|
928
|
+
```ts
|
|
929
|
+
const tampered = { ...bundle, context: { signals: [...altered] } };
|
|
930
|
+
verifyCer(tampered).ok; // false
|
|
931
|
+
verifyCer(tampered).code; // 'CERTIFICATE_HASH_MISMATCH'
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### Exported types
|
|
935
|
+
|
|
936
|
+
```ts
|
|
937
|
+
import type { CerContextSignal, CerContext } from '@nexart/ai-execution';
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
`CerContext` is the shape of `bundle.context` — `{ signals: CerContextSignal[] }`. Both are exported from the package root.
|
|
941
|
+
|
|
942
|
+
---
|
|
943
|
+
|
|
675
944
|
## Version History
|
|
676
945
|
|
|
677
946
|
| Version | Description |
|
|
@@ -685,7 +954,10 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
|
|
|
685
954
|
| v0.5.0 | Ed25519 signed receipt verification: `verifyNodeReceiptSignature`, `verifyBundleAttestation`, `fetchNodeKeys`, `selectNodeKey`; new attestation `CerVerifyCode` entries; `SPEC.md`; `NodeKeysDocument`, `SignedAttestationReceipt`, `NodeReceiptVerifyResult` types |
|
|
686
955
|
| 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
956
|
| 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
|
-
|
|
|
957
|
+
| 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 |
|
|
958
|
+
| v0.9.0 | CER Protocol types: `CerVerificationResult`, `ReasonCode`, `CheckStatus`; `verifyAiCerBundleDetailed()`; `CertifyDecisionParams.createdAt` wired through; determinism bug fix |
|
|
959
|
+
| 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. **Context signals:** optional `signals?: CerContextSignal[]` on `certifyDecision`, `certifyLangChainRun`, and `createLangChainCer` — sealed into `bundle.context` and included in `certificateHash` when non-empty; absent or empty = hash unchanged from pre-v0.10.0. New types: `CerContextSignal`, `CerContext`. New example: `examples/signals-cer.ts`. 413 total tests; all prior bundles verify identically |
|
|
960
|
+
| **v0.11.0** | Version release. Ships all v0.10.0 features (LangChain integration + context signals) as the published package version. `cerSignals.test.js` added to the `npm test` script. No API, hash, or canonicalization changes |
|
|
689
961
|
| v1.0.0 | Planned: API stabilization, freeze public API surface |
|
|
690
962
|
|
|
691
963
|
## 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,
|
|
@@ -337,14 +339,22 @@ function computeCertificateHash(payload) {
|
|
|
337
339
|
const canonical = toCanonicalJson(payload);
|
|
338
340
|
return `sha256:${sha256Hex(canonical)}`;
|
|
339
341
|
}
|
|
342
|
+
function buildContext(signals) {
|
|
343
|
+
if (!signals || signals.length === 0) return void 0;
|
|
344
|
+
return { signals };
|
|
345
|
+
}
|
|
340
346
|
function sealCer(snapshot, options) {
|
|
341
347
|
const createdAt = options?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
348
|
+
const context = buildContext(options?.signals);
|
|
342
349
|
const payload = {
|
|
343
350
|
bundleType: "cer.ai.execution.v1",
|
|
344
351
|
createdAt,
|
|
345
352
|
snapshot,
|
|
346
353
|
version: "0.1"
|
|
347
354
|
};
|
|
355
|
+
if (context) {
|
|
356
|
+
payload.context = context;
|
|
357
|
+
}
|
|
348
358
|
const certificateHash = computeCertificateHash(payload);
|
|
349
359
|
const bundle = {
|
|
350
360
|
bundleType: "cer.ai.execution.v1",
|
|
@@ -353,6 +363,9 @@ function sealCer(snapshot, options) {
|
|
|
353
363
|
version: "0.1",
|
|
354
364
|
snapshot
|
|
355
365
|
};
|
|
366
|
+
if (context) {
|
|
367
|
+
bundle.context = context;
|
|
368
|
+
}
|
|
356
369
|
if (options?.meta) {
|
|
357
370
|
bundle.meta = options.meta;
|
|
358
371
|
}
|
|
@@ -401,6 +414,10 @@ function verifyCer(bundle) {
|
|
|
401
414
|
snapshot: bundle.snapshot,
|
|
402
415
|
version: "0.1"
|
|
403
416
|
};
|
|
417
|
+
const verifyContext = buildContext(bundle.context?.signals);
|
|
418
|
+
if (verifyContext) {
|
|
419
|
+
payload.context = verifyContext;
|
|
420
|
+
}
|
|
404
421
|
const expectedHash = computeCertificateHash(payload);
|
|
405
422
|
if (bundle.certificateHash !== expectedHash) {
|
|
406
423
|
certHashErrors.push(`certificateHash mismatch: expected ${expectedHash}, got ${bundle.certificateHash}`);
|
|
@@ -461,7 +478,11 @@ function certifyDecision(params) {
|
|
|
461
478
|
conversationId: params.conversationId,
|
|
462
479
|
prevStepHash: params.prevStepHash
|
|
463
480
|
});
|
|
464
|
-
return sealCer(snapshot, {
|
|
481
|
+
return sealCer(snapshot, {
|
|
482
|
+
createdAt: params.createdAt,
|
|
483
|
+
meta: params.meta,
|
|
484
|
+
signals: params.signals
|
|
485
|
+
});
|
|
465
486
|
}
|
|
466
487
|
|
|
467
488
|
// src/run.ts
|
|
@@ -2622,6 +2643,100 @@ function verifyAiCerBundleDetailed(bundle) {
|
|
|
2622
2643
|
verifier
|
|
2623
2644
|
};
|
|
2624
2645
|
}
|
|
2646
|
+
|
|
2647
|
+
// src/langchain.ts
|
|
2648
|
+
var crypto6 = __toESM(require("crypto"), 1);
|
|
2649
|
+
function normalizeCerValue(value) {
|
|
2650
|
+
if (typeof value === "string") return value;
|
|
2651
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
2652
|
+
return value;
|
|
2653
|
+
}
|
|
2654
|
+
if (Array.isArray(value)) return { items: value };
|
|
2655
|
+
return String(value);
|
|
2656
|
+
}
|
|
2657
|
+
function extractPrompt(input, metadata) {
|
|
2658
|
+
if (typeof metadata?.prompt === "string" && metadata.prompt.length > 0) {
|
|
2659
|
+
return metadata.prompt;
|
|
2660
|
+
}
|
|
2661
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
2662
|
+
const obj = input;
|
|
2663
|
+
if (Array.isArray(obj.messages) && obj.messages.length > 0) {
|
|
2664
|
+
const first = obj.messages[0];
|
|
2665
|
+
if (typeof first?.content === "string" && first.content.length > 0) {
|
|
2666
|
+
return first.content;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
if (typeof obj.prompt === "string" && obj.prompt.length > 0) return obj.prompt;
|
|
2670
|
+
if (typeof obj.text === "string" && obj.text.length > 0) return obj.text;
|
|
2671
|
+
}
|
|
2672
|
+
if (typeof input === "string" && input.length > 0) return input;
|
|
2673
|
+
return "[LangChain run]";
|
|
2674
|
+
}
|
|
2675
|
+
function buildMeta(metadata) {
|
|
2676
|
+
if (!metadata || Object.keys(metadata).length === 0) return void 0;
|
|
2677
|
+
const reserved = /* @__PURE__ */ new Set(["prompt", "appId", "runId", "workflowId", "conversationId"]);
|
|
2678
|
+
const extraTags = [];
|
|
2679
|
+
for (const [k, v] of Object.entries(metadata)) {
|
|
2680
|
+
if (!reserved.has(k)) {
|
|
2681
|
+
extraTags.push(typeof v === "string" ? `${k}:${v}` : `${k}:${JSON.stringify(v)}`);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
return extraTags.length > 0 ? { tags: extraTags } : void 0;
|
|
2685
|
+
}
|
|
2686
|
+
function resolveParameters(params) {
|
|
2687
|
+
return {
|
|
2688
|
+
temperature: params?.temperature ?? 0,
|
|
2689
|
+
maxTokens: params?.maxTokens ?? 0,
|
|
2690
|
+
topP: params?.topP ?? null,
|
|
2691
|
+
seed: params?.seed ?? null
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
function buildCertifyParams(input, executionId) {
|
|
2695
|
+
return {
|
|
2696
|
+
executionId,
|
|
2697
|
+
timestamp: input.timestamp,
|
|
2698
|
+
createdAt: input.createdAt,
|
|
2699
|
+
provider: input.provider,
|
|
2700
|
+
model: input.model,
|
|
2701
|
+
modelVersion: input.modelVersion ?? null,
|
|
2702
|
+
prompt: extractPrompt(input.input, input.metadata),
|
|
2703
|
+
input: normalizeCerValue(input.input),
|
|
2704
|
+
output: normalizeCerValue(input.output),
|
|
2705
|
+
parameters: resolveParameters(input.parameters),
|
|
2706
|
+
appId: typeof input.metadata?.appId === "string" ? input.metadata.appId : null,
|
|
2707
|
+
runId: typeof input.metadata?.runId === "string" ? input.metadata.runId : void 0,
|
|
2708
|
+
workflowId: typeof input.metadata?.workflowId === "string" ? input.metadata.workflowId : void 0,
|
|
2709
|
+
conversationId: typeof input.metadata?.conversationId === "string" ? input.metadata.conversationId : void 0,
|
|
2710
|
+
meta: buildMeta(input.metadata),
|
|
2711
|
+
signals: input.signals
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function createLangChainCer(input) {
|
|
2715
|
+
const executionId = input.executionId ?? crypto6.randomUUID();
|
|
2716
|
+
const bundle = certifyDecision(buildCertifyParams(input, executionId));
|
|
2717
|
+
return {
|
|
2718
|
+
bundle,
|
|
2719
|
+
certificateHash: bundle.certificateHash,
|
|
2720
|
+
executionId: bundle.snapshot.executionId
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
function certifyLangChainRun(input, _options) {
|
|
2724
|
+
if (input.nodeUrl && input.apiKey) {
|
|
2725
|
+
const executionId = input.executionId ?? crypto6.randomUUID();
|
|
2726
|
+
const certifyParams = buildCertifyParams({ ...input, executionId }, executionId);
|
|
2727
|
+
const attestFn = _options?._attestFn ?? certifyAndAttestDecision;
|
|
2728
|
+
return attestFn(certifyParams, { nodeUrl: input.nodeUrl, apiKey: input.apiKey }).then(
|
|
2729
|
+
({ bundle, receipt }) => ({
|
|
2730
|
+
bundle,
|
|
2731
|
+
receipt,
|
|
2732
|
+
certificateHash: bundle.certificateHash,
|
|
2733
|
+
executionId: bundle.snapshot.executionId,
|
|
2734
|
+
attested: true
|
|
2735
|
+
})
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
return createLangChainCer(input);
|
|
2739
|
+
}
|
|
2625
2740
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2626
2741
|
0 && (module.exports = {
|
|
2627
2742
|
CerAttestationError,
|
|
@@ -2635,9 +2750,11 @@ function verifyAiCerBundleDetailed(bundle) {
|
|
|
2635
2750
|
certifyAndAttestRun,
|
|
2636
2751
|
certifyDecision,
|
|
2637
2752
|
certifyDecisionFromProviderCall,
|
|
2753
|
+
certifyLangChainRun,
|
|
2638
2754
|
computeInputHash,
|
|
2639
2755
|
computeOutputHash,
|
|
2640
2756
|
createClient,
|
|
2757
|
+
createLangChainCer,
|
|
2641
2758
|
createSnapshot,
|
|
2642
2759
|
exportCer,
|
|
2643
2760
|
exportVerifiableRedacted,
|