usertrust 1.5.0 → 2.0.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.
Files changed (115) hide show
  1. package/README.md +17 -1
  2. package/dist/anomaly/detector.js.map +1 -1
  3. package/dist/anomaly/index.d.ts +2 -2
  4. package/dist/anomaly/index.d.ts.map +1 -1
  5. package/dist/anomaly/index.js +1 -1
  6. package/dist/anomaly/index.js.map +1 -1
  7. package/dist/audit/anchor-doctor.d.ts +42 -0
  8. package/dist/audit/anchor-doctor.d.ts.map +1 -0
  9. package/dist/audit/anchor-doctor.js +285 -0
  10. package/dist/audit/anchor-doctor.js.map +1 -0
  11. package/dist/audit/anchor-verify.d.ts +229 -0
  12. package/dist/audit/anchor-verify.d.ts.map +1 -0
  13. package/dist/audit/anchor-verify.js +809 -0
  14. package/dist/audit/anchor-verify.js.map +1 -0
  15. package/dist/audit/anchor.d.ts +177 -0
  16. package/dist/audit/anchor.d.ts.map +1 -0
  17. package/dist/audit/anchor.js +922 -0
  18. package/dist/audit/anchor.js.map +1 -0
  19. package/dist/audit/canonical.js +1 -1
  20. package/dist/audit/canonical.js.map +1 -1
  21. package/dist/audit/chain.d.ts.map +1 -1
  22. package/dist/audit/chain.js +21 -7
  23. package/dist/audit/chain.js.map +1 -1
  24. package/dist/audit/read.d.ts +19 -0
  25. package/dist/audit/read.d.ts.map +1 -0
  26. package/dist/audit/read.js +94 -0
  27. package/dist/audit/read.js.map +1 -0
  28. package/dist/audit/rekor-verify.d.ts +116 -0
  29. package/dist/audit/rekor-verify.d.ts.map +1 -0
  30. package/dist/audit/rekor-verify.js +533 -0
  31. package/dist/audit/rekor-verify.js.map +1 -0
  32. package/dist/audit/rekor.d.ts +71 -0
  33. package/dist/audit/rekor.d.ts.map +1 -0
  34. package/dist/audit/rekor.js +556 -0
  35. package/dist/audit/rekor.js.map +1 -0
  36. package/dist/audit/rotation.js +1 -1
  37. package/dist/audit/rotation.js.map +1 -1
  38. package/dist/audit/sigv4.d.ts +44 -0
  39. package/dist/audit/sigv4.d.ts.map +1 -0
  40. package/dist/audit/sigv4.js +134 -0
  41. package/dist/audit/sigv4.js.map +1 -0
  42. package/dist/audit/verify.d.ts +78 -0
  43. package/dist/audit/verify.d.ts.map +1 -1
  44. package/dist/audit/verify.js +195 -1
  45. package/dist/audit/verify.js.map +1 -1
  46. package/dist/cli/anchor.d.ts +3 -0
  47. package/dist/cli/anchor.d.ts.map +1 -0
  48. package/dist/cli/anchor.js +626 -0
  49. package/dist/cli/anchor.js.map +1 -0
  50. package/dist/cli/completions.js +1 -1
  51. package/dist/cli/export.d.ts +3 -0
  52. package/dist/cli/export.d.ts.map +1 -0
  53. package/dist/cli/export.js +58 -0
  54. package/dist/cli/export.js.map +1 -0
  55. package/dist/cli/health.js.map +1 -1
  56. package/dist/cli/init.js +1 -1
  57. package/dist/cli/init.js.map +1 -1
  58. package/dist/cli/inspect.d.ts.map +1 -1
  59. package/dist/cli/inspect.js +4 -34
  60. package/dist/cli/inspect.js.map +1 -1
  61. package/dist/cli/main.d.ts +2 -2
  62. package/dist/cli/main.d.ts.map +1 -1
  63. package/dist/cli/main.js +20 -1
  64. package/dist/cli/main.js.map +1 -1
  65. package/dist/cli/pricing.js +1 -1
  66. package/dist/cli/pricing.js.map +1 -1
  67. package/dist/cli/ui.d.ts +3 -0
  68. package/dist/cli/ui.d.ts.map +1 -0
  69. package/dist/cli/ui.js +35 -0
  70. package/dist/cli/ui.js.map +1 -0
  71. package/dist/cli/verify.d.ts.map +1 -1
  72. package/dist/cli/verify.js +342 -3
  73. package/dist/cli/verify.js.map +1 -1
  74. package/dist/config.d.ts.map +1 -1
  75. package/dist/config.js.map +1 -1
  76. package/dist/detect.d.ts +37 -15
  77. package/dist/detect.d.ts.map +1 -1
  78. package/dist/detect.js +17 -6
  79. package/dist/detect.js.map +1 -1
  80. package/dist/export/markdown.d.ts +12 -0
  81. package/dist/export/markdown.d.ts.map +1 -0
  82. package/dist/export/markdown.js +142 -0
  83. package/dist/export/markdown.js.map +1 -0
  84. package/dist/govern.d.ts +60 -3
  85. package/dist/govern.d.ts.map +1 -1
  86. package/dist/govern.js +842 -229
  87. package/dist/govern.js.map +1 -1
  88. package/dist/headless.d.ts.map +1 -1
  89. package/dist/headless.js +4 -4
  90. package/dist/headless.js.map +1 -1
  91. package/dist/index.d.ts +33 -26
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +30 -25
  94. package/dist/index.js.map +1 -1
  95. package/dist/ledger/client.d.ts.map +1 -1
  96. package/dist/ledger/client.js +57 -46
  97. package/dist/ledger/client.js.map +1 -1
  98. package/dist/ledger/engine.d.ts.map +1 -1
  99. package/dist/ledger/engine.js +22 -13
  100. package/dist/ledger/engine.js.map +1 -1
  101. package/dist/shared/types.d.ts +114 -475
  102. package/dist/shared/types.d.ts.map +1 -1
  103. package/dist/shared/types.js +13 -13
  104. package/dist/shared/types.js.map +1 -1
  105. package/dist/snapshot/checkpoint.d.ts.map +1 -1
  106. package/dist/snapshot/checkpoint.js +20 -2
  107. package/dist/snapshot/checkpoint.js.map +1 -1
  108. package/dist/streaming.d.ts +9 -4
  109. package/dist/streaming.d.ts.map +1 -1
  110. package/dist/streaming.js +27 -0
  111. package/dist/streaming.js.map +1 -1
  112. package/dist/supply-chain/sign.d.ts.map +1 -1
  113. package/dist/supply-chain/sign.js +3 -0
  114. package/dist/supply-chain/sign.js.map +1 -1
  115. package/package.json +6 -4
package/dist/govern.js CHANGED
@@ -30,7 +30,7 @@ import { createHash, randomUUID } from "node:crypto";
30
30
  import { existsSync, mkdirSync } from "node:fs";
31
31
  import { readFile, rename, unlink, writeFile } from "node:fs/promises";
32
32
  import { join } from "node:path";
33
- import { CreateTransferError } from "tigerbeetle-node";
33
+ import { CreateTransferStatus } from "tigerbeetle-node";
34
34
  import { createAuditWriter } from "./audit/chain.js";
35
35
  import { writeReceipt } from "./audit/rotation.js";
36
36
  import { classifyEndpoint, detectClientKind } from "./detect.js";
@@ -306,7 +306,19 @@ export async function trust(client, opts) {
306
306
  },
307
307
  });
308
308
  // 7. Two-phase intercept
