@fluxpointstudios/orynq-sdk-process-trace 0.1.0 → 0.3.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/src/index.ts CHANGED
@@ -52,6 +52,11 @@ export type {
52
52
  ObservationEvent,
53
53
  ErrorTraceEvent,
54
54
  CustomEvent,
55
+ GovernanceAttestationEvent,
56
+ GovernanceSignatureScheme,
57
+ GovernanceEip712Binding,
58
+ ToolReceiptEvent,
59
+ ToolReceiptScheme,
55
60
  TraceEvent,
56
61
  TraceEventKind,
57
62
  } from "./types.js";
@@ -90,6 +95,9 @@ export type {
90
95
  TraceManifest,
91
96
  } from "./types.js";
92
97
 
98
+ // Model manifest types (pre-execution pinning, issue #59)
99
+ export type { ModelManifest } from "./types.js";
100
+
93
101
  // Disclosure types
94
102
  export type {
95
103
  DisclosureMode,
@@ -214,6 +222,12 @@ export {
214
222
  countSpansByVisibility,
215
223
  } from "./bundle.js";
216
224
 
225
+ // Bundle verification option types (governance + tool-receipt hooks)
226
+ export type {
227
+ VerifyBundleOptions,
228
+ ToolReceiptVerifyOutcome,
229
+ } from "./bundle.js";
230
+
217
231
  // ---------------------------------------------------------------------------
218
232
  // Disclosure Exports
219
233
  // ---------------------------------------------------------------------------
@@ -254,6 +268,61 @@ export {
254
268
  parseChunkContent,
255
269
  } from "./manifest.js";
256
270
 
271
+ // ---------------------------------------------------------------------------
272
+ // Model Manifest Exports (pre-execution pinning, issue #59)
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export {
276
+ // Hash + validation
277
+ computeModelManifestHash,
278
+ validateModelManifest,
279
+ freezeModelManifest,
280
+
281
+ // Framework builders
282
+ manifestFromHuggingFace,
283
+ manifestFromOpenAI,
284
+ manifestFromAnthropic,
285
+ manifestFromCheckpoint,
286
+ } from "./model-manifest.js";
287
+
288
+ export type {
289
+ HuggingFaceManifestOptions,
290
+ OpenAIManifestOptions,
291
+ AnthropicManifestOptions,
292
+ CheckpointManifestOptions,
293
+ } from "./model-manifest.js";
294
+
295
+ // ---------------------------------------------------------------------------
296
+ // Governance Attestation Exports (issue #58)
297
+ // ---------------------------------------------------------------------------
298
+
299
+ export {
300
+ // Preimage + helper
301
+ governanceAttestationPreimage,
302
+ addGovernanceAttestation,
303
+
304
+ // Built-in signers (sr25519 / ed25519 via optional @polkadot peer dep)
305
+ createSr25519GovernanceSigner,
306
+ createEd25519GovernanceSigner,
307
+ SS58_PREFIX,
308
+
309
+ // Verification
310
+ verifyGovernanceAttestations,
311
+ createEip712GovernanceVerifier,
312
+ } from "./governance.js";
313
+
314
+ export type {
315
+ GovernanceSigner,
316
+ GovernanceSignContext,
317
+ GovernanceAttestationFields,
318
+ GovernanceVerifier,
319
+ GovernanceVerifyContext,
320
+ SubstrateGovernanceSignerOptions,
321
+ AddGovernanceAttestationOptions,
322
+ GovernanceAttestationSummary,
323
+ VerifyGovernanceOptions,
324
+ } from "./governance.js";
325
+
257
326
  // ---------------------------------------------------------------------------
258
327
  // Version
259
328
  // ---------------------------------------------------------------------------
Binary file
@@ -284,30 +284,34 @@ function constantTimeCompare(a: string, b: string): boolean {
284
284
  // -----------------------------------------------------------------------------
285
285
 
286
286
  /**
287
- * Compute the final root hash from rolling hash and span hashes.
287
+ * Compute the final root hash from rolling hash, span hashes, and (when pinned)
288
+ * the model-manifest commitment.
288
289
  *
289
290
  * The root hash is computed as:
290
- * `H("poi-trace:root:v1|" + rollingHash + "|" + spanHash1 + "|" + spanHash2 + ...)`
291
+ * `H("poi-trace:root:v1|" + rollingHash + "|" + spanHash1 + "|" + ... [+ "|manifest:" + modelManifestHash])`
291
292
  *
292
293
  * Spans are sorted by their `spanSeq` field before joining to ensure
293
- * deterministic ordering. This creates a single commitment that covers
294
- * both the event sequence (via rolling hash) and the span structure.
294
+ * deterministic ordering. Folding `modelManifestHash` in binds the pinned
295
+ * model/data manifest to the committed root (issue #59): swapping the manifest
296
+ * after commitment changes the recomputed root and fails verification.
295
297
  *
296
298
  * @param rollingHash - The final rolling hash from all events
297
299
  * @param spans - Array of trace spans (must have hash field populated)
300
+ * @param modelManifestHash - Optional pre-execution model-manifest commitment to bind
298
301
  * @returns Promise resolving to the root hash as a hex string
299
302
  *
300
303
  * @example
301
304
  * ```typescript
302
305
  * const rollingHash = await computeRollingHash(events);
303
- * const rootHash = await computeRootHash(rollingHash, spans);
306
+ * const rootHash = await computeRootHash(rollingHash, spans, run.modelManifestHash);
304
307
  *
305
308
  * // rootHash can now be published as the trace commitment
306
309
  * ```
307
310
  */
308
311
  export async function computeRootHash(
309
312
  rollingHash: string,
310
- spans: TraceSpan[]
313
+ spans: TraceSpan[],
314
+ modelManifestHash?: string
311
315
  ): Promise<string> {
312
316
  // Sort spans by spanSeq for deterministic ordering
313
317
  const sortedSpans = [...spans].sort((a, b) => a.spanSeq - b.spanSeq);
@@ -328,6 +332,11 @@ export async function computeRootHash(
328
332
  input += "|" + spanHashes.join("|");
329
333
  }
330
334
 
335
+ // Bind the pinned model-manifest commitment into the committed root (#59).
336
+ if (modelManifestHash !== undefined && modelManifestHash.length > 0) {
337
+ input += "|manifest:" + modelManifestHash;
338
+ }
339
+
331
340
  return sha256StringHex(input);
332
341
  }
333
342
 
@@ -59,6 +59,11 @@ import {
59
59
  computeRootHash,
60
60
  } from "./rolling-hash.js";
61
61
  import { buildSpanMerkleTree, computeSpanHash } from "./merkle.js";
62
+ import {
63
+ computeModelManifestHash,
64
+ validateModelManifest,
65
+ freezeModelManifest,
66
+ } from "./model-manifest.js";
62
67
 
63
68
  // =============================================================================
64
69
  // TRACE CREATION
@@ -129,6 +134,28 @@ export async function createTrace(opts: CreateTraceOptions): Promise<TraceRun> {
129
134
  };
130
135
  }
131
136
 
137
+ // -------------------------------------------------------------------------
138
+ // Pre-execution model-manifest pinning (issue #59)
139
+ // -------------------------------------------------------------------------
140
+ const strict = opts.strict ?? false;
141
+ if (strict) {
142
+ run.strict = true;
143
+ }
144
+
145
+ if (opts.manifest !== undefined) {
146
+ const manifest = validateModelManifest(opts.manifest);
147
+ // Pin the hash now, BEFORE any event is recorded — this is the enforcement
148
+ // point for "the model was not altered over the run".
149
+ run.modelManifestHash = await computeModelManifestHash(manifest);
150
+ // Freeze the object so any later mutation throws (ESM strict mode). The pin
151
+ // is the hash computed above, not the live object.
152
+ run.modelManifest = freezeModelManifest(manifest);
153
+ } else if (strict) {
154
+ throw new Error(
155
+ "createTrace: strict mode requires a `manifest` to be pinned before execution"
156
+ );
157
+ }
158
+
132
159
  return run;
133
160
  }
