usertrust 1.0.0 → 1.1.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 (80) hide show
  1. package/dist/cli/init.d.ts +2 -0
  2. package/dist/cli/init.d.ts.map +1 -1
  3. package/dist/cli/init.js +231 -37
  4. package/dist/cli/init.js.map +1 -1
  5. package/dist/cli/main.d.ts +1 -1
  6. package/dist/cli/main.d.ts.map +1 -1
  7. package/dist/cli/main.js +18 -3
  8. package/dist/cli/main.js.map +1 -1
  9. package/dist/cli/pricing.d.ts +5 -0
  10. package/dist/cli/pricing.d.ts.map +1 -0
  11. package/dist/cli/pricing.js +61 -0
  12. package/dist/cli/pricing.js.map +1 -0
  13. package/dist/cli/validate-key.d.ts +17 -0
  14. package/dist/cli/validate-key.d.ts.map +1 -0
  15. package/dist/cli/validate-key.js +58 -0
  16. package/dist/cli/validate-key.js.map +1 -0
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +31 -0
  19. package/dist/config.js.map +1 -1
  20. package/dist/govern.d.ts +3 -1
  21. package/dist/govern.d.ts.map +1 -1
  22. package/dist/govern.js +415 -47
  23. package/dist/govern.js.map +1 -1
  24. package/dist/headless.d.ts +77 -0
  25. package/dist/headless.d.ts.map +1 -0
  26. package/dist/headless.js +566 -0
  27. package/dist/headless.js.map +1 -0
  28. package/dist/index.d.ts +12 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +13 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/ledger/pricing.d.ts +6 -2
  33. package/dist/ledger/pricing.d.ts.map +1 -1
  34. package/dist/ledger/pricing.js +31 -3
  35. package/dist/ledger/pricing.js.map +1 -1
  36. package/dist/policy/canary.d.ts +17 -0
  37. package/dist/policy/canary.d.ts.map +1 -0
  38. package/dist/policy/canary.js +44 -0
  39. package/dist/policy/canary.js.map +1 -0
  40. package/dist/policy/injection.d.ts +24 -0
  41. package/dist/policy/injection.d.ts.map +1 -0
  42. package/dist/policy/injection.js +178 -0
  43. package/dist/policy/injection.js.map +1 -0
  44. package/dist/policy/pii.d.ts +13 -1
  45. package/dist/policy/pii.d.ts.map +1 -1
  46. package/dist/policy/pii.js +88 -0
  47. package/dist/policy/pii.js.map +1 -1
  48. package/dist/shared/errors.d.ts +20 -0
  49. package/dist/shared/errors.d.ts.map +1 -1
  50. package/dist/shared/errors.js +46 -0
  51. package/dist/shared/errors.js.map +1 -1
  52. package/dist/shared/types.d.ts +264 -0
  53. package/dist/shared/types.d.ts.map +1 -1
  54. package/dist/shared/types.js +67 -0
  55. package/dist/shared/types.js.map +1 -1
  56. package/dist/supply-chain/manifest.d.ts +22 -0
  57. package/dist/supply-chain/manifest.d.ts.map +1 -0
  58. package/dist/supply-chain/manifest.js +46 -0
  59. package/dist/supply-chain/manifest.js.map +1 -0
  60. package/dist/supply-chain/permissions.d.ts +12 -0
  61. package/dist/supply-chain/permissions.d.ts.map +1 -0
  62. package/dist/supply-chain/permissions.js +89 -0
  63. package/dist/supply-chain/permissions.js.map +1 -0
  64. package/dist/supply-chain/sign.d.ts +17 -0
  65. package/dist/supply-chain/sign.d.ts.map +1 -0
  66. package/dist/supply-chain/sign.js +95 -0
  67. package/dist/supply-chain/sign.js.map +1 -0
  68. package/dist/vault/derive.d.ts +13 -0
  69. package/dist/vault/derive.d.ts.map +1 -0
  70. package/dist/vault/derive.js +32 -0
  71. package/dist/vault/derive.js.map +1 -0
  72. package/dist/vault/scope.d.ts +20 -0
  73. package/dist/vault/scope.d.ts.map +1 -0
  74. package/dist/vault/scope.js +35 -0
  75. package/dist/vault/scope.js.map +1 -0
  76. package/dist/vault/store.d.ts +31 -0
  77. package/dist/vault/store.d.ts.map +1 -0
  78. package/dist/vault/store.js +204 -0
  79. package/dist/vault/store.js.map +1 -0
  80. package/package.json +13 -2
