@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/bundle.ts CHANGED
@@ -69,8 +69,48 @@ import {
69
69
  computeRootHash,
70
70
  } from "./rolling-hash.js";
71
71
 
72
+ import { computeModelManifestHash } from "./model-manifest.js";
73
+
72
74
  import { buildSpanMerkleTree, computeSpanHash } from "./merkle.js";
73
75
 
76
+ import {
77
+ verifyGovernanceAttestations,
78
+ type VerifyGovernanceOptions,
79
+ } from "./governance.js";
80
+
81
+ // =============================================================================
82
+ // VERIFY OPTIONS
83
+ // =============================================================================
84
+
85
+ /**
86
+ * Outcome shape returned by an injected tool-receipt verifier (provided by
87
+ * `@fluxpointstudios/orynq-sdk-tool-receipts` — passed in to avoid a circular
88
+ * dependency on that package from process-trace).
89
+ */
90
+ export interface ToolReceiptVerifyOutcome {
91
+ valid: boolean;
92
+ errors: string[];
93
+ }
94
+
95
+ /** Options for {@link verifyBundle}. */
96
+ export interface VerifyBundleOptions {
97
+ /**
98
+ * Verify `governance-attestation` events. `true` uses the built-in
99
+ * sr25519/ed25519 verifiers; pass {@link VerifyGovernanceOptions} to register
100
+ * a pluggable verifier (e.g. eip712). Any unverified attestation fails the
101
+ * bundle.
102
+ */
103
+ governance?: boolean | VerifyGovernanceOptions;
104
+ /**
105
+ * Verify `tool-receipt` events with an injected verifier from
106
+ * `@fluxpointstudios/orynq-sdk-tool-receipts`. Any failed receipt fails the
107
+ * bundle.
108
+ */
109
+ toolReceipts?: (
110
+ bundle: TraceBundle
111
+ ) => Promise<ToolReceiptVerifyOutcome> | ToolReceiptVerifyOutcome;
112
+ }
113
+
74
114
  // =============================================================================
75
115
  // VISIBILITY HELPERS
76
116
  // =============================================================================