134
161
 
@@ -531,6 +558,24 @@ export async function finalizeTrace(run: TraceRun): Promise<TraceBundle> {
531
558
  throw new Error("Trace run is already finalized");
532
559
  }
533
560
 
561
+ // -------------------------------------------------------------------------
562
+ // Pre-execution manifest enforcement (issue #59)
563
+ // -------------------------------------------------------------------------
564
+ if (run.modelManifest === undefined) {
565
+ if (run.strict) {
566
+ throw new Error(
567
+ "finalizeTrace: strict mode requires a model manifest pinned at createTrace() time"
568
+ );
569
+ }
570
+ // Warn-only path for v0.x — surfaces the missing model-immutability guarantee.
571
+ // Becomes a hard error under strict-by-default in v1.0.
572
+ console.warn(
573
+ "[orynq] finalizeTrace: no model manifest was pinned — model/data immutability " +
574
+ "is NOT proven for this trace. Pass `manifest` to createTrace() (and " +
575
+ "`strict: true` to enforce). This will become an error in v1.0."
576
+ );
577
+ }
578
+
534
579
  // Close any open spans
535
580
  for (const span of run.spans) {
536
581
  if (span.status === "running") {
@@ -551,8 +596,13 @@ export async function finalizeTrace(run: TraceRun): Promise<TraceBundle> {
551
596
  // Build Merkle tree from spans
552
597
  const merkleTree = await buildSpanMerkleTree(run.spans, run.events);
553
598
 
554
- // Compute root hash from rolling hash + span hashes
555
- const rootHash = await computeRootHash(run.rollingHash, run.spans);
599
+ // Compute root hash from rolling hash + span hashes, binding the pinned
600
+ // model-manifest commitment into the committed root (#59).
601
+ const rootHash = await computeRootHash(
602
+ run.rollingHash,
603
+ run.spans,
604
+ run.modelManifestHash
605
+ );
556
606
  run.rootHash = rootHash;
557
607
 
558
608
  // Create public view
@@ -567,6 +617,14 @@ export async function finalizeTrace(run: TraceRun): Promise<TraceBundle> {
567
617
  rootHash,
568
618
  };
569
619
 
620
+ // Surface the pinned model manifest on the bundle (public-safe: hashes only).
621
+ if (run.modelManifestHash !== undefined) {
622
+ bundle.modelManifestHash = run.modelManifestHash;
623
+ }
624
+ if (run.modelManifest !== undefined) {
625
+ bundle.modelManifest = run.modelManifest;
626
+ }
627
+
570
628
  return bundle;
571
629
  }
572
630
 
@@ -649,6 +707,14 @@ function createPublicView(
649
707
  redactedSpanHashes,
650
708
  };
651
709
 
710
+ // Model-state commitment is public-safe (it is only a hash).
711
+ if (run.modelManifestHash !== undefined) {
712
+ publicView.modelManifestHash = run.modelManifestHash;
713
+ }
714
+ if (run.modelManifest !== undefined) {
715
+ publicView.modelManifest = run.modelManifest;
716
+ }
717
+
652
718
  return publicView;
653
719
  }
654
720
 
package/src/types.ts CHANGED
@@ -124,6 +124,101 @@ export interface CustomEvent extends BaseTraceEvent {
124
124
  data: Record<string, unknown>;
125
125
  }
126
126
 
127
+ /**
128
+ * Signature scheme used by a governance attestor.
129
+ * - "sr25519" / "ed25519": Substrate/Materios wallets (verified in-package)
130
+ * - "eip712": EVM typed-data signatures (verified via a pluggable verifier)
131
+ */
132
+ export type GovernanceSignatureScheme = "sr25519" | "ed25519" | "eip712";
133
+
134
+ /**
135
+ * EIP-712 typed-data binding, required to verify an `eip712` governance
136
+ * signature. Mirrors the shape consumed by viem's `verifyTypedData`.
137
+ */
138
+ export interface GovernanceEip712Binding {
139
+ domain: Record<string, unknown>;
140
+ types: Record<string, Array<{ name: string; type: string }>>;
141
+ primaryType: string;
142
+ message?: Record<string, unknown>;
143
+ }
144
+
145
+ /**
146
+ * Governance attestation event — a verifiable, role-scoped sign-off recorded
147
+ * inside a trace (compliance review, release approval, data-steward sign-off).
148
+ *
149
+ * The signature is computed over a canonical, domain-separated preimage of
150
+ * `(runId || role || policyRef || decisionRef || signedAt)` (see
151
+ * `governanceAttestationPreimage`), so an auditor can verify *who* governed a
152
+ * decision without trusting the wrapper that recorded it, and a genuine
153
+ * attestation cannot be replayed into a different trace.
154
+ *
155
+ * Default visibility: "public" (governance provenance is meant to be auditable).
156
+ */
157
+ export interface GovernanceAttestationEvent extends BaseTraceEvent {
158
+ kind: "governance-attestation";
159
+ /** Governance role; common values plus free-form extension. */
160
+ role: "compliance" | "release-authority" | "data-steward" | (string & {});
161
+ /** Hash or URI of the policy being attested to. */
162
+ policyRef: string;
163
+ /** Hash or id of the decision/event being governed. */
164
+ decisionRef: string;
165
+ /** The signing identity and scheme. */
166
+ attestor: { address: string; signatureScheme: GovernanceSignatureScheme };
167
+ /** Signature over the canonical preimage (hex, optionally `0x`-prefixed). */
168
+ signature: string;
169
+ /** ISO 8601 timestamp; part of the signed preimage. */
170
+ signedAt: string;
171
+ /** EIP-712 binding — required only when `attestor.signatureScheme === "eip712"`. */
172
+ eip712?: GovernanceEip712Binding;
173
+ }
174
+
175
+ /**
176
+ * Signature scheme for a tool-call receipt.
177
+ * - "http-message-signatures": RFC 9421 signed HTTP responses
178
+ * - "stripe-webhook" / "github-webhook": SaaS webhook HMAC signatures
179
+ * - "jws": generic JWS/JWT-signed responses
180
+ * - (string): forward-compatible custom schemes
181
+ */
182
+ export type ToolReceiptScheme =
183
+ | "http-message-signatures"
184
+ | "stripe-webhook"
185
+ | "github-webhook"
186
+ | "jws"
187
+ | (string & {});
188
+
189
+ /**
190
+ * Verifiable tool-call receipt event — proves "the tool actually returned this
191
+ * response", not merely "the agent says the tool returned this response".
192
+ *
193
+ * The wrapper records a signed receipt produced by (or about) the external
194
+ * system; a verifier independently re-checks `receipt.signature` over
195
+ * `receipt.signedPayload` against `receipt.signer`.
196
+ *
197
+ * Default visibility: "private" (responses may contain PII / secrets — only the
198
+ * hashes and signature are needed for verification).
199
+ */
200
+ export interface ToolReceiptEvent extends BaseTraceEvent {
201
+ kind: "tool-receipt";
202
+ /** Identifier of the tool/endpoint that was called. */
203
+ toolId: string;
204
+ /** Commitment to the request (SHA-256 hex). */
205
+ request: { hash: string };
206
+ /** Commitment to the response, with an optional retained payload. */
207
+ response: { hash: string; payload?: unknown };
208
+ /** The independently-verifiable signed receipt. */
209
+ receipt: {
210
+ scheme: ToolReceiptScheme;
211
+ /** Verifier-resolvable identity: URL, DID, on-chain address, or keyId. */
212
+ signer: string;
213
+ /** Signature bytes (encoding depends on scheme: base64/hex/0x-hex). */
214
+ signature: string;
215
+ /** Canonicalized signed bytes the signature is computed over. */
216
+ signedPayload: string;
217
+ /** Scheme-specific verification material (headers, keyId, components, ...). */
218
+ params?: Record<string, unknown>;
219
+ };
220
+ }
221
+
127
222
  /**
128
223
  * Discriminated union of all trace event types.
129
224
  */
@@ -133,7 +228,9 @@ export type TraceEvent =
133
228
  | DecisionEvent
134
229
  | ObservationEvent
135
230
  | ErrorTraceEvent
136
- | CustomEvent;
231
+ | CustomEvent
232
+ | GovernanceAttestationEvent
233
+ | ToolReceiptEvent;
137
234
 
138
235
  /**
139
236
  * Event kind string literals for type guards.
@@ -150,6 +247,10 @@ export const DEFAULT_EVENT_VISIBILITY: Record<TraceEventKind, Visibility> = {
150
247
  observation: "public",
151
248
  error: "private",
152
249
  custom: "private",
250
+ // Governance provenance is meant to be auditable by third parties.
251
+ "governance-attestation": "public",
252
+ // Tool responses may carry PII/secrets; only hashes + signature are required.
253
+ "tool-receipt": "private",
153
254
  };
154
255
 
155
256
  // =============================================================================
@@ -186,6 +287,41 @@ export interface TraceSpan {
186
287
  hash?: string;
187
288
  }
188
289
 
290
+ // =============================================================================
291
+ // MODEL MANIFEST (PRE-EXECUTION PINNING)
292
+ // =============================================================================
293
+
294
+ /**
295
+ * Fingerprint of the model/data state used during a trace run.
296
+ *
297
+ * Distinct from {@link TraceManifest} (which describes off-chain *storage*
298
+ * chunks). A `ModelManifest` is pinned at {@link CreateTraceOptions} time —
299
+ * *before* execution — and frozen, so the resulting trace can prove that
300
+ * "neither the data nor the model was altered" for a given inference.
301
+ *
302
+ * Two traces of "the same model" should produce the same `modelManifestHash`,
303
+ * so the field values must be deterministic fingerprints (see the
304
+ * `manifestFrom*` builders).
305
+ */
306
+ export interface ModelManifest {
307
+ /** Model checkpoint fingerprint, e.g. "sha256:..." */
308
+ modelHash: string;
309
+ /** Tokenizer fingerprint. */
310
+ tokenizerHash?: string;
311
+ /** System-prompt fingerprint. */
312
+ systemPromptHash?: string;
313
+ /** Training-dataset manifest fingerprint. */
314
+ trainingDataManifest?: string;
315
+ /** Producing framework, e.g. "huggingface" | "openai" | "anthropic" | "checkpoint". */
316
+ framework?: string;
317
+ /** Model identifier (e.g. HF repo id, OpenAI/Anthropic model name). */
318
+ modelId?: string;
319
+ /** Revision / snapshot id, when applicable. */
320
+ revision?: string;
321
+ /** Free-form additional fingerprint inputs (hashed into manifestHash). */
322
+ metadata?: Record<string, unknown>;
323
+ }
324
+
189
325
  // =============================================================================
190
326
  // TRACE RUN
191
327
  // =============================================================================
@@ -218,6 +354,18 @@ export interface TraceRun {
218
354
  rootHash?: string;
219
355
  nextSeq: number;
220
356
  nextSpanSeq: number;
357
+ /**
358
+ * Model/data manifest pinned at createTrace() time (frozen). When present,
359
+ * `modelManifestHash` is the cryptographic commitment to it.
360
+ */
361
+ modelManifest?: ModelManifest;
362
+ /** H("poi-trace:model-manifest:v1|" + canonical(modelManifest)), pinned at creation. */
363
+ modelManifestHash?: string;
364
+ /**
365
+ * Strict-mode flag (pinned at creation). When true, finalizeTrace() throws
366
+ * if no manifest was pinned. Default false (warn-only) for v0.x.
367
+ */
368
+ strict?: boolean;
221
369
  }
222
370
 
223
371
  // =============================================================================
@@ -301,6 +449,10 @@ export interface TraceBundlePublicView {
301
449
  redactedSpanHashes: Array<{ spanId: string; hash: string }>;
302
450
  redactionPolicyId?: string;
303
451
  redactionRulesHash?: string;
452
+ /** Model-state commitment (public-safe: it is only a hash). */
453
+ modelManifestHash?: string;
454
+ /** Pinned model manifest (hashes only — public-safe). */
455
+ modelManifest?: ModelManifest;
304
456
  }
305
457
 
306
458
  /**
@@ -323,6 +475,14 @@ export interface TraceBundle {
323
475
  merkleRoot: string;
324
476
  rootHash: string;
325
477
  manifestHash?: string;
478
+ /**
479
+ * Model/data manifest commitment pinned at createTrace() time. Distinct from
480
+ * `manifestHash` (the off-chain storage-manifest hash). Place this in on-chain
481
+ * anchor metadata to make model drift cryptographically detectable.
482
+ */
483
+ modelManifestHash?: string;
484
+ /** The pinned model manifest (hashes only — public-safe). */
485
+ modelManifest?: ModelManifest;
326
486
  signerId?: string;
327
487
  signature?: string;
328
488
  }