package/dist/govern.js CHANGED
@@ -36,8 +36,10 @@ import { detectClientKind } from "./detect.js";
36
36
  import { TrustTBClient, XFER_SPEND } from "./ledger/client.js";
37
37
  import { estimateCost, estimateInputTokens } from "./ledger/pricing.js";
38
38
  import { recordPattern } from "./memory/patterns.js";
39
+ import { DEFAULT_RULES } from "./policy/default-rules.js";
39
40
  import { evaluatePolicy, loadPolicies } from "./policy/gate.js";
40
- import { detectPII } from "./policy/pii.js";
41
+ import { detectInjection } from "./policy/injection.js";
42
+ import { detectPII, redactPII } from "./policy/pii.js";
41
43
  import { connectProxy } from "./proxy.js";
42
44
  import { CircuitBreakerRegistry } from "./resilience/circuit.js";
43
45
  import { DEFAULT_BUDGET, VAULT_DIR } from "./shared/constants.js";
@@ -122,6 +124,7 @@ export async function trust(client, opts) {
122
124
  budget: opts?.budget ?? DEFAULT_BUDGET,
123
125
  });
124
126
  }
127
+ const customRates = config.pricing === "custom" ? config.customRates : undefined;
125
128
  const isDryRun = opts?.dryRun ?? process.env.USERTRUST_DRY_RUN === "true";
126
129
  // AUD-470: Only accept injected _engine/_audit in test environments.
127
130
  // In production, silently ignore them to prevent governance bypass.