309
- async function interceptCall(originalFn, thisArg, args) {
309
+ async function interceptCall(originalFn, thisArg, args,
310
+ // A1: which provider surface this call arrived through. "create" is the
311
+ // default request/response (and create-stream) path. "stream-helper" is the
312
+ // Anthropic messages.stream / beta.messages.stream convenience helper, whose
313
+ // forwarded call returns a self-driving MessageStream we settle via
314
+ // NON-CONSUMING emitter events instead of routing through
315
+ // createGovernedStream (A1) — consuming its single-owner async iterator would
316
+ // steal it from the caller. "openai-responses" (Task 3, A6) is the OpenAI
317
+ // Responses API (create, stream and non-stream): it shares the create request/
318
+ // response lifecycle and the generic createGovernedStream streaming path (A7),
319
+ // but MUST NOT receive the chat.completions-only stream_options.include_usage
320
+ // injection (A6) — the surface flag suppresses it below.
321
+ surfaceKind = "create") {
310
322
  if (destroyed) {
311
323
  throw new Error("TrustedClient has been destroyed");
312
324
  }
@@ -336,7 +348,12 @@ export async function trust(client, opts) {
336
348
  // authorize time, before any PENDING hold (A5).
337
349
  const transferId = trustId("tx");
338
350
  const estimatedInputTokens = estimateInputTokens(promptParts);
339
- const maxOutputTokens = params.max_tokens ?? 4096;
351
+ // F1: size the output estimate to the caller's ACTUAL cap. Responses callers
352
+ // set `max_output_tokens`; chat.completions set `max_tokens`. Read the
353
+ // Responses field first, then chat, then the 4096 default — each only when it
354
+ // is a finite positive number (a 0/negative/NaN cap never shrinks the hold).
355
+ const finitePositiveCap = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
356
+ const maxOutputTokens = finitePositiveCap(params.max_output_tokens) ?? finitePositiveCap(params.max_tokens) ?? 4096;
340
357
  const rateResolution = resolveRates(model, endpoint.class, config);
341
358
  enforceUnknownModelPolicy(model, rateResolution, config);
342
359
  const estimatedCost = costFromRates(rateResolution.rates, estimatedInputTokens, maxOutputTokens);
@@ -468,9 +485,15 @@ export async function trust(client, opts) {
468
485
  // stream_options.include_usage is set). The merge preserves the caller's
469
486
  // other stream_options fields and respects an explicit include_usage.
470
487
  // The resulting usage chunk is FORWARDED to the consumer unmodified
471
- // (transparent middleware).
488
+ // (transparent middleware). Task 3/A6: the OpenAI Responses API is EXCLUDED
489
+ // — stream_options.include_usage is chat.completions-only; Responses carries
490
+ // usage automatically on the terminal `response.completed` event, and
491
+ // injecting the param would be an unknown field on the Responses request.
472
492
  let preInjectionArgs = null;
473
- if (kind === "openai" && endpoint.class === "local" && config.local.injectUsageOptions) {
493
+ if (kind === "openai" &&
494
+ surfaceKind !== "openai-responses" &&
495
+ endpoint.class === "local" &&
496
+ config.local.injectUsageOptions) {
474
497
  const injected = withInjectedUsageOptions(forwardArgs);
475
498
  if (injected != null) {
476
499
  preInjectionArgs = forwardArgs;
@@ -480,6 +503,476 @@ export async function trust(client, opts) {
480
503
  // e. Forward to original SDK. P3-PII-REDACT-EGRESS: forwardArgs is the
481
504
  // redacted clone in redact mode, or the original args otherwise.
482
505
  let settled = true;
506
+ // A2: one idempotent finalize gate per hold. The FIRST terminal signal for
507
+ // this authorization wins and claims the single ledger outcome (settle XOR
508
+ // void); every later signal returns false and MUST NOT mutate the ledger.
509
+ // Every terminal path routes through it: the non-stream settle + its
510
+ // outer-catch void, the stream onComplete/onError closures below, and
511
+ // (Tasks 2/3) any new surface's emitter/iterator/error/abort listeners. A
512
+ // duplicate terminal signal can neither double-settle nor double-void.
513
+ let finalizeState = "pending";
514
+ function finalizeOnce(outcome) {
515
+ if (finalizeState !== "pending")
516
+ return false;
517
+ finalizeState = outcome === "settle" ? "settled" : "voided";
518
+ return true;
519
+ }
520
+ // Estimate-priced stream receipt (settled:false). Serves as the initial
521
+ // handle returned to the caller AND the idempotent fallback a duplicate
522
+ // stream terminal returns after the hold was already resolved.
523
+ const buildEstimatedStreamReceipt = () => {
524
+ const streamEstimateHash = createHash("sha256").update(transferId).digest("hex");
525
+ return {
526
+ transferId,
527
+ cost: estimatedCost,
528
+ budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
529
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : streamEstimateHash,
530
+ chainPath: join(VAULT_DIR, "audit"),
531
+ receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
532
+ settled: false, // AUD-454: not settled yet — stream hasn't been consumed
533
+ model,
534
+ provider: kind,
535
+ timestamp: new Date().toISOString(),
536
+ // M2: authorize-time scope, already fixed for the eventual settle (A3).
537
+ endpoint: { class: endpoint.class, runtime: endpoint.runtime },
538
+ meter: {
539
+ costBasis: rateResolution.costBasis,
540
+ rateSource: rateResolution.rateSource,
541
+ },
542
+ ...(callAuditDegraded ? { auditDegraded: true } : {}),
543
+ // AUD-456: Flag proxy stub receipts
544
+ ...(proxyConn != null ? { proxyStub: true } : {}),
545
+ };
546
+ };
547
+ // Terminal stream settlement (A2 settle branch). Idempotent via
548
+ // finalizeOnce: budget commit + ledger POST + llm_call audit happen AT MOST
549
+ // ONCE per hold. Tasks 2/3 reuse this verbatim — the
550
+ // Anthropic MessageStream 'finalMessage' listener and any new stream surface
551
+ // call it with a StreamCompletion built from final usage. Does NOT touch
552
+ // inFlightStreamCount — the caller (createGovernedStream wiring, or a new
553
+ // surface's listener) owns that counter.
554
+ let streamSettledReceipt;
555
+ const finalizeStreamSettle = async (completion) => {
556
+ if (!finalizeOnce("settle")) {
557
+ // A duplicate terminal (e.g. 'finalMessage' after 'end', or a settle
558
+ // racing an abort-void): never mutate the ledger again.
559
+ return streamSettledReceipt ?? buildEstimatedStreamReceipt();
560
+ }
561
+ // Determine cost: use provider usage if reported, else fall back to
562
+ // estimate. A3: priced with the rate resolution captured at authorize;
563
+ // A11: costFromRates floors at 1 even for 0/0 usage.
564
+ let streamCost;
565
+ let usageSource;
566
+ if (completion.usageReported) {
567
+ streamCost = costFromRates(rateResolution.rates, completion.usage.inputTokens, completion.usage.outputTokens);
568
+ usageSource = "provider";
569
+ }
570
+ else {
571
+ streamCost = estimatedCost;
572
+ usageSource = "estimated";
573
+ }
574
+ // Release in-flight hold and commit budget under mutex.
575
+ // AUD-468: holdActive guard prevents double-release.
576
+ if (holdActive) {
577
+ holdActive = false;
578
+ const releaseLock = await budgetMutex.acquire();
579
+ inFlightHoldTotal -= estimatedCost;
580
+ budgetSpent += streamCost;
581
+ releaseLock();
582
+ }
583
+ else {
584
+ // Hold already released — still need to record the spend.
585
+ const releaseLock = await budgetMutex.acquire();
586
+ budgetSpent += streamCost;
587
+ releaseLock();
588
+ }
589
+ // AUD-457: Persist cumulative spend to disk
590
+ await persistSpendLedger(vaultBase, budgetSpent);
591
+ cb.recordSuccess();
592
+ if (proxyConn != null && !isDryRun) {
593
+ try {
594
+ // AUD-460: Use the proxy's transferId for settlement
595
+ await proxyConn.settle(proxyTransferId ?? transferId, streamCost);
596
+ }
597
+ catch (postErr) {
598
+ settled = false;
599
+ await audit
600
+ .appendEvent({
601
+ kind: "settlement_ambiguous",
602
+ actor: "local",
603
+ data: {
604
+ model,
605
+ cost: streamCost,
606
+ transferId,
607
+ error: postErr instanceof Error
608
+ ? postErr.message.slice(0, 200)
609
+ : String(postErr).slice(0, 200),
610
+ },
611
+ })
612
+ .catch(() => {
613
+ callAuditDegraded = true;
614
+ });
615
+ }
616
+ }
617
+ else if (engine != null && !isDryRun) {
618
+ try {
619
+ // Post the ACTUAL consumed cost (RECON #3).
620
+ await engine.postPendingSpend(transferId, streamCost);
621
+ }
622
+ catch (postErr) {
623
+ settled = false;
624
+ await audit
625
+ .appendEvent({
626
+ kind: "settlement_ambiguous",
627
+ actor: "local",
628
+ data: {
629
+ model,
630
+ cost: streamCost,
631
+ transferId,
632
+ error: postErr instanceof Error
633
+ ? postErr.message.slice(0, 200)
634
+ : String(postErr).slice(0, 200),
635
+ },
636
+ })
637
+ .catch(() => {
638
+ callAuditDegraded = true;
639
+ });
640
+ }
641
+ }
642
+ const syntheticHash = createHash("sha256").update(transferId).digest("hex");
643
+ let auditHash = syntheticHash;
644
+ try {
645
+ const auditEventData = {
646
+ model,
647
+ cost: streamCost,
648
+ settled,
649
+ transferId,
650
+ usageSource,
651
+ chunksDelivered: completion.chunksDelivered,
652
+ // M2: metering provenance mirrors the receipt (A3 authorize-time scope).
653
+ endpointClass: endpoint.class,
654
+ costBasis: rateResolution.costBasis,
655
+ rateSource: rateResolution.rateSource,
656
+ };
657
+ if (config.pii === "warn" || config.pii === "redact") {
658
+ const piiResult = redactPII(promptParts);
659
+ if (piiResult.detection.found) {
660
+ auditEventData.piiDetected = piiResult.detection.types;
661
+ auditEventData.piiPaths = piiResult.detection.paths;
662
+ }
663
+ }
664
+ const auditEvent = await audit.appendEvent({
665
+ kind: "llm_call",
666
+ actor: "local",
667
+ data: auditEventData,
668
+ });
669
+ auditHash = auditEvent.hash;
670
+ }
671
+ catch {
672
+ callAuditDegraded = true;
673
+ }
674
+ // P3-AUDIT-FAILCLOSED (streaming): chunks were already delivered, so the
675
+ // strongest post-delivery signal is to REJECT `.receipt`. The caller's
676
+ // finally decrements inFlightStreamCount so destroy() never blocks.
677
+ if (config.audit.failClosed && (callAuditDegraded || audit.isDegraded())) {
678
+ throw new AuditDegradedError(`audit unavailable (writeFailures=${audit.getWriteFailures()}) for ${transferId}`);
679
+ }
680
+ const streamReceipt = {
681
+ transferId,
682
+ cost: streamCost,
683
+ budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
684
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : auditHash,
685
+ chainPath: join(VAULT_DIR, "audit"),
686
+ receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
687
+ settled,
688
+ model,
689
+ provider: kind,
690
+ timestamp: new Date().toISOString(),
691
+ usageSource,
692
+ chunksDelivered: completion.chunksDelivered,
693
+ // M2 provenance (A6: computeMs omitted — no compute-time source here).
694
+ endpoint: { class: endpoint.class, runtime: endpoint.runtime },
695
+ meter: {
696
+ costBasis: rateResolution.costBasis,
697
+ rateSource: rateResolution.rateSource,
698
+ },
699
+ ...(callAuditDegraded ? { auditDegraded: true } : {}),
700
+ // AUD-456: Flag proxy stub receipts
701
+ ...(proxyConn != null ? { proxyStub: true } : {}),
702
+ };
703
+ streamSettledReceipt = streamReceipt;
704
+ return streamReceipt;
705
+ };
706
+ // Terminal stream void (A2 void branch). Idempotent via finalizeOnce:
707
+ // releases the hold and voids the ledger AT MOST ONCE. Tasks 2/3 reuse this
708
+ // for the Anthropic MessageStream 'error'/'abort' listeners and any new
709
+ // stream surface's error path. Does NOT touch inFlightStreamCount.
710
+ const finalizeStreamVoid = async (error, partial) => {
711
+ if (!finalizeOnce("void"))
712
+ return;
713
+ // Release in-flight hold under mutex (AUD-468: guarded).
714
+ await releaseInFlightHold();
715
+ cb.recordFailure();
716
+ // Best-effort audit of partial delivery
717
+ audit
718
+ .appendEvent({
719
+ kind: "stream_partial_delivery",
720
+ actor: "local",
721
+ data: {
722
+ transferId,
723
+ model,
724
+ chunksDelivered: partial.chunksDelivered,
725
+ partialInputTokens: partial.usage.inputTokens,
726
+ partialOutputTokens: partial.usage.outputTokens,
727
+ usageReported: partial.usageReported,
728
+ error: (() => {
729
+ const raw = error instanceof Error ? error.message : String(error);
730
+ return config.pii === "warn" || config.pii === "redact"
731
+ ? redactPII(raw).data.slice(0, 200)
732
+ : raw.slice(0, 200);
733
+ })(),
734
+ },
735
+ })
736
+ .catch(() => { });
737
+ if (proxyConn != null && !isDryRun) {
738
+ // AUD-460: Use the proxy's transferId for void
739
+ proxyConn.void(proxyTransferId ?? transferId).catch(() => { });
740
+ }
741
+ else if (engine != null && !isDryRun) {
742
+ engine.voidPendingSpend(transferId).catch(() => { });
743
+ }
744
+ };
745
+ // A1: settle an Anthropic MessageStream (messages.stream /
746
+ // beta.messages.stream) via its MULTICAST event emitter, never by consuming
747
+ // its single-owner async iterator (that belongs to the caller). The stream
748
+ // self-drives, so finalMessage/error/abort fire even if the caller never
749
+ // consumes (A4); the TigerBeetle PENDING 300s timeout (see destroy() and
750
+ // createPendingTransfer) is the last-resort backstop for a hold whose
751
+ // terminal event never arrives. finalizeOnce makes the six-mode consumption
752
+ // matrix safe: settle XOR void, exactly one ledger mutation.
753
+ const settleViaMessageStream = (streamObj) => {
754
+ // The initial handle: estimate-priced, settled:false — identical to the
755
+ // generic stream path. The caller reads the real receipt off `.receipt`.
756
+ const estimatedReceipt = buildEstimatedStreamReceipt();
757
+ const emitter = streamObj != null && typeof streamObj === "object"
758
+ ? streamObj
759
+ : null;
760
+ // This closure OWNS inFlightStreamCount for the stream's lifetime; the
761
+ // finalize* gates never touch it. Release it EXACTLY ONCE no matter how
762
+ // many terminal events fire (finalMessage-then-abort race, error-after-
763
+ // end, …) so destroy() never blocks and the counter never drifts.
764
+ inFlightStreamCount++;
765
+ let streamCountReleased = false;
766
+ const releaseStreamCount = () => {
767
+ if (streamCountReleased)
768
+ return;
769
+ streamCountReleased = true;
770
+ inFlightStreamCount--;
771
+ };
772
+ // `.receipt`: resolves with the settled receipt, rejects on a genuine
773
+ // stream error / failClosed. Built BEFORE the feature-detect branch so BOTH
774
+ // the emitter path and the non-emitter fallback expose a live `.receipt` the
775
+ // caller can await for the settled outcome. F8: governance never SILENTLY
776
+ // swallows a stream failure — a consumer who awaits `.receipt` always sees
777
+ // the error (the pre-attached catch below is a SEPARATE branch: promises are
778
+ // multicast, so it silences the unhandled-rejection ONLY for a fire-and-forget
779
+ // consumer that never awaits `.receipt`, never registers an error handler, and
780
+ // never calls done()/finalMessage() — that narrow gap is documented).
781
+ let receiptResolve;
782
+ let receiptReject;
783
+ const receiptPromise = new Promise((res, rej) => {
784
+ receiptResolve = res;
785
+ receiptReject = rej;
786
+ });
787
+ receiptPromise.catch(() => { });
788
+ // Expose `.receipt` on the returned stream object (best-effort — a frozen or
789
+ // non-object stream simply omits it; the terminal events still settle
790
+ // governance).
791
+ const attachReceipt = () => {
792
+ if (streamObj == null || typeof streamObj !== "object")
793
+ return;
794
+ try {
795
+ Object.assign(streamObj, { receipt: receiptPromise });
796
+ }
797
+ catch {
798
+ // non-extensible target — skip
799
+ }
800
+ };
801
+ // A3: a helper that connected is a billable SUCCESS. If the forwarded
802
+ // object is not an event-emitter MessageStream (feature-detect miss / an
803
+ // SDK too old to return one), we cannot tap emitter events — settle at
804
+ // ESTIMATE now so the hold never dangles, resolve `.receipt` from that
805
+ // background settle (mirroring the finalMessage path), and hand the object
806
+ // back raw.
807
+ if (emitter == null || typeof emitter.on !== "function") {
808
+ finalizeStreamSettle({
809
+ usage: { inputTokens: 0, outputTokens: 0 },
810
+ chunksDelivered: 0,
811
+ usageReported: false,
812
+ })
813
+ .then(receiptResolve, receiptReject)
814
+ .finally(releaseStreamCount);
815
+ attachReceipt();
816
+ return { response: streamObj, receipt: estimatedReceipt };
817
+ }
818
+ // Non-consuming chunk accounting: streamEvent is multicast, so tapping it
819
+ // does not steal the caller's iterator. Mirrors the wrapStream tap so
820
+ // partial-void audits and a usage-less finalMessage both carry real
821
+ // numbers, and (best-effort, A1) trips the shared anomaly detector.
822
+ let chunksDelivered = 0;
823
+ let accInput = 0;
824
+ let accOutput = 0;
825
+ let accUsageReported = false;
826
+ // R1: set TRUE before governance calls emitter.abort() on an anomaly trip,
827
+ // so the 'abort' handler can distinguish a governance cutoff (void + breaker
828
+ // failure) from a genuine consumer abort (F9 settle-partial).
829
+ let anomalyAbort = false;
830
+ const partial = () => ({
831
+ usage: { inputTokens: accInput, outputTokens: accOutput },
832
+ chunksDelivered,
833
+ usageReported: accUsageReported,
834
+ });
835
+ emitter.on("streamEvent", (event) => {
836
+ chunksDelivered++;
837
+ const tokens = extractAnthropicStreamUsage(event);
838
+ let deltaTokens = 0;
839
+ if (tokens.inputTokens > accInput) {
840
+ deltaTokens += tokens.inputTokens - accInput;
841
+ accInput = tokens.inputTokens;
842
+ accUsageReported = true;
843
+ }
844
+ if (tokens.outputTokens > accOutput) {
845
+ deltaTokens += tokens.outputTokens - accOutput;
846
+ accOutput = tokens.outputTokens;
847
+ accUsageReported = true;
848
+ }
849
+ if (!config.anomaly.enabled)
850
+ return;
851
+ anomalyDetector.observe({
852
+ kind: "chunk",
853
+ deltaTokens,
854
+ cumulativeInputTokens: accInput,
855
+ cumulativeOutputTokens: accOutput,
856
+ // M2: stamp the per-call scope so the SHARED detector prices this
857
+ // event with the same scoped rates as settlement.
858
+ model,
859
+ endpointClass: endpoint.class,
860
+ });
861
+ const verdict = anomalyDetector.check();
862
+ if (verdict.tripped) {
863
+ audit
864
+ .appendEvent({
865
+ kind: "anomaly_detected",
866
+ actor: "local",
867
+ data: {
868
+ anomalyKind: verdict.kind,
869
+ message: verdict.message,
870
+ metric: verdict.metric,
871
+ threshold: verdict.threshold,
872
+ model,
873
+ transferId,
874
+ provider: kind,
875
+ },
876
+ })
877
+ .catch(() => { });
878
+ // A1 best-effort mid-stream cutoff (full parity deferred). R1: flag
879
+ // this as a GOVERNANCE abort BEFORE aborting so the 'abort' handler
880
+ // VOIDs + records a breaker failure — same ledger + breaker outcome as
881
+ // the generic createGovernedStream anomaly path (which throws
882
+ // AnomalyError → finalizeStreamVoid). finalizeOnce keeps it safe if a
883
+ // finalMessage is already in flight.
884
+ anomalyAbort = true;
885
+ if (typeof emitter.abort === "function")
886
+ emitter.abort();
887
+ }
888
+ });
889
+ emitter.on("finalMessage", (msg) => {
890
+ // A3: usage-extraction failure after a successful stream settles at
891
+ // ESTIMATE, never voids. F3: MERGE per-field — a finalMessage that
892
+ // carries only one field (or null for the other) keeps the counter
893
+ // accumulated from streamEvent for the MISSING field, rather than
894
+ // zeroing it. Reported when EITHER source carried a real number;
895
+ // otherwise settle on the estimate.
896
+ const finalUsage = readFinalMessageUsage(msg);
897
+ const usageReported = finalUsage.reported || accUsageReported;
898
+ const completion = usageReported
899
+ ? {
900
+ usage: {
901
+ inputTokens: finalUsage.inputTokens ?? accInput,
902
+ outputTokens: finalUsage.outputTokens ?? accOutput,
903
+ },
904
+ chunksDelivered,
905
+ usageReported: true,
906
+ }
907
+ : {
908
+ usage: { inputTokens: 0, outputTokens: 0 },
909
+ chunksDelivered,
910
+ usageReported: false,
911
+ };
912
+ finalizeStreamSettle(completion)
913
+ .then(receiptResolve, receiptReject)
914
+ .finally(releaseStreamCount);
915
+ });
916
+ const voidVia = (error) => {
917
+ // finalizeStreamVoid claims finalizeOnce("void") SYNCHRONOUSLY at its
918
+ // top, so finalizeState reflects the outcome the moment it returns.
919
+ finalizeStreamVoid(error, partial()).finally(releaseStreamCount);
920
+ // Only surface the void on `.receipt` when THIS terminal actually won
921
+ // the gate. A late abort after a settle is a ledger no-op (finalizeOnce
922
+ // already granted "settle") — it must not reject a receipt the settle
923
+ // path will resolve, even though that settle's async is still in flight.
924
+ if (finalizeState === "voided")
925
+ receiptReject(error);
926
+ };
927
+ // F9: a consumer-initiated abort surfaces as the SDK's 'abort' event
928
+ // (APIUserAbortError), distinct from the provider-failure 'error' event. It
929
+ // is an early EXIT, not a failure: settle at the PARTIAL accumulated usage
930
+ // (mirroring the generic governed-stream break-out-of-for-await path) instead
931
+ // of voiding, and never record a circuit-breaker failure — a caller who
932
+ // aborts N streams must not trip the breaker. finalizeStreamSettle records
933
+ // success. The `pending` guard makes a LATE abort after a real terminal a
934
+ // clean no-op: without it the no-op settle would resolve `.receipt` with a
935
+ // stale estimate, racing (and beating) the winning terminal's async settle.
936
+ const settlePartialVia = () => {
937
+ if (finalizeState !== "pending")
938
+ return;
939
+ finalizeStreamSettle(partial())
940
+ .then(receiptResolve, receiptReject)
941
+ .finally(releaseStreamCount);
942
+ };
943
+ emitter.on("error", (err) => voidVia(err ?? new Error("stream error")));
944
+ // R1: a GOVERNANCE anomaly cutoff (anomalyAbort) is a provider-side FAILURE —
945
+ // void the hold + record a breaker failure, matching the generic path. A
946
+ // genuine consumer abort is an early exit — settle the partial usage (F9).
947
+ emitter.on("abort", (err) => {
948
+ if (anomalyAbort) {
949
+ voidVia(err ?? new Error("anomaly cutoff"));
950
+ }
951
+ else {
952
+ settlePartialVia();
953
+ }
954
+ });
955
+ // F6: the real MessageStream emits NO 'finalMessage' on a clean SSE close
956
+ // that lacks a message_stop (receivedMessages stays empty → _emitFinal emits
957
+ // nothing) — only 'end' fires. Without an 'end' catch-all the PENDING hold
958
+ // would dangle until the TigerBeetle 300s timeout. When 'end' is the SOLE
959
+ // terminal (gate still pending) settle at ESTIMATE so the hold releases; on
960
+ // every other terminal ('end' always fires last, after the winning settle/
961
+ // void) the guard makes this a no-op that must NOT touch `.receipt`.
962
+ emitter.on("end", () => {
963
+ if (finalizeState !== "pending")
964
+ return;
965
+ finalizeStreamSettle({
966
+ usage: { inputTokens: 0, outputTokens: 0 },
967
+ chunksDelivered,
968
+ usageReported: false,
969
+ })
970
+ .then(receiptResolve, receiptReject)
971
+ .finally(releaseStreamCount);
972
+ });
973
+ attachReceipt();
974
+ return { response: streamObj, receipt: estimatedReceipt };
975
+ };
483
976
  try {
484
977
  let response;
485
978
  try {
@@ -498,6 +991,18 @@ export async function trust(client, opts) {
498
991
  }
499
992
  response = await originalFn.apply(thisArg, preInjectionArgs);
500
993
  }
994
+ // A1: Anthropic messages.stream / beta.messages.stream helper. The
995
+ // forwarded call returned a MessageStream (event-emitter + AsyncIterable)
996
+ // owned by the CALLER. Settle via its multicast emitter — this branch
997
+ // runs BEFORE the generic Symbol.asyncIterator path because a
998
+ // MessageStream matches that check too but must NEVER be routed through
999
+ // createGovernedStream (that would steal the caller's single async
1000
+ // iterator, A1). A synchronous throw from stream() (bad params, connect
1001
+ // failure before the object is returned) never reaches here — it lands in
1002
+ // the outer catch, which voids the still-PENDING hold once.
1003
+ if (surfaceKind === "stream-helper") {
1004
+ return settleViaMessageStream(response);
1005
+ }
501
1006
  // e2. Streaming detection: if response is an async iterable, wrap with
502
1007
  // token accumulation. Settlement and audit happen when the stream ends.
503
1008
  if (response != null &&
@@ -505,187 +1010,25 @@ export async function trust(client, opts) {
505
1010
  Symbol.asyncIterator in response) {
506
1011
  const stream = response;
507
1012
  const governedStream = createGovernedStream(stream, kind, async (completion) => {
508
- // Determine cost: use provider usage if reported, else fall back to
509
- // estimate. A3: priced with the rate resolution captured at
510
- // authorize; A11: costFromRates floors at 1 even for 0/0 usage.
511
- let streamCost;
512
- let usageSource;
513
- if (completion.usageReported) {
514
- streamCost = costFromRates(rateResolution.rates, completion.usage.inputTokens, completion.usage.outputTokens);
515
- usageSource = "provider";
516
- }
517
- else {
518
- streamCost = estimatedCost;
519
- usageSource = "estimated";
520
- }
521
- // Release in-flight hold and commit budget under mutex
522
- // AUD-468: holdActive guard prevents double-release if the
523
- // outer catch also fires (e.g. from a synchronous exception
524
- // during stream construction).
525
- if (holdActive) {
526
- holdActive = false;
527
- const releaseLock = await budgetMutex.acquire();
528
- inFlightHoldTotal -= estimatedCost;
529
- budgetSpent += streamCost;
530
- releaseLock();
531
- }
532
- else {
533
- // Hold already released — still need to record the spend.
534
- const releaseLock = await budgetMutex.acquire();
535
- budgetSpent += streamCost;
536
- releaseLock();
537
- }
538
- // AUD-457: Persist cumulative spend to disk
539
- await persistSpendLedger(vaultBase, budgetSpent);
540
- cb.recordSuccess();
541
- if (proxyConn != null && !isDryRun) {
542
- try {
543
- // AUD-460: Use the proxy's transferId for settlement
544
- await proxyConn.settle(proxyTransferId ?? transferId, streamCost);
545
- }
546
- catch (postErr) {
547
- settled = false;
548
- await audit
549
- .appendEvent({
550
- kind: "settlement_ambiguous",
551
- actor: "local",
552
- data: {
553
- model,
554
- cost: streamCost,
555
- transferId,
556
- error: postErr instanceof Error
557
- ? postErr.message.slice(0, 200)
558
- : String(postErr).slice(0, 200),
559
- },
560
- })
561
- .catch(() => {
562
- callAuditDegraded = true;
563
- });
564
- }
565
- }
566
- else if (engine != null && !isDryRun) {
567
- try {
568
- // Post the ACTUAL consumed cost (RECON #3).
569
- await engine.postPendingSpend(transferId, streamCost);
570
- }
571
- catch (postErr) {
572
- settled = false;
573
- await audit
574
- .appendEvent({
575
- kind: "settlement_ambiguous",
576
- actor: "local",
577
- data: {
578
- model,
579
- cost: streamCost,
580
- transferId,
581
- error: postErr instanceof Error
582
- ? postErr.message.slice(0, 200)
583
- : String(postErr).slice(0, 200),
584
- },
585
- })
586
- .catch(() => {
587
- callAuditDegraded = true;
588
- });
589
- }
590
- }
591
- const syntheticHash = createHash("sha256").update(transferId).digest("hex");
592
- let auditHash = syntheticHash;
1013
+ // A2: route the createGovernedStream terminal through the shared
1014
+ // idempotent settle gate. The finally decrements the stream counter on
1015
+ // BOTH the receipt return and the failClosed throw.
593
1016
  try {
594
- const auditEventData = {
595
- model,
596
- cost: streamCost,
597
- settled,
598
- transferId,
599
- usageSource,
600
- chunksDelivered: completion.chunksDelivered,
601
- // M2: metering provenance mirrors the receipt (A3 authorize-time scope).
602
- endpointClass: endpoint.class,
603
- costBasis: rateResolution.costBasis,
604
- rateSource: rateResolution.rateSource,
605
- };
606
- if (config.pii === "warn" || config.pii === "redact") {
607
- const piiResult = redactPII(promptParts);
608
- if (piiResult.detection.found) {
609
- auditEventData.piiDetected = piiResult.detection.types;
610
- auditEventData.piiPaths = piiResult.detection.paths;
611
- }
612
- }
613
- const auditEvent = await audit.appendEvent({
614
- kind: "llm_call",
615
- actor: "local",
616
- data: auditEventData,
617
- });
618
- auditHash = auditEvent.hash;
1017
+ return await finalizeStreamSettle(completion);
619
1018
  }
620
- catch {
621
- callAuditDegraded = true;
622
- }
623
- // P3-AUDIT-FAILCLOSED (streaming): chunks were already delivered,
624
- // so the strongest post-delivery signal is to REJECT `.receipt`.
625
- // Decrement the stream counter first so destroy() never blocks.
626
- if (config.audit.failClosed && (callAuditDegraded || audit.isDegraded())) {
1019
+ finally {
627
1020
  inFlightStreamCount--;
628
- throw new AuditDegradedError(`audit unavailable (writeFailures=${audit.getWriteFailures()}) for ${transferId}`);
629
1021
  }
630
- const streamReceipt = {
631
- transferId,
632
- cost: streamCost,
633
- budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
634
- auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : auditHash,
635
- chainPath: join(VAULT_DIR, "audit"),
636
- receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
637
- settled,
638
- model,
639
- provider: kind,
640
- timestamp: new Date().toISOString(),
641
- usageSource,
642
- chunksDelivered: completion.chunksDelivered,
643
- // M2 provenance (A6: computeMs omitted — no compute-time source here).
644
- endpoint: { class: endpoint.class, runtime: endpoint.runtime },
645
- meter: {
646
- costBasis: rateResolution.costBasis,
647
- rateSource: rateResolution.rateSource,
648
- },
649
- ...(callAuditDegraded ? { auditDegraded: true } : {}),
650
- // AUD-456: Flag proxy stub receipts
651
- ...(proxyConn != null ? { proxyStub: true } : {}),
652
- };
653
- inFlightStreamCount--;
654
- return streamReceipt;
655
1022
  }, async (error, partial) => {
656
- // Release in-flight hold under mutex
657
- // AUD-468: holdActive guard prevents double-release.
658
- await releaseInFlightHold();
659
- cb.recordFailure();
660
- // Best-effort audit of partial delivery
661
- audit
662
- .appendEvent({
663
- kind: "stream_partial_delivery",
664
- actor: "local",
665
- data: {
666
- transferId,
667
- model,
668
- chunksDelivered: partial.chunksDelivered,
669
- partialInputTokens: partial.usage.inputTokens,
670
- partialOutputTokens: partial.usage.outputTokens,
671
- usageReported: partial.usageReported,
672
- error: (() => {
673
- const raw = error instanceof Error ? error.message : String(error);
674
- return config.pii === "warn" || config.pii === "redact"
675
- ? redactPII(raw).data.slice(0, 200)
676
- : raw.slice(0, 200);
677
- })(),
678
- },
679
- })
680
- .catch(() => { });
681
- if (proxyConn != null && !isDryRun) {
682
- // AUD-460: Use the proxy's transferId for void
683
- proxyConn.void(proxyTransferId ?? transferId).catch(() => { });
1023
+ // A2: route the createGovernedStream error terminal through the shared
1024
+ // idempotent void gate. The finally decrements the stream counter exactly
1025
+ // as the success path does.
1026
+ try {
1027
+ await finalizeStreamVoid(error, partial);
684
1028
  }
685
- else if (engine != null && !isDryRun) {
686
- engine.voidPendingSpend(transferId).catch(() => { });
1029
+ finally {
1030
+ inFlightStreamCount--;
687
1031
  }
688
- inFlightStreamCount--;
689
1032
  },
690
1033
  // onChunk: streaming anomaly detector hook. Observe each chunk;
691
1034
  // if a signal trips, throw AnomalyError to abort the stream.
@@ -727,30 +1070,9 @@ export async function trust(client, opts) {
727
1070
  }
728
1071
  });
729
1072
  // AUD-454: For streaming responses, settlement has NOT happened yet.
730
- // Set settled: false the real settlement status will be on
731
- // governedStream.receipt after the stream is fully consumed.
732
- const streamEstimateHash = createHash("sha256").update(transferId).digest("hex");
733
- const estimatedReceipt = {
734
- transferId,
735
- cost: estimatedCost,
736
- budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
737
- auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : streamEstimateHash,
738
- chainPath: join(VAULT_DIR, "audit"),
739
- receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
740
- settled: false, // AUD-454: not settled yet — stream hasn't been consumed
741
- model,
742
- provider: kind,
743
- timestamp: new Date().toISOString(),
744
- // M2: authorize-time scope, already fixed for the eventual settle (A3).
745
- endpoint: { class: endpoint.class, runtime: endpoint.runtime },
746
- meter: {
747
- costBasis: rateResolution.costBasis,
748
- rateSource: rateResolution.rateSource,
749
- },
750
- ...(callAuditDegraded ? { auditDegraded: true } : {}),
751
- // AUD-456: Flag proxy stub receipts
752
- ...(proxyConn != null ? { proxyStub: true } : {}),
753
- };
1073
+ // The initial handle carries settled:false at the estimate the real
1074
+ // settlement status lands on governedStream.receipt after consumption.
1075
+ const estimatedReceipt = buildEstimatedStreamReceipt();
754
1076
  inFlightStreamCount++;
755
1077
  return { response: governedStream, receipt: estimatedReceipt };
756
1078
  }
@@ -819,6 +1141,11 @@ export async function trust(client, opts) {
819
1141
  if (config.audit.failClosed && llmAuditFailed) {
820
1142
  throw new AuditDegradedError(`audit unavailable (writeFailures=${audit.getWriteFailures()}) for ${transferId}`);
821
1143
  }
1144
+ // A2: claim the single settle outcome for this hold BEFORE money moves.
1145
+ // Always granted here (no other terminal path runs for a non-stream call);
1146
+ // the claim exists so a post-commit throw routes to the outer catch WITHOUT
1147
+ // double-voiding an already-settled hold.
1148
+ finalizeOnce("settle");
822
1149
  // Release in-flight hold and commit budget under mutex — money moves only
823
1150
  // AFTER the spend is audited. AUD-468: mark hold released before the commit
824
1151
  // so a later throw cannot double-decrement inFlightHoldTotal via the catch.
@@ -943,23 +1270,32 @@ export async function trust(client, opts) {
943
1270
  await releaseInFlightHold();
944
1271
  // j. Circuit breaker: record failure
945
1272
  cb.recordFailure();
946
- // j2. Failure mode 15.2: LLM fails VOID the pending hold
947
- if (engine != null && !isDryRun) {
948
- try {
949
- await engine.voidPendingSpend(transferId);
950
- }
951
- catch {
952
- // Best-effort void log and continue
953
- }
954
- }
955
- // j3. Proxy void
956
- if (proxyConn != null && !isDryRun) {
957
- try {
958
- // AUD-460: Use the proxy's transferId for void
959
- await proxyConn.void(proxyTransferId ?? transferId);
1273
+ // j2/j3. VOID the pending holdbut ONLY if this hold has not already
1274
+ // claimed a settle outcome (A2). A pre-commit throw (provider/transport
1275
+ // error, PII/policy deny, failClosed-before-commit) reaches here with the
1276
+ // hold still PENDING → finalizeOnce("void") is granted and the ledger is
1277
+ // voided. A post-commit throw (failClosed belt-and-suspenders) already
1278
+ // settled the spend → the claim is refused and no spurious void is issued
1279
+ // against an already-posted transfer.
1280
+ if (finalizeOnce("void")) {
1281
+ // Failure mode 15.2: LLM fails — VOID the pending hold
1282
+ if (engine != null && !isDryRun) {
1283
+ try {
1284
+ await engine.voidPendingSpend(transferId);
1285
+ }
1286
+ catch {
1287
+ // Best-effort void — log and continue
1288
+ }
960
1289
  }
961
- catch {
962
- // Best-effort void
1290
+ // Proxy void
1291
+ if (proxyConn != null && !isDryRun) {
1292
+ try {
1293
+ // AUD-460: Use the proxy's transferId for void
1294
+ await proxyConn.void(proxyTransferId ?? transferId);
1295
+ }
1296
+ catch {
1297
+ // Best-effort void
1298
+ }
963
1299
  }
964
1300
  }
965
1301
  // k. Audit the failure
@@ -1370,13 +1706,46 @@ export async function trust(client, opts) {
1370
1706
  * Without this, a Google `generateContent({ model, contents })` call has an empty
1371
1707
  * `messages` array, so every PII/injection scan sees nothing and PII egresses.
1372
1708
  */
1373
- function extractPromptParts(params, kind) {
1709
+ /**
1710
+ * Normalize the prompt-bearing payload across providers into a flat parts array
1711
+ * that PII/injection scanning, token estimation, redaction, and pattern hashing
1712
+ * all consume. Exported for the A8 byte-exact regression test (kept internal to
1713
+ * the package — not re-exported from index.ts).
1714
+ *
1715
+ * Branch order is load-bearing (A8): the OpenAI Responses branch is shape-
1716
+ * discriminated (`kind === "openai"` AND no `messages` key) and placed so it can
1717
+ * NEVER preempt the chat.completions / Anthropic `messages` branch — every
1718
+ * chat.completions or Anthropic call carries `messages` and skips it, so existing
1719
+ * extraction stays byte-unchanged.
1720
+ */
1721
+ export function extractPromptParts(params, kind) {
1374
1722
  if (kind === "google") {
1375
1723
  const contents = params.contents;
1376
1724
  if (Array.isArray(contents))
1377
1725
  return contents;
1378
1726
  return contents != null ? [contents] : [];
1379
1727
  }
1728
+ // A8: OpenAI Responses API uses `input`/`instructions`, not `messages`. Wrap the
1729
+ // input so BOTH the token estimator (which walks a message's `content`) and the
1730
+ // recursive PII/injection scanners (which deep-walk every string) cover it,
1731
+ // whether `input` is a plain string, a message array, or a content-part array.
1732
+ if (kind === "openai" && params.messages === undefined) {
1733
+ const parts = [];
1734
+ const input = params.input;
1735
+ if (typeof input === "string") {
1736
+ parts.push({ role: "user", content: input });
1737
+ }
1738
+ else if (Array.isArray(input)) {
1739
+ parts.push({ role: "user", content: input });
1740
+ }
1741
+ else if (input != null && typeof input === "object") {
1742
+ parts.push({ role: "user", content: [input] });
1743
+ }
1744
+ if (typeof params.instructions === "string") {
1745
+ parts.push({ role: "system", content: params.instructions });
1746
+ }
1747
+ return parts;
1748
+ }
1380
1749
  const messages = params.messages;
1381
1750
  const parts = Array.isArray(messages) ? [...messages] : [];
1382
1751
  if (typeof params.system === "string") {
@@ -1384,14 +1753,68 @@ function extractPromptParts(params, kind) {
1384
1753
  }
1385
1754
  return parts;
1386
1755
  }
1756
+ // ── Anthropic MessageStream helpers (Task 2, A1/A3) ──
1757
+ /**
1758
+ * A1/A3: read provider usage off an Anthropic final Message (the `finalMessage`
1759
+ * event payload). Each field is returned PER-FIELD: a finite, non-negative count
1760
+ * when present, else `undefined` (so the caller can fall back to the counter it
1761
+ * accumulated from streamEvent for THAT field, rather than zeroing it — F3).
1762
+ * `reported` is true when at least one field carried a real number; when false the
1763
+ * caller settles at ESTIMATE rather than voiding a billable success (A3).
1764
+ */
1765
+ function readFinalMessageUsage(msg) {
1766
+ if (msg != null && typeof msg === "object") {
1767
+ const usage = msg.usage;
1768
+ if (usage != null && typeof usage === "object") {
1769
+ const u = usage;
1770
+ const inTok = typeof u.input_tokens === "number" && Number.isFinite(u.input_tokens) && u.input_tokens >= 0
1771
+ ? u.input_tokens
1772
+ : undefined;
1773
+ const outTok = typeof u.output_tokens === "number" &&
1774
+ Number.isFinite(u.output_tokens) &&
1775
+ u.output_tokens >= 0
1776
+ ? u.output_tokens
1777
+ : undefined;
1778
+ if (inTok !== undefined || outTok !== undefined) {
1779
+ return { inputTokens: inTok, outputTokens: outTok, reported: true };
1780
+ }
1781
+ }
1782
+ }
1783
+ return { inputTokens: undefined, outputTokens: undefined, reported: false };
1784
+ }
1785
+ /**
1786
+ * A1: incremental token extraction from a raw Anthropic stream event, for the
1787
+ * non-consuming messages.stream tap. Kept local to govern.ts (Task 2 does not
1788
+ * touch streaming.ts); mirrors streaming.ts extractAnthropicTokens — message_start
1789
+ * carries input_tokens, message_delta carries cumulative output_tokens.
1790
+ */
1791
+ function extractAnthropicStreamUsage(event) {
1792
+ if (event == null || typeof event !== "object")
1793
+ return { inputTokens: 0, outputTokens: 0 };
1794
+ const c = event;
1795
+ if (c.type === "message_start" && c.message != null && typeof c.message === "object") {
1796
+ const msg = c.message;
1797
+ if (msg.usage != null && typeof msg.usage === "object") {
1798
+ const usage = msg.usage;
1799
+ const inTok = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
1800
+ return { inputTokens: inTok > 0 ? inTok : 0, outputTokens: 0 };
1801
+ }
1802
+ }
1803
+ if (c.type === "message_delta" && c.usage != null && typeof c.usage === "object") {
1804
+ const usage = c.usage;
1805
+ const outTok = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
1806
+ return { inputTokens: 0, outputTokens: outTok > 0 ? outTok : 0 };
1807
+ }
1808
+ return { inputTokens: 0, outputTokens: 0 };
1809
+ }
1387
1810
  // ── TigerBeetle engine factory ──
1388
1811
  /** TigerBeetle codes that mean "this debit would exceed the account's credits". */
1389
1812
  function isTBInsufficientBalance(err) {
1390
1813
  if (!(err instanceof TBTransferError))
1391
1814
  return false;
1392
- return (err.code === CreateTransferError.exceeds_credits ||
1393
- err.code === CreateTransferError.overflows_debits ||
1394
- err.code === CreateTransferError.overflows_debits_pending);
1815
+ return (err.code === CreateTransferStatus.exceeds_credits ||
1816
+ err.code === CreateTransferStatus.overflows_debits ||
1817
+ err.code === CreateTransferStatus.overflows_debits_pending);
1395
1818
  }
1396
1819
  /**
1397
1820
  * Create a balance-enforcing TrustEngine backed by a real TigerBeetle client.
@@ -1486,24 +1909,173 @@ async function createTBEngine(config, seedBudget) {
1486
1909
  },
1487
1910
  };
1488
1911
  }
1489
- function buildAnthropicProxy(client, intercept, destroy, governAction) {
1490
- const original = client;
1491
- const messages = original.messages;
1912
+ /**
1913
+ * F4: mirror the Anthropic SDK's `parseMessage` / `parseBetaMessage` transform for a
1914
+ * governed `messages.parse()` / `beta.messages.parse()`. The SDK's `parse()` is
1915
+ * `this.create(params).then((m) => parseMessage(m, params))` — routed through our
1916
+ * proxy, `this.create` is the GOVERNED create, which returns a `{ response, receipt }`
1917
+ * wrapper the SDK parser crashes on (`wrapper.content` is undefined) AFTER the spend
1918
+ * has already settled. So the proxy runs the governed create itself and applies the
1919
+ * transform to the raw Message here.
1920
+ *
1921
+ * Faithful to the SDK for the modern `parsed_output` API: each `text` block gains a
1922
+ * NON-ENUMERABLE `parsed_output` (parsed via the caller-provided
1923
+ * `output_config.format.parse` when structured outputs are requested, else `JSON.parse`,
1924
+ * else null), and the message carries the first text block's `parsed_output` at the top
1925
+ * level. Reads the deprecated beta `output_format` too, so ONE transform covers the
1926
+ * stable and beta surfaces (stable params simply lack `output_format`). A structured
1927
+ * parse failure throws, matching the SDK. The deprecated per-block `parsed` getter is
1928
+ * intentionally not reproduced (superseded by `parsed_output`).
1929
+ */
1930
+ function applyAnthropicParseTransform(message, params) {
1931
+ if (message == null || typeof message !== "object")
1932
+ return message;
1933
+ const p = (params ?? {});
1934
+ const outputConfig = p.output_config;
1935
+ const format = (p.output_format ?? outputConfig?.format);
1936
+ const hasSchema = format != null && typeof format === "object" && format.type === "json_schema";
1937
+ const parseText = (text) => {
1938
+ if (!hasSchema)
1939
+ return null;
1940
+ try {
1941
+ return typeof format?.parse === "function"
1942
+ ? format.parse(text)
1943
+ : JSON.parse(text);
1944
+ }
1945
+ catch (err) {
1946
+ throw new Error(`Failed to parse structured output: ${err}`);
1947
+ }
1948
+ };
1949
+ const msg = message;
1950
+ let firstParsed = null;
1951
+ const content = Array.isArray(msg.content)
1952
+ ? msg.content.map((block) => {
1953
+ if (block != null &&
1954
+ typeof block === "object" &&
1955
+ block.type === "text") {
1956
+ const parsed = parseText(String(block.text ?? ""));
1957
+ if (firstParsed === null)
1958
+ firstParsed = parsed;
1959
+ const parsedBlock = { ...block };
1960
+ Object.defineProperty(parsedBlock, "parsed_output", {
1961
+ value: parsed,
1962
+ enumerable: false,
1963
+ });
1964
+ return parsedBlock;
1965
+ }
1966
+ return block;
1967
+ })
1968
+ : msg.content;
1969
+ return { ...msg, content, parsed_output: firstParsed };
1970
+ }
1971
+ /**
1972
+ * A1/A5: build a governed proxy over an Anthropic `messages` resource (stable or
1973
+ * beta — both share the create/stream shape). `create` routes through
1974
+ * interceptCall (surface "create"). `stream`, when present as a function (A5
1975
+ * feature-detect), routes through interceptCall (surface "stream-helper") bound to
1976
+ * the RAW `messages` target (A5) so the helper's internal
1977
+ * `messages.create({stream:true}).withResponse()` runs against the un-proxied
1978
+ * create and never re-enters the governed create trap. The governed `stream` is
1979
+ * async (it authorizes before forwarding), so callers `await` the handle, then use
1980
+ * `.on()` / `.finalMessage()` / `.abort()` / `.receipt`. `parse`, when present (A5
1981
+ * feature-detect), routes its create through governance and applies the SDK parse
1982
+ * transform (F4). Everything else (countTokens, batches, …) falls through to
1983
+ * Reflect.get untouched.
1984
+ */
1985
+ function buildAnthropicMessagesProxy(messages, intercept) {
1492
1986
  const originalCreate = messages.create;
1493
- // Proxy on the messages object: intercept `create`
1494
- const messagesProxy = new Proxy(messages, {
1987
+ // A5: feature-detect `stream` as a FUNCTION before wrapping it — an older SDK
1988
+ // without the helper leaves it absent and it stays a raw pass-through.
1989
+ const originalStream = typeof messages.stream === "function"
1990
+ ? messages.stream
1991
+ : undefined;
1992
+ // A5/F4: feature-detect `parse` the same way — absent on older SDKs.
1993
+ const hasParse = typeof messages.parse === "function";
1994
+ return new Proxy(messages, {
1495
1995
  get(target, prop, receiver) {
1496
1996
  if (prop === "create") {
1497
1997
  return (...args) => intercept(originalCreate, target, args);
1498
1998
  }
1999
+ if (prop === "stream" && originalStream !== undefined) {
2000
+ return async (...args) => {
2001
+ const { response } = await intercept(originalStream, target, args, "stream-helper");
2002
+ return response;
2003
+ };
2004
+ }
2005
+ if (prop === "parse" && hasParse) {
2006
+ // F4: run the governed create ONCE, then apply the SDK parse transform to
2007
+ // the settled Message and hand back the parsed Message (SDK-shaped) with
2008
+ // the settlement receipt attached.
2009
+ return async (...args) => {
2010
+ const { response, receipt } = await intercept(originalCreate, target, args);
2011
+ const parsed = applyAnthropicParseTransform(response, args[0]);
2012
+ if (parsed != null && typeof parsed === "object") {
2013
+ try {
2014
+ Object.defineProperty(parsed, "receipt", {
2015
+ value: receipt,
2016
+ enumerable: true,
2017
+ configurable: true,
2018
+ });
2019
+ }
2020
+ catch {
2021
+ // non-extensible parsed object — skip
2022
+ }
2023
+ }
2024
+ return parsed;
2025
+ };
2026
+ }
1499
2027
  return Reflect.get(target, prop, receiver);
1500
2028
  },
1501
2029
  });
1502
- // Proxy on the client: intercept `messages` to return our proxy, add `destroy`
2030
+ }
2031
+ /**
2032
+ * A5: build a governed proxy over `client.beta` when it exposes a create-shaped
2033
+ * `beta.messages`. Returns undefined on any miss (no beta, non-object beta, a beta
2034
+ * getter that throws, or a beta.messages without a create function) so the caller
2035
+ * falls back to raw Reflect.get and NOTHING throws at wrap time (peerDep-range
2036
+ * compatibility). beta.models / beta.files / beta.messages.batches stay documented
2037
+ * pass-throughs — create, stream, and parse on beta.messages are governed.
2038
+ */
2039
+ function buildAnthropicBetaProxy(original, intercept) {
2040
+ let beta;
2041
+ try {
2042
+ beta = original.beta;
2043
+ }
2044
+ catch {
2045
+ return undefined;
2046
+ }
2047
+ if (beta == null || typeof beta !== "object")
2048
+ return undefined;
2049
+ const betaObj = beta;
2050
+ const betaMessages = betaObj.messages;
2051
+ if (betaMessages == null ||
2052
+ typeof betaMessages !== "object" ||
2053
+ typeof betaMessages.create !== "function") {
2054
+ return undefined;
2055
+ }
2056
+ const betaMessagesProxy = buildAnthropicMessagesProxy(betaMessages, intercept);
2057
+ return new Proxy(betaObj, {
2058
+ get(target, prop, receiver) {
2059
+ if (prop === "messages")
2060
+ return betaMessagesProxy;
2061
+ return Reflect.get(target, prop, receiver);
2062
+ },
2063
+ });
2064
+ }
2065
+ function buildAnthropicProxy(client, intercept, destroy, governAction) {
2066
+ const original = client;
2067
+ const messages = original.messages;
2068
+ // Stable messages: governed `create` + (feature-detected) `stream`.
2069
+ const messagesProxy = buildAnthropicMessagesProxy(messages, intercept);
2070
+ // beta.messages.create/stream/parse — governed identically when present (A5); a
2071
+ // client without `beta` yields undefined and `beta` stays a raw pass-through.
2072
+ const betaProxy = buildAnthropicBetaProxy(original, intercept);
1503
2073
  const clientProxy = new Proxy(original, {
1504
2074
  get(target, prop, receiver) {
1505
2075
  if (prop === "messages")
1506
2076
  return messagesProxy;
2077
+ if (prop === "beta" && betaProxy !== undefined)
2078
+ return betaProxy;
1507
2079
  if (prop === "destroy")
1508
2080
  return destroy;
1509
2081
  if (prop === "governAction")
@@ -1513,6 +2085,42 @@ function buildAnthropicProxy(client, intercept, destroy, governAction) {
1513
2085
  });
1514
2086
  return clientProxy;
1515
2087
  }
2088
+ /**
2089
+ * Task 3 (A5): build a governed proxy over `client.responses` when the OpenAI SDK
2090
+ * exposes a create-shaped Responses API. Returns undefined on any miss (no
2091
+ * `responses`, non-object `responses`, a getter that throws, or a `responses`
2092
+ * without a `create` function) so the caller falls back to raw Reflect.get and
2093
+ * NOTHING throws at wrap time — the peer range (`openai >=4.70.0`) predates the
2094
+ * Responses API (~4.87), so older clients must keep working untouched.
2095
+ *
2096
+ * `responses.create` routes through interceptCall on the SEPARATE "openai-responses"
2097
+ * surface (A6: no stream_options.include_usage injection; A7: streaming still flows
2098
+ * through the generic createGovernedStream path). `this` binds to the RAW `responses`
2099
+ * object. Everything else (retrieve / cancel / delete / …) stays a pass-through.
2100
+ */
2101
+ function buildOpenAIResponsesProxy(original, intercept) {
2102
+ let responses;
2103
+ try {
2104
+ responses = original.responses;
2105
+ }
2106
+ catch {
2107
+ return undefined;
2108
+ }
2109
+ if (responses == null || typeof responses !== "object")
2110
+ return undefined;
2111
+ const responsesObj = responses;
2112
+ if (typeof responsesObj.create !== "function")
2113
+ return undefined;
2114
+ const originalCreate = responsesObj.create;
2115
+ return new Proxy(responsesObj, {
2116
+ get(target, prop, receiver) {
2117
+ if (prop === "create") {
2118
+ return (...args) => intercept(originalCreate, target, args, "openai-responses");
2119
+ }
2120
+ return Reflect.get(target, prop, receiver);
2121
+ },
2122
+ });
2123
+ }
1516
2124
  function buildOpenAIProxy(client, intercept, destroy, governAction) {
1517
2125
  const original = client;
1518
2126
  const chat = original.chat;
@@ -1533,10 +2141,15 @@ function buildOpenAIProxy(client, intercept, destroy, governAction) {
1533
2141
  return Reflect.get(target, prop, receiver);
1534
2142
  },
1535
2143
  });
2144
+ // responses.create — governed when present (A5); an older client without the
2145
+ // Responses API yields undefined and `responses` stays a raw pass-through.
2146
+ const responsesProxy = buildOpenAIResponsesProxy(original, intercept);
1536
2147
  const clientProxy = new Proxy(original, {
1537
2148
  get(target, prop, receiver) {
1538
2149
  if (prop === "chat")
1539
2150
  return chatProxy;
2151
+ if (prop === "responses" && responsesProxy !== undefined)
2152
+ return responsesProxy;
1540
2153
  if (prop === "destroy")
1541
2154
  return destroy;
1542
2155
  if (prop === "governAction")