@nexart/ai-execution 0.10.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 CHANGED
@@ -1,4 +1,4 @@
1
- # @nexart/ai-execution v0.10.0
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.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
 
@@ -674,7 +686,7 @@ Priority when multiple failures exist: `CANONICALIZATION_ERROR` > `SCHEMA_ERROR`
674
686
 
675
687
  ## LangChain Integration
676
688
 
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.
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.
678
690
 
679
691
  `certifyLangChainRun` operates in two modes depending on whether `nodeUrl` and `apiKey` are supplied:
680
692
 
@@ -747,6 +759,8 @@ console.log(result.receipt);
747
759
 
748
760
  ### Three helpers
749
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
+
750
764
  | Helper | Returns | Network |
751
765
  |---|---|---|
752
766
  | `createLangChainCer(input)` | `LangChainCerResult` (sync) | Never |
@@ -834,6 +848,99 @@ const result = await certifyLangChainRun(
834
848
 
835
849
  ---
836
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
+
837
944
  ## Version History
838
945
 
839
946
  | Version | Description |
@@ -849,7 +956,8 @@ const result = await certifyLangChainRun(
849
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 |
850
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 |
851
958
  | 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 |
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 |
853
961
  | v1.0.0 | Planned: API stabilization, freeze public API surface |
854
962
 
855
963
  ## Releasing
package/dist/index.cjs CHANGED
@@ -339,14 +339,22 @@ function computeCertificateHash(payload) {
339
339
  const canonical = toCanonicalJson(payload);
340
340
  return `sha256:${sha256Hex(canonical)}`;
341
341
  }
342
+ function buildContext(signals) {
343
+ if (!signals || signals.length === 0) return void 0;
344
+ return { signals };
345
+ }
342
346
  function sealCer(snapshot, options) {
343
347
  const createdAt = options?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
348
+ const context = buildContext(options?.signals);
344
349
  const payload = {
345
350
  bundleType: "cer.ai.execution.v1",
346
351
  createdAt,
347
352
  snapshot,
348
353
  version: "0.1"
349
354
  };
355
+ if (context) {
356
+ payload.context = context;
357
+ }
350
358
  const certificateHash = computeCertificateHash(payload);
351
359
  const bundle = {
352
360
  bundleType: "cer.ai.execution.v1",
@@ -355,6 +363,9 @@ function sealCer(snapshot, options) {
355
363
  version: "0.1",
356
364
  snapshot
357
365
  };
366
+ if (context) {
367
+ bundle.context = context;
368
+ }
358
369
  if (options?.meta) {
359
370
  bundle.meta = options.meta;
360
371
  }
@@ -403,6 +414,10 @@ function verifyCer(bundle) {
403
414
  snapshot: bundle.snapshot,
404
415
  version: "0.1"
405
416
  };
417
+ const verifyContext = buildContext(bundle.context?.signals);
418
+ if (verifyContext) {
419
+ payload.context = verifyContext;
420
+ }
406
421
  const expectedHash = computeCertificateHash(payload);
407
422
  if (bundle.certificateHash !== expectedHash) {
408
423
  certHashErrors.push(`certificateHash mismatch: expected ${expectedHash}, got ${bundle.certificateHash}`);
@@ -463,7 +478,11 @@ function certifyDecision(params) {
463
478
  conversationId: params.conversationId,
464
479
  prevStepHash: params.prevStepHash
465
480
  });
466
- return sealCer(snapshot, { createdAt: params.createdAt, meta: params.meta });
481
+ return sealCer(snapshot, {
482
+ createdAt: params.createdAt,
483
+ meta: params.meta,
484
+ signals: params.signals
485
+ });
467
486
  }
468
487
 
469
488
  // src/run.ts
@@ -2688,7 +2707,8 @@ function buildCertifyParams(input, executionId) {
2688
2707
  runId: typeof input.metadata?.runId === "string" ? input.metadata.runId : void 0,
2689
2708
  workflowId: typeof input.metadata?.workflowId === "string" ? input.metadata.workflowId : void 0,
2690
2709
  conversationId: typeof input.metadata?.conversationId === "string" ? input.metadata.conversationId : void 0,
2691
- meta: buildMeta(input.metadata)
2710
+ meta: buildMeta(input.metadata),
2711
+ signals: input.signals
2692
2712
  };
2693
2713
  }
2694
2714
  function createLangChainCer(input) {