@@ -449,6 +609,22 @@ export interface TraceVerificationResult {
449
609
  spanHashesValid: boolean;
450
610
  eventHashesValid: boolean;
451
611
  sequenceValid: boolean;
612
+ /**
613
+ * Model-manifest pin binding (#59): the pinned manifest hashes to its
614
+ * recorded commitment AND that commitment is folded into the committed
615
+ * root. True when no manifest is pinned (nothing to bind).
616
+ */
617
+ modelManifestValid?: boolean;
618
+ /**
619
+ * Set only when governance verification is requested via
620
+ * verifyBundle(bundle, { governance }). Undefined means "not checked".
621
+ */
622
+ governanceValid?: boolean;
623
+ /**
624
+ * Set only when tool-receipt verification is requested via
625
+ * verifyBundle(bundle, { toolReceipts }). Undefined means "not checked".
626
+ */
627
+ toolReceiptsValid?: boolean;
452
628
  };
453
629
  }
454
630
 
@@ -478,6 +654,17 @@ export interface CreateTraceOptions {
478
654
  agentId: string;
479
655
  description?: string;
480
656
  metadata?: Record<string, unknown>;
657
+ /**
658
+ * Model/data manifest to pin *before* execution. Its hash is computed and
659
+ * frozen at createTrace() time; mutating the manifest afterwards throws.
660
+ */
661
+ manifest?: ModelManifest;
662
+ /**
663
+ * Strict mode. When true, createTrace() requires a `manifest` and
664
+ * finalizeTrace() refuses to finalize an unpinned trace. Default false
665
+ * (warn-only) for v0.x; planned strict-by-default in v1.0.
666
+ */
667
+ strict?: boolean;
481
668
  }
482
669
 
483
670
  /**
@@ -514,6 +701,10 @@ export const HASH_DOMAIN_PREFIXES = {
514
701
  node: "poi-trace:node:v1|",
515
702
  manifest: "poi-trace:manifest:v1|",
516
703
  root: "poi-trace:root:v1|",
704
+ /** Model/data manifest commitment (pre-execution pinning). */
705
+ modelManifest: "poi-trace:model-manifest:v1|",
706
+ /** Governance-attestation signing preimage. */
707
+ governance: "poi-trace:governance:v1|",
517
708
  } as const;
518
709
 
519
710
  /**