@@ -350,11 +390,12 @@ export function extractPublicView(bundle: TraceBundle): TraceBundlePublicView {
350
390
  * ```
351
391
  */
352
392
  export async function verifyBundle(
353
- bundle: TraceBundle
393
+ bundle: TraceBundle,
394
+ options: VerifyBundleOptions = {}
354
395
  ): Promise<TraceVerificationResult> {
355
396
  const errors: string[] = [];
356
397
  const warnings: string[] = [];
357
- const checks = {
398
+ const checks: TraceVerificationResult["checks"] = {
358
399
  rollingHashValid: false,
359
400
  rootHashValid: false,
360
401
  merkleRootValid: false,
@@ -422,7 +463,13 @@ export async function verifyBundle(
422
463
  // ---------------------------------------------------------------------------
423
464
 
424
465
  try {
425
- const computedRootHash = await computeRootHash(run.rollingHash, run.spans);
466
+ // Recompute the root binding the recorded model-manifest commitment (#59),
467
+ // so a manifest swapped after commitment fails here.
468
+ const computedRootHash = await computeRootHash(
469
+ run.rollingHash,
470
+ run.spans,
471
+ run.modelManifestHash
472
+ );
426
473
  if (computedRootHash === bundle.rootHash) {
427
474
  checks.rootHashValid = true;
428
475
  } else {
@@ -455,6 +502,105 @@ export async function verifyBundle(
455
502
  );
456
503
  }
457
504
 
505
+ // ---------------------------------------------------------------------------
506
+ // Verify Model-Manifest Pin Binding (issue #59)
507
+ // ---------------------------------------------------------------------------
508
+ // When a manifest is pinned it MUST (a) hash to the recorded commitment and
509
+ // (b) be bound into the committed root. (b) is enforced by folding
510
+ // modelManifestHash into computeRootHash above — a swapped hash breaks
511
+ // rootHashValid. Here we additionally recompute the commitment from the
512
+ // manifest itself so that swapping the manifest (while leaving the recorded
513
+ // hash) is caught, and reject a manifest present but not bound into the root.
514
+
515
+ const hasManifest = run.modelManifest !== undefined;
516
+ const hasManifestHash =
517
+ run.modelManifestHash !== undefined && run.modelManifestHash.length > 0;
518
+
519
+ if (!hasManifest && !hasManifestHash) {
520
+ // No manifest pinned — nothing to bind (warn-only path lives in finalizeTrace).
521
+ checks.modelManifestValid = true;
522
+ } else {
523
+ let manifestBindingValid = true;
524
+
525
+ if (hasManifest && !hasManifestHash) {
526
+ manifestBindingValid = false;
527
+ errors.push(
528
+ "Model manifest present but modelManifestHash (its commitment) is missing"
529
+ );
530
+ } else if (!hasManifest && hasManifestHash) {
531
+ manifestBindingValid = false;
532
+ errors.push(
533
+ "modelManifestHash present but the model manifest itself is missing"
534
+ );
535
+ } else if (run.modelManifest !== undefined) {
536
+ try {
537
+ const recomputed = await computeModelManifestHash(run.modelManifest);
538
+ if (recomputed !== run.modelManifestHash) {
539
+ manifestBindingValid = false;
540
+ errors.push(
541
+ `Model manifest hash mismatch: recorded ${run.modelManifestHash}, computed ${recomputed}`
542
+ );
543
+ }
544
+ } catch (error) {
545
+ manifestBindingValid = false;
546
+ errors.push(
547
+ `Failed to recompute model manifest hash: ${error instanceof Error ? error.message : String(error)}`
548
+ );
549
+ }
550
+ }
551
+
552
+ // The manifest must actually be bound into the committed root. If the root
553
+ // recompute (with the manifest folded in) matched, rootHashValid is true;
554
+ // a manifest that is NOT bound produces a root mismatch above.
555
+ if (manifestBindingValid && !checks.rootHashValid) {
556
+ manifestBindingValid = false;
557
+ errors.push(
558
+ "Model manifest is not bound into the committed root hash"
559
+ );
560
+ }
561
+
562
+ checks.modelManifestValid = manifestBindingValid;
563
+ }
564
+
565
+ // ---------------------------------------------------------------------------
566
+ // Verify PublicView Model-Manifest (issue #59)
567
+ // ---------------------------------------------------------------------------
568
+ // publicView.{modelManifest,modelManifestHash} is the shared artifact an
569
+ // EXTERNAL verifier reads. It must (a) equal the bound privateRun commitment
570
+ // and (b) recompute consistently from the publicView manifest itself — else an
571
+ // attacker can leave privateRun honest but fabricate the public fields.
572
+
573
+ const pvManifest = bundle.publicView.modelManifest;
574
+ const pvManifestHash = bundle.publicView.modelManifestHash;
575
+
576
+ if (pvManifestHash !== undefined && pvManifestHash !== run.modelManifestHash) {
577
+ checks.modelManifestValid = false;
578
+ errors.push(
579
+ `PublicView modelManifestHash (${pvManifestHash}) does not match the bound commitment (${run.modelManifestHash})`
580
+ );
581
+ }
582
+ if (pvManifest !== undefined && run.modelManifest === undefined) {
583
+ checks.modelManifestValid = false;
584
+ errors.push("PublicView carries a model manifest but the bound run has none");
585
+ }
586
+ if (pvManifest !== undefined) {
587
+ try {
588
+ const recomputed = await computeModelManifestHash(pvManifest);
589
+ const expected = pvManifestHash ?? run.modelManifestHash;
590
+ if (expected !== undefined && recomputed !== expected) {
591
+ checks.modelManifestValid = false;
592
+ errors.push(
593
+ `PublicView model manifest hash mismatch: recorded ${expected}, computed ${recomputed}`
594
+ );
595
+ }
596
+ } catch (error) {
597
+ checks.modelManifestValid = false;
598
+ errors.push(
599
+ `Failed to recompute publicView model manifest hash: ${error instanceof Error ? error.message : String(error)}`
600
+ );
601
+ }
602
+ }
603
+
458
604
  // ---------------------------------------------------------------------------
459
605
  // Additional Warnings
460
606
  // ---------------------------------------------------------------------------
@@ -474,14 +620,62 @@ export async function verifyBundle(
474
620
  );
475
621
  }
476
622
 
477
- // Determine overall validity
623
+ // ---------------------------------------------------------------------------
624
+ // Verify Governance Attestations (issue #58, opt-in)
625
+ // ---------------------------------------------------------------------------
626
+
627
+ if (options.governance) {
628
+ try {
629
+ const govOpts: VerifyGovernanceOptions =
630
+ options.governance === true ? {} : options.governance;
631
+ const summaries = await verifyGovernanceAttestations(bundle, govOpts);
632
+ const failed = summaries.filter((s) => !s.verified);
633
+ checks.governanceValid = failed.length === 0;
634
+ for (const f of failed) {
635
+ errors.push(
636
+ `Governance attestation failed (${f.scheme}, role ${f.role}, attestor ${f.attestor})` +
637
+ (f.error ? `: ${f.error}` : "")
638
+ );
639
+ }
640
+ } catch (error) {
641
+ checks.governanceValid = false;
642
+ errors.push(
643
+ `Failed to verify governance attestations: ${error instanceof Error ? error.message : String(error)}`
644
+ );
645
+ }
646
+ }
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // Verify Tool-Call Receipts (issue #60, opt-in, injected verifier)
650
+ // ---------------------------------------------------------------------------
651
+
652
+ if (options.toolReceipts) {
653
+ try {
654
+ const outcome = await options.toolReceipts(bundle);
655
+ checks.toolReceiptsValid = outcome.valid;
656
+ if (!outcome.valid) {
657
+ errors.push(...outcome.errors);
658
+ }
659
+ } catch (error) {
660
+ checks.toolReceiptsValid = false;
661
+ errors.push(
662
+ `Failed to verify tool receipts: ${error instanceof Error ? error.message : String(error)}`
663
+ );
664
+ }
665
+ }
666
+
667
+ // Determine overall validity. Optional checks only fail the bundle when
668
+ // explicitly run and false (undefined === "not checked").
478
669
  const valid =
479
670
  checks.rollingHashValid &&
480
671
  checks.rootHashValid &&
481
672
  checks.merkleRootValid &&
482
673
  checks.spanHashesValid &&
483
674
  checks.eventHashesValid &&
484
- checks.sequenceValid;
675
+ checks.sequenceValid &&
676
+ checks.modelManifestValid !== false &&
677
+ checks.governanceValid !== false &&
678
+ checks.toolReceiptsValid !== false;
485
679
 
486
680
  return {
487
681
  valid,