@@ -130,7 +133,8 @@ export async function trust(client, opts) {
130
133
  const vaultPath = vaultBase;
131
134
  const audit = (isTestEnv ? opts?._audit : undefined) ?? createAuditWriter(vaultPath);
132
135
  const policiesPath = join(vaultPath, VAULT_DIR, config.policies);
133
- const policyRules = existsSync(policiesPath) ? loadPolicies(policiesPath) : [];
136
+ const loadedRules = existsSync(policiesPath) ? loadPolicies(policiesPath) : [];
137
+ const policyRules = loadedRules.length > 0 ? loadedRules : DEFAULT_RULES;
134
138
  const breaker = new CircuitBreakerRegistry({
135
139
  failureThreshold: config.circuitBreaker.failureThreshold,
136
140
  resetTimeoutMs: config.circuitBreaker.resetTimeout,
@@ -186,7 +190,7 @@ export async function trust(client, opts) {
186
190
  const transferId = trustId("tx");
187
191
  const estimatedInputTokens = estimateInputTokens(messages);
188
192
  const maxOutputTokens = params.max_tokens ?? 4096;
189
- const estimatedCost = estimateCost(model, estimatedInputTokens, maxOutputTokens);
193
+ const estimatedCost = estimateCost(model, estimatedInputTokens, maxOutputTokens, customRates);
190
194
  // AUD-453: Acquire mutex to serialise budget-check + PENDING hold.
191
195
  // This prevents concurrent calls from both passing the budget check
192
196
  // and overshooting the budget.
@@ -214,7 +218,28 @@ export async function trust(client, opts) {
214
218
  }
215
219
  // "warn" and "redact" modes: continue (redact is not implemented at SDK level)
216
220
  }
217
- // d2. Failure mode 15.4: TigerBeetle / engine unreachable — PENDING hold
221
+ // d2. Injection detection
222
+ if (config.injection !== "off") {
223
+ const injectionResult = detectInjection(messages);
224
+ if (injectionResult.detected) {
225
+ if (config.injection === "block") {
226
+ throw new PolicyDeniedError(`Prompt injection detected: ${injectionResult.patterns.join(", ")}`);
227
+ }
228
+ // warn: log to audit trail (non-fatal)
229
+ await audit
230
+ .appendEvent({
231
+ kind: "injection_detected",
232
+ actor: "local",
233
+ data: {
234
+ patterns: injectionResult.patterns,
235
+ score: injectionResult.score,
236
+ model,
237
+ },
238
+ })
239
+ .catch(() => { });
240
+ }
241
+ }
242
+ // e. Failure mode 15.4: TigerBeetle / engine unreachable — PENDING hold
218
243
  if (proxyConn != null && !isDryRun) {
219
244
  try {
220
245
  // AUD-460: Capture the proxy's returned transferId
@@ -263,16 +288,20 @@ export async function trust(client, opts) {
263
288
  let streamCost;
264
289
  let usageSource;
265
290
  if (completion.usageReported) {
266
- streamCost = estimateCost(model, completion.usage.inputTokens, completion.usage.outputTokens);
291
+ streamCost = estimateCost(model, completion.usage.inputTokens, completion.usage.outputTokens, customRates);
267
292
  usageSource = "provider";
268
293
  }
269
294
  else {
270
295
  streamCost = estimatedCost;
271
296
  usageSource = "estimated";
272
297
  }
273
- // Release in-flight hold
274
- inFlightHoldTotal -= estimatedCost;
275
- budgetSpent += streamCost;
298
+ // Release in-flight hold and commit budget under mutex
299
+ {
300
+ const releaseLock = await budgetMutex.acquire();
301
+ inFlightHoldTotal -= estimatedCost;
302
+ budgetSpent += streamCost;
303
+ releaseLock();
304
+ }
276
305
  // AUD-457: Persist cumulative spend to disk
277
306
  await persistSpendLedger(vaultBase, budgetSpent);
278
307
  cb.recordSuccess();
@@ -328,17 +357,25 @@ export async function trust(client, opts) {
328
357
  const syntheticHash = createHash("sha256").update(transferId).digest("hex");
329
358
  let auditHash = syntheticHash;
330
359
  try {
360
+ const auditEventData = {
361
+ model,
362
+ cost: streamCost,
363
+ settled,
364
+ transferId,
365
+ usageSource,
366
+ chunksDelivered: completion.chunksDelivered,
367
+ };
368
+ if (config.pii === "warn" || config.pii === "redact") {
369
+ const piiResult = redactPII(messages);
370
+ if (piiResult.detection.found) {
371
+ auditEventData.piiDetected = piiResult.detection.types;
372
+ auditEventData.piiPaths = piiResult.detection.paths;
373
+ }
374
+ }
331
375
  const auditEvent = await audit.appendEvent({
332
376
  kind: "llm_call",
333
377
  actor: "local",
334
- data: {
335
- model,
336
- cost: streamCost,
337
- settled,
338
- transferId,
339
- usageSource,
340
- chunksDelivered: completion.chunksDelivered,
341
- },
378
+ data: auditEventData,
342
379
  });
343
380
  auditHash = auditEvent.hash;
344
381
  }
@@ -349,7 +386,7 @@ export async function trust(client, opts) {
349
386
  transferId,
350
387
  cost: streamCost,
351
388
  budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
352
- auditHash,
389
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : auditHash,
353
390
  chainPath: join(VAULT_DIR, "audit"),
354
391
  receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
355
392
  settled,
@@ -364,9 +401,13 @@ export async function trust(client, opts) {
364
401
  };
365
402
  inFlightStreamCount--;
366
403
  return streamReceipt;
367
- }, (error, partial) => {
368
- // Release in-flight hold
369
- inFlightHoldTotal -= estimatedCost;
404
+ }, async (error, partial) => {
405
+ // Release in-flight hold under mutex
406
+ {
407
+ const releaseLock = await budgetMutex.acquire();
408
+ inFlightHoldTotal -= estimatedCost;
409
+ releaseLock();
410
+ }
370
411
  cb.recordFailure();
371
412
  // Best-effort audit of partial delivery
372
413
  audit
@@ -380,9 +421,12 @@ export async function trust(client, opts) {
380
421
  partialInputTokens: partial.usage.inputTokens,
381
422
  partialOutputTokens: partial.usage.outputTokens,
382
423
  usageReported: partial.usageReported,
383
- error: error instanceof Error
384
- ? error.message.slice(0, 200)
385
- : String(error).slice(0, 200),
424
+ error: (() => {
425
+ const raw = error instanceof Error ? error.message : String(error);
426
+ return config.pii === "warn" || config.pii === "redact"
427
+ ? redactPII(raw).data.slice(0, 200)
428
+ : raw.slice(0, 200);
429
+ })(),
386
430
  },
387
431
  })
388
432
  .catch(() => { });
@@ -398,12 +442,12 @@ export async function trust(client, opts) {
398
442
  // AUD-454: For streaming responses, settlement has NOT happened yet.
399
443
  // Set settled: false — the real settlement status will be on
400
444
  // governedStream.receipt after the stream is fully consumed.
401
- const auditHash = createHash("sha256").update(transferId).digest("hex");
445
+ const streamEstimateHash = createHash("sha256").update(transferId).digest("hex");
402
446
  const estimatedReceipt = {
403
447
  transferId,
404
448
  cost: estimatedCost,
405
449
  budgetRemaining: config.budget - budgetSpent - inFlightHoldTotal,
406
- auditHash,
450
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : streamEstimateHash,
407
451
  chainPath: join(VAULT_DIR, "audit"),
408
452
  receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
409
453
  settled: false, // AUD-454: not settled yet — stream hasn't been consumed
@@ -428,13 +472,16 @@ export async function trust(client, opts) {
428
472
  const outputTokens = usage.output_tokens ??
429
473
  usage.completion_tokens ??
430
474
  0;
431
- actualCost = estimateCost(model, inputTokens, outputTokens);
475
+ actualCost = estimateCost(model, inputTokens, outputTokens, customRates);
432
476
  }
433
477
  }
434
- // Release in-flight hold (non-streaming success)
435
- inFlightHoldTotal -= estimatedCost;
436
- // Track budget (cumulative)
437
- budgetSpent += actualCost;
478
+ // Release in-flight hold and commit budget under mutex
479
+ {
480
+ const releaseLock = await budgetMutex.acquire();
481
+ inFlightHoldTotal -= estimatedCost;
482
+ budgetSpent += actualCost;
483
+ releaseLock();
484
+ }
438
485
  // AUD-457: Persist cumulative spend to disk
439
486
  await persistSpendLedger(vaultBase, budgetSpent);
440
487
  // g. Circuit breaker: record success
@@ -470,23 +517,47 @@ export async function trust(client, opts) {
470
517
  // AUD-460: Use the proxy's transferId for settlement
471
518
  await proxyConn.settle(proxyTransferId ?? transferId, actualCost);
472
519
  }
473
- catch {
520
+ catch (postErr) {
474
521
  settled = false;
522
+ await audit
523
+ .appendEvent({
524
+ kind: "settlement_ambiguous",
525
+ actor: "local",
526
+ data: {
527
+ model,
528
+ cost: actualCost,
529
+ transferId,
530
+ error: postErr instanceof Error
531
+ ? postErr.message.slice(0, 200)
532
+ : String(postErr).slice(0, 200),
533
+ },
534
+ })
535
+ .catch(() => {
536
+ callAuditDegraded = true;
537
+ });
475
538
  }
476
539
  }
477
540
  // h. Audit event — failure mode 15.3: audit write failure
478
541
  const syntheticHash = createHash("sha256").update(transferId).digest("hex");
479
542
  let auditHash = syntheticHash;
480
543
  try {
544
+ const auditData = {
545
+ model,
546
+ cost: actualCost,
547
+ settled,
548
+ transferId,
549
+ };
550
+ if (config.pii === "warn" || config.pii === "redact") {
551
+ const piiResult = redactPII(messages);
552
+ if (piiResult.detection.found) {
553
+ auditData.piiDetected = piiResult.detection.types;
554
+ auditData.piiPaths = piiResult.detection.paths;
555
+ }
556
+ }
481
557
  const auditEvent = await audit.appendEvent({
482
558
  kind: "llm_call",
483
559
  actor: "local",
484
- data: {
485
- model,
486
- cost: actualCost,
487
- settled,
488
- transferId,
489
- },
560
+ data: auditData,
490
561
  });
491
562
  auditHash = auditEvent.hash;
492
563
  }
@@ -519,7 +590,7 @@ export async function trust(client, opts) {
519
590
  transferId,
520
591
  cost: actualCost,
521
592
  budgetRemaining,
522
- auditHash,
593
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : auditHash,
523
594
  chainPath: join(VAULT_DIR, "audit"),
524
595
  receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
525
596
  settled,
@@ -533,8 +604,12 @@ export async function trust(client, opts) {
533
604
  return { response, receipt };
534
605
  }
535
606
  catch (err) {
536
- // Release in-flight hold (non-streaming failure)
537
- inFlightHoldTotal -= estimatedCost;
607
+ // Release in-flight hold under mutex (non-streaming failure)
608
+ {
609
+ const releaseLock = await budgetMutex.acquire();
610
+ inFlightHoldTotal -= estimatedCost;
611
+ releaseLock();
612
+ }
538
613
  // j. Circuit breaker: record failure
539
614
  cb.recordFailure();
540
615
  // j2. Failure mode 15.2: LLM fails — VOID the pending hold
@@ -561,7 +636,13 @@ export async function trust(client, opts) {
561
636
  .appendEvent({
562
637
  kind: "llm_call_failed",
563
638
  actor: "local",
564
- data: { model, error: String(err), transferId },
639
+ data: {
640
+ model,
641
+ error: config.pii === "warn" || config.pii === "redact"
642
+ ? redactPII(String(err)).data.slice(0, 200)
643
+ : String(err),
644
+ transferId,
645
+ },
565
646
  })
566
647
  .catch(() => {
567
648
  callAuditDegraded = true;
@@ -584,6 +665,287 @@ export async function trust(client, opts) {
584
665
  inFlightCount--;
585
666
  }
586
667
  }
668
+ // 7b. Action governance — simplified pipeline for non-LLM actions
669
+ async function governActionImpl(action, execute) {
670
+ if (destroyed) {
671
+ throw new Error("TrustedClient has been destroyed");
672
+ }
673
+ // AUD-466: Validate cost to prevent budget inflation via negative values
674
+ if (!Number.isFinite(action.cost) || action.cost < 0) {
675
+ throw new Error(`action.cost must be a non-negative finite number, got ${String(action.cost)}`);
676
+ }
677
+ inFlightCount++;
678
+ let callAuditDegraded = false;
679
+ try {
680
+ const actor = action.actor ?? "local";
681
+ const transferId = trustId("tx");
682
+ // a. Circuit breaker check (keyed by action kind)
683
+ const cb = breaker.get(action.kind);
684
+ cb.allowRequest();
685
+ // b. Acquire mutex for budget atomicity
686
+ const releaseBudgetLock = await budgetMutex.acquire();
687
+ let proxyTransferId;
688
+ try {
689
+ // c. Policy gate — action fields available in context
690
+ // AUD-467: Caller params spread FIRST so governance fields cannot be shadowed
691
+ const policyResult = evaluatePolicy(policyRules, {
692
+ ...(action.params ?? {}),
693
+ action_kind: action.kind,
694
+ action_name: action.name,
695
+ estimated_cost: action.cost,
696
+ budget_remaining: config.budget - budgetSpent - inFlightHoldTotal,
697
+ tier: config.tier,
698
+ });
699
+ if (policyResult.decision === "deny") {
700
+ const reason = policyResult.reasons.length > 0 ? policyResult.reasons.join("; ") : "Policy denied";
701
+ throw new PolicyDeniedError(reason);
702
+ }
703
+ // d. PII check on action params
704
+ if (config.pii !== "off" && action.params != null) {
705
+ const piiResult = detectPII(action.params);
706
+ if (piiResult.found && config.pii === "block") {
707
+ throw new PolicyDeniedError(`PII detected in action params: ${piiResult.types.join(", ")}`);
708
+ }
709
+ }
710
+ // d2. Injection detection on action params
711
+ if (config.injection !== "off" && action.params != null) {
712
+ const injectionResult = detectInjection(action.params);
713
+ if (injectionResult.detected) {
714
+ if (config.injection === "block") {
715
+ throw new PolicyDeniedError(`Injection detected in action params: ${injectionResult.patterns.join(", ")}`);
716
+ }
717
+ // warn: log to audit trail (non-fatal)
718
+ await audit
719
+ .appendEvent({
720
+ kind: "injection_detected",
721
+ actor: action.actor ?? "local",
722
+ data: {
723
+ patterns: injectionResult.patterns,
724
+ score: injectionResult.score,
725
+ actionName: action.name,
726
+ actionKind: action.kind,
727
+ },
728
+ })
729
+ .catch(() => { });
730
+ }
731
+ }
732
+ // e. PENDING hold
733
+ if (proxyConn != null && !isDryRun) {
734
+ try {
735
+ const proxyResult = await proxyConn.spend({
736
+ model: action.name,
737
+ estimatedCost: action.cost,
738
+ actor,
739
+ });
740
+ proxyTransferId = proxyResult.transferId;
741
+ }
742
+ catch (holdErr) {
743
+ throw new LedgerUnavailableError(holdErr instanceof Error ? holdErr.message : String(holdErr));
744
+ }
745
+ }
746
+ else if (engine != null && !isDryRun) {
747
+ try {
748
+ await engine.spendPending({
749
+ transferId,
750
+ amount: action.cost,
751
+ });
752
+ }
753
+ catch (holdErr) {
754
+ throw new LedgerUnavailableError(holdErr instanceof Error ? holdErr.message : String(holdErr));
755
+ }
756
+ }
757
+ inFlightHoldTotal += action.cost;
758
+ }
759
+ finally {
760
+ releaseBudgetLock();
761
+ }
762
+ // f. Execute the action
763
+ // Guard against double-decrement of inFlightHoldTotal (AUD-465)
764
+ let holdReleased = false;
765
+ async function releaseHoldAndCommit(cost) {
766
+ if (!holdReleased) {
767
+ holdReleased = true;
768
+ const releaseLock = await budgetMutex.acquire();
769
+ inFlightHoldTotal -= action.cost;
770
+ if (cost !== undefined) {
771
+ budgetSpent += cost;
772
+ }
773
+ releaseLock();
774
+ }
775
+ }
776
+ try {
777
+ const result = await execute();
778
+ // Release in-flight hold and commit budget under mutex
779
+ await releaseHoldAndCommit(action.cost);
780
+ await persistSpendLedger(vaultBase, budgetSpent);
781
+ // g. Circuit breaker: record success
782
+ cb.recordSuccess();
783
+ // h. POST settlement
784
+ let settled = true;
785
+ if (engine != null && !isDryRun) {
786
+ try {
787
+ await engine.postPendingSpend(transferId);
788
+ }
789
+ catch (postErr) {
790
+ settled = false;
791
+ await audit
792
+ .appendEvent({
793
+ kind: "settlement_ambiguous",
794
+ actor,
795
+ data: {
796
+ actionKind: action.kind,
797
+ actionName: action.name,
798
+ cost: action.cost,
799
+ transferId,
800
+ error: postErr instanceof Error
801
+ ? postErr.message.slice(0, 200)
802
+ : String(postErr).slice(0, 200),
803
+ },
804
+ })
805
+ .catch(() => {
806
+ callAuditDegraded = true;
807
+ });
808
+ }
809
+ }
810
+ if (proxyConn != null && !isDryRun) {
811
+ try {
812
+ await proxyConn.settle(proxyTransferId ?? transferId, action.cost);
813
+ }
814
+ catch (postErr) {
815
+ settled = false;
816
+ await audit
817
+ .appendEvent({
818
+ kind: "settlement_ambiguous",
819
+ actor,
820
+ data: {
821
+ actionKind: action.kind,
822
+ actionName: action.name,
823
+ cost: action.cost,
824
+ transferId,
825
+ error: postErr instanceof Error
826
+ ? postErr.message.slice(0, 200)
827
+ : String(postErr).slice(0, 200),
828
+ },
829
+ })
830
+ .catch(() => {
831
+ callAuditDegraded = true;
832
+ });
833
+ }
834
+ }
835
+ // i. Prepare params for audit — redact PII if configured
836
+ let auditParams;
837
+ if (action.params != null) {
838
+ if (config.pii === "warn" || config.pii === "redact") {
839
+ const redacted = redactPII(action.params);
840
+ auditParams = redacted.data;
841
+ }
842
+ else {
843
+ auditParams = action.params;
844
+ }
845
+ }
846
+ // i2. Audit event
847
+ const syntheticHash = createHash("sha256").update(transferId).digest("hex");
848
+ let auditHash = syntheticHash;
849
+ try {
850
+ const auditEvent = await audit.appendEvent({
851
+ kind: action.kind,
852
+ actor,
853
+ data: {
854
+ actionName: action.name,
855
+ cost: action.cost,
856
+ settled,
857
+ transferId,
858
+ ...(auditParams != null ? { params: auditParams } : {}),
859
+ },
860
+ });
861
+ auditHash = auditEvent.hash;
862
+ }
863
+ catch {
864
+ // Failure mode 15.3: Audit degraded — do not fail the response
865
+ callAuditDegraded = true;
866
+ process.stderr.write(`[usertrust] audit degraded: failed to write ${action.kind} event for ${transferId}\n`);
867
+ }
868
+ // j. Daily-rotated receipt
869
+ if (config.audit.rotation !== "none") {
870
+ writeReceipt(vaultPath, {
871
+ kind: action.kind,
872
+ subsystem: "trust",
873
+ actor,
874
+ data: {
875
+ actionName: action.name,
876
+ cost: action.cost,
877
+ settled,
878
+ transferId,
879
+ ...(auditParams != null ? { params: auditParams } : {}),
880
+ },
881
+ }, config.audit.indexLimit);
882
+ }
883
+ const budgetRemaining = config.budget - budgetSpent - inFlightHoldTotal;
884
+ const receipt = {
885
+ transferId,
886
+ cost: action.cost,
887
+ budgetRemaining,
888
+ auditHash: callAuditDegraded ? "AUDIT_DEGRADED" : auditHash,
889
+ chainPath: join(VAULT_DIR, "audit"),
890
+ receiptUrl: opts?.proxy != null ? `${VERIFY_URL_BASE}/${transferId}` : null,
891
+ settled,
892
+ model: action.name,
893
+ provider: action.kind,
894
+ timestamp: new Date().toISOString(),
895
+ actionKind: action.kind,
896
+ ...(callAuditDegraded ? { auditDegraded: true } : {}),
897
+ ...(proxyConn != null ? { proxyStub: true } : {}),
898
+ };
899
+ return { result, receipt };
900
+ }
901
+ catch (err) {
902
+ // Release in-flight hold (AUD-465: guard prevents double-decrement)
903
+ await releaseHoldAndCommit();
904
+ // Circuit breaker: record failure
905
+ cb.recordFailure();
906
+ // VOID the pending hold
907
+ if (engine != null && !isDryRun) {
908
+ try {
909
+ await engine.voidPendingSpend(transferId);
910
+ }
911
+ catch {
912
+ // Best-effort void
913
+ }
914
+ }
915
+ if (proxyConn != null && !isDryRun) {
916
+ try {
917
+ await proxyConn.void(proxyTransferId ?? transferId);
918
+ }
919
+ catch {
920
+ // Best-effort void
921
+ }
922
+ }
923
+ // Audit the failure
924
+ await audit
925
+ .appendEvent({
926
+ kind: `${action.kind}_failed`,
927
+ actor,
928
+ data: {
929
+ actionName: action.name,
930
+ error: (() => {
931
+ const raw = err instanceof Error ? err.message : String(err);
932
+ return config.pii === "warn" || config.pii === "redact"
933
+ ? redactPII(raw).data.slice(0, 200)
934
+ : raw.slice(0, 200);
935
+ })(),
936
+ transferId,
937
+ },
938
+ })
939
+ .catch(() => {
940
+ callAuditDegraded = true;
941
+ });
942
+ throw err;
943
+ }
944
+ }
945
+ finally {
946
+ inFlightCount--;
947
+ }
948
+ }
587
949
  // 8. Safety net: clean up on process exit if destroy() was never called
588
950
  let beforeExitHandler = null;
589
951
  // 9. Build Proxy based on client kind
@@ -623,13 +985,13 @@ export async function trust(client, opts) {
623
985
  }
624
986
  };
625
987
  if (kind === "anthropic") {
626
- return buildAnthropicProxy(client, interceptCall, destroyFn);
988
+ return buildAnthropicProxy(client, interceptCall, destroyFn, governActionImpl);
627
989
  }
628
990
  if (kind === "openai") {
629
- return buildOpenAIProxy(client, interceptCall, destroyFn);
991
+ return buildOpenAIProxy(client, interceptCall, destroyFn, governActionImpl);
630
992
  }
631
993
  // google
632
- return buildGoogleProxy(client, interceptCall, destroyFn);
994
+ return buildGoogleProxy(client, interceptCall, destroyFn, governActionImpl);
633
995
  }
634
996
  const governedClient = createClientProxy();
635
997
  beforeExitHandler = () => {
@@ -707,7 +1069,7 @@ async function createTBEngine(config) {
707
1069
  },
708
1070
  };
709
1071
  }
710
- function buildAnthropicProxy(client, intercept, destroy) {
1072
+ function buildAnthropicProxy(client, intercept, destroy, governAction) {
711
1073
  const original = client;
712
1074
  const messages = original.messages;
713
1075
  const originalCreate = messages.create;
@@ -727,12 +1089,14 @@ function buildAnthropicProxy(client, intercept, destroy) {
727
1089
  return messagesProxy;
728
1090
  if (prop === "destroy")
729
1091
  return destroy;
1092
+ if (prop === "governAction")
1093
+ return governAction;
730
1094
  return Reflect.get(target, prop, receiver);
731
1095
  },
732
1096
  });
733
1097
  return clientProxy;
734
1098
  }
735
- function buildOpenAIProxy(client, intercept, destroy) {
1099
+ function buildOpenAIProxy(client, intercept, destroy, governAction) {
736
1100
  const original = client;
737
1101
  const chat = original.chat;
738
1102
  const completions = chat.completions;
@@ -758,12 +1122,14 @@ function buildOpenAIProxy(client, intercept, destroy) {
758
1122
  return chatProxy;
759
1123
  if (prop === "destroy")
760
1124
  return destroy;
1125
+ if (prop === "governAction")
1126
+ return governAction;
761
1127
  return Reflect.get(target, prop, receiver);
762
1128
  },
763
1129
  });
764
1130
  return clientProxy;
765
1131
  }
766
- function buildGoogleProxy(client, intercept, destroy) {
1132
+ function buildGoogleProxy(client, intercept, destroy, governAction) {
767
1133
  const original = client;
768
1134
  const models = original.models;
769
1135
  const originalGenerate = models.generateContent;
@@ -781,6 +1147,8 @@ function buildGoogleProxy(client, intercept, destroy) {
781
1147
  return modelsProxy;
782
1148
  if (prop === "destroy")
783
1149
  return destroy;
1150
+ if (prop === "governAction")
1151
+ return governAction;
784
1152
  return Reflect.get(target, prop, receiver);
785
1153
  },
786
1154
  });