@vellumai/assistant 0.5.4 → 0.5.6

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 (151) hide show
  1. package/Dockerfile +17 -27
  2. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
  3. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
  4. package/package.json +1 -1
  5. package/src/__tests__/actor-token-service.test.ts +113 -0
  6. package/src/__tests__/config-schema.test.ts +2 -2
  7. package/src/__tests__/context-window-manager.test.ts +78 -0
  8. package/src/__tests__/conversation-title-service.test.ts +30 -1
  9. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  10. package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
  11. package/src/__tests__/memory-regressions.test.ts +8 -30
  12. package/src/__tests__/openai-whisper.test.ts +93 -0
  13. package/src/__tests__/require-fresh-approval.test.ts +4 -0
  14. package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
  15. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
  16. package/src/__tests__/tool-executor.test.ts +4 -0
  17. package/src/__tests__/volume-security-guard.test.ts +155 -0
  18. package/src/cli/commands/conversations.ts +0 -18
  19. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
  20. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
  21. package/src/config/env-registry.ts +9 -0
  22. package/src/config/env.ts +8 -2
  23. package/src/config/feature-flag-registry.json +8 -8
  24. package/src/config/schema.ts +0 -12
  25. package/src/config/schemas/memory.ts +0 -4
  26. package/src/config/schemas/platform.ts +1 -1
  27. package/src/config/schemas/security.ts +4 -0
  28. package/src/context/window-manager.ts +53 -2
  29. package/src/credential-execution/managed-catalog.ts +5 -15
  30. package/src/daemon/conversation-agent-loop.ts +0 -60
  31. package/src/daemon/conversation-memory.ts +0 -117
  32. package/src/daemon/conversation-runtime-assembly.ts +0 -2
  33. package/src/daemon/daemon-control.ts +7 -0
  34. package/src/daemon/handlers/conversations.ts +0 -11
  35. package/src/daemon/lifecycle.ts +10 -47
  36. package/src/daemon/providers-setup.ts +2 -1
  37. package/src/followups/followup-store.ts +5 -2
  38. package/src/hooks/manager.ts +7 -0
  39. package/src/instrument.ts +33 -1
  40. package/src/memory/conversation-crud.ts +0 -236
  41. package/src/memory/conversation-title-service.ts +26 -10
  42. package/src/memory/db-init.ts +5 -13
  43. package/src/memory/embedding-local.ts +11 -5
  44. package/src/memory/indexer.ts +15 -106
  45. package/src/memory/job-handlers/conversation-starters.ts +24 -36
  46. package/src/memory/job-handlers/embedding.ts +0 -79
  47. package/src/memory/job-utils.ts +1 -1
  48. package/src/memory/jobs-store.ts +0 -8
  49. package/src/memory/jobs-worker.ts +0 -20
  50. package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
  51. package/src/memory/migrations/index.ts +1 -3
  52. package/src/memory/qdrant-client.ts +4 -6
  53. package/src/memory/schema/conversations.ts +0 -3
  54. package/src/memory/schema/index.ts +0 -2
  55. package/src/messaging/draft-store.ts +2 -2
  56. package/src/messaging/provider.ts +9 -0
  57. package/src/messaging/providers/slack/adapter.ts +29 -2
  58. package/src/oauth/connection-resolver.test.ts +22 -18
  59. package/src/oauth/connection-resolver.ts +92 -7
  60. package/src/oauth/platform-connection.test.ts +78 -69
  61. package/src/oauth/platform-connection.ts +12 -19
  62. package/src/permissions/defaults.ts +3 -3
  63. package/src/permissions/trust-client.ts +332 -0
  64. package/src/permissions/trust-store-interface.ts +105 -0
  65. package/src/permissions/trust-store.ts +531 -39
  66. package/src/platform/client.test.ts +148 -0
  67. package/src/platform/client.ts +71 -0
  68. package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
  69. package/src/providers/speech-to-text/openai-whisper.ts +68 -0
  70. package/src/providers/speech-to-text/resolve.ts +9 -0
  71. package/src/providers/speech-to-text/types.ts +17 -0
  72. package/src/runtime/auth/route-policy.ts +14 -0
  73. package/src/runtime/auth/token-service.ts +133 -0
  74. package/src/runtime/http-server.ts +4 -2
  75. package/src/runtime/routes/conversation-management-routes.ts +0 -36
  76. package/src/runtime/routes/conversation-query-routes.ts +44 -2
  77. package/src/runtime/routes/conversation-routes.ts +2 -1
  78. package/src/runtime/routes/inbound-message-handler.ts +27 -3
  79. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
  80. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
  81. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
  82. package/src/runtime/routes/log-export-routes.ts +1 -0
  83. package/src/runtime/routes/memory-item-routes.test.ts +221 -3
  84. package/src/runtime/routes/memory-item-routes.ts +124 -2
  85. package/src/runtime/routes/secret-routes.ts +4 -1
  86. package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
  87. package/src/schedule/schedule-store.ts +0 -21
  88. package/src/security/ces-credential-client.ts +173 -0
  89. package/src/security/secure-keys.ts +65 -22
  90. package/src/signals/bash.ts +3 -0
  91. package/src/signals/cancel.ts +3 -0
  92. package/src/signals/confirm.ts +3 -0
  93. package/src/signals/conversation-undo.ts +3 -0
  94. package/src/signals/event-stream.ts +7 -0
  95. package/src/signals/shotgun.ts +3 -0
  96. package/src/signals/trust-rule.ts +3 -0
  97. package/src/skills/inline-command-render.ts +5 -1
  98. package/src/skills/inline-command-runner.ts +30 -2
  99. package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
  100. package/src/telemetry/usage-telemetry-reporter.ts +21 -19
  101. package/src/tools/memory/handlers.ts +1 -129
  102. package/src/tools/permission-checker.ts +18 -0
  103. package/src/tools/skills/load.ts +9 -2
  104. package/src/util/device-id.ts +70 -7
  105. package/src/util/logger.ts +35 -9
  106. package/src/util/platform.ts +29 -5
  107. package/src/util/xml.ts +8 -0
  108. package/src/workspace/heartbeat-service.ts +5 -24
  109. package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
  110. package/src/workspace/migrations/registry.ts +2 -0
  111. package/src/__tests__/archive-recall.test.ts +0 -560
  112. package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
  113. package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
  114. package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
  115. package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
  116. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
  117. package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
  118. package/src/__tests__/memory-brief-time.test.ts +0 -285
  119. package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
  120. package/src/__tests__/memory-chunk-archive.test.ts +0 -400
  121. package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
  122. package/src/__tests__/memory-episode-archive.test.ts +0 -370
  123. package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
  124. package/src/__tests__/memory-observation-archive.test.ts +0 -375
  125. package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
  126. package/src/__tests__/memory-reducer-job.test.ts +0 -538
  127. package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
  128. package/src/__tests__/memory-reducer-store.test.ts +0 -728
  129. package/src/__tests__/memory-reducer-types.test.ts +0 -707
  130. package/src/__tests__/memory-reducer.test.ts +0 -704
  131. package/src/__tests__/memory-simplified-config.test.ts +0 -281
  132. package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
  133. package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
  134. package/src/config/schemas/memory-simplified.ts +0 -101
  135. package/src/memory/archive-recall.ts +0 -516
  136. package/src/memory/archive-store.ts +0 -400
  137. package/src/memory/brief-formatting.ts +0 -33
  138. package/src/memory/brief-open-loops.ts +0 -266
  139. package/src/memory/brief-time.ts +0 -162
  140. package/src/memory/brief.ts +0 -75
  141. package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
  142. package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
  143. package/src/memory/migrations/185-memory-brief-state.ts +0 -52
  144. package/src/memory/migrations/186-memory-archive.ts +0 -109
  145. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
  146. package/src/memory/reducer-scheduler.ts +0 -242
  147. package/src/memory/reducer-store.ts +0 -271
  148. package/src/memory/reducer-types.ts +0 -106
  149. package/src/memory/reducer.ts +0 -467
  150. package/src/memory/schema/memory-archive.ts +0 -121
  151. package/src/memory/schema/memory-brief.ts +0 -55
@@ -11,11 +11,24 @@ import { dirname, join } from "node:path";
11
11
  import { Minimatch } from "minimatch";
12
12
  import { v4 as uuid } from "uuid";
13
13
 
14
+ import { getIsContainerized } from "../config/env-registry.js";
14
15
  import { getLogger } from "../util/logger.js";
15
16
  import { getRootDir } from "../util/platform.js";
16
17
  import { getDefaultRuleTemplates } from "./defaults.js";
18
+ import * as trustClient from "./trust-client.js";
19
+ import type {
20
+ AcceptStarterBundleResult,
21
+ StarterBundleRule,
22
+ TrustStoreBackend,
23
+ } from "./trust-store-interface.js";
17
24
  import type { PolicyContext, TrustRule } from "./types.js";
18
25
 
26
+ export type {
27
+ AcceptStarterBundleResult,
28
+ StarterBundleRule,
29
+ } from "./trust-store-interface.js";
30
+ export type { TrustStoreBackend } from "./trust-store-interface.js";
31
+
19
32
  const log = getLogger("trust-store");
20
33
 
21
34
  const TRUST_FILE_VERSION = 3;
@@ -34,8 +47,8 @@ let cachedStarterBundleAccepted: boolean | null = null;
34
47
  // Used by the permission checker to invalidate dependent caches.
35
48
  const rulesChangedListeners: Array<() => void> = [];
36
49
 
37
- /** Register a callback to be invoked whenever trust rules change. */
38
- export function onRulesChanged(listener: () => void): void {
50
+ /** Register a callback to be invoked whenever trust rules change (file backend). */
51
+ function fileOnRulesChanged(listener: () => void): void {
39
52
  rulesChangedListeners.push(listener);
40
53
  }
41
54
 
@@ -77,10 +90,10 @@ function getCompiledPattern(pattern: string): Minimatch | null {
77
90
  }
78
91
 
79
92
  /**
80
- * Check whether a minimatch pattern matches a candidate string.
93
+ * Check whether a minimatch pattern matches a candidate string (file backend).
81
94
  * Reuses the compiled pattern cache from trust rule evaluation.
82
95
  */
83
- export function patternMatchesCandidate(
96
+ function filePatternMatchesCandidate(
84
97
  pattern: string,
85
98
  candidate: string,
86
99
  ): boolean {
@@ -172,9 +185,10 @@ function backfillDefaults(rules: TrustRule[]): boolean {
172
185
  }
173
186
  }
174
187
 
175
- // Migrate existing default rules whose priority, pattern, decision, or
176
- // allowHighRisk has changed in the template (e.g. host_bash pattern changed
177
- // from '*' to '**', host tool priorities changed from 1000 to 50).
188
+ // Migrate existing default rules whose priority, pattern, scope, decision,
189
+ // or allowHighRisk has changed in the template (e.g. host_bash pattern
190
+ // changed from '*' to '**', host tool priorities changed from 1000 to 50,
191
+ // workspace scope changed from getRootDir()+workspace to getWorkspaceDir()).
178
192
  for (const template of getDefaultRuleTemplates()) {
179
193
  if (existingIds.has(template.id)) {
180
194
  const rule = rules.find((r) => r.id === template.id);
@@ -182,6 +196,7 @@ function backfillDefaults(rules: TrustRule[]): boolean {
182
196
  rule &&
183
197
  (rule.priority !== template.priority ||
184
198
  rule.pattern !== template.pattern ||
199
+ rule.scope !== template.scope ||
185
200
  rule.decision !== template.decision ||
186
201
  rule.allowHighRisk !== template.allowHighRisk)
187
202
  ) {
@@ -192,11 +207,14 @@ function backfillDefaults(rules: TrustRule[]): boolean {
192
207
  newPriority: template.priority,
193
208
  oldPattern: rule.pattern,
194
209
  newPattern: template.pattern,
210
+ oldScope: rule.scope,
211
+ newScope: template.scope,
195
212
  },
196
213
  "Migrated default rule to updated template values",
197
214
  );
198
215
  rule.priority = template.priority;
199
216
  rule.pattern = template.pattern;
217
+ rule.scope = template.scope;
200
218
  rule.decision = template.decision;
201
219
  if (template.allowHighRisk != null) {
202
220
  rule.allowHighRisk = template.allowHighRisk;
@@ -363,7 +381,7 @@ function getRules(): TrustRule[] {
363
381
  return cachedRules;
364
382
  }
365
383
 
366
- export function addRule(
384
+ function fileAddRule(
367
385
  tool: string,
368
386
  pattern: string,
369
387
  scope: string,
@@ -405,7 +423,7 @@ export function addRule(
405
423
  return rule;
406
424
  }
407
425
 
408
- export function updateRule(
426
+ function fileUpdateRule(
409
427
  id: string,
410
428
  updates: {
411
429
  tool?: string;
@@ -444,7 +462,7 @@ export function updateRule(
444
462
  return rule;
445
463
  }
446
464
 
447
- export function removeRule(id: string): boolean {
465
+ function fileRemoveRule(id: string): boolean {
448
466
  const defaultIds = new Set(getDefaultRuleTemplates().map((t) => t.id));
449
467
  if (defaultIds.has(id))
450
468
  throw new Error(`Cannot remove default trust rule: ${id}`);
@@ -502,14 +520,14 @@ function matchesExecutionTarget(rule: TrustRule, ctx?: PolicyContext): boolean {
502
520
  }
503
521
 
504
522
  /**
505
- * Find the highest-priority rule that matches any of the command candidates.
523
+ * Find the highest-priority rule that matches any of the command candidates (file backend).
506
524
  * Rules are pre-sorted by priority descending, so the first match wins.
507
525
  *
508
526
  * When a `PolicyContext` is provided, rules that specify executionTarget
509
527
  * constraints are filtered accordingly. Rules without those constraints
510
528
  * act as wildcards and match any context.
511
529
  */
512
- export function findHighestPriorityRule(
530
+ function fileFindHighestPriorityRule(
513
531
  tool: string,
514
532
  commands: string[],
515
533
  scope: string,
@@ -544,7 +562,7 @@ export function findHighestPriorityRule(
544
562
  return null;
545
563
  }
546
564
 
547
- export function findMatchingRule(
565
+ function fileFindMatchingRule(
548
566
  tool: string,
549
567
  command: string,
550
568
  scope: string,
@@ -552,7 +570,7 @@ export function findMatchingRule(
552
570
  return findRuleByDecision(tool, command, scope, "allow");
553
571
  }
554
572
 
555
- export function findDenyRule(
573
+ function fileFindDenyRule(
556
574
  tool: string,
557
575
  command: string,
558
576
  scope: string,
@@ -560,11 +578,11 @@ export function findDenyRule(
560
578
  return findRuleByDecision(tool, command, scope, "deny");
561
579
  }
562
580
 
563
- export function getAllRules(): TrustRule[] {
581
+ function fileGetAllRules(): TrustRule[] {
564
582
  return [...getRules()];
565
583
  }
566
584
 
567
- export function clearAllRules(): void {
585
+ function fileClearAllRules(): void {
568
586
  // Reset the starter bundle flag so the bundle can be re-accepted after clear.
569
587
  cachedStarterBundleAccepted = false;
570
588
  // Re-backfill default rules so protected directory stays guarded.
@@ -578,7 +596,7 @@ export function clearAllRules(): void {
578
596
  log.info("Cleared all user trust rules (default rules preserved)");
579
597
  }
580
598
 
581
- export function clearCache(): void {
599
+ function fileClearCache(): void {
582
600
  cachedRules = null;
583
601
  cachedStarterBundleAccepted = null;
584
602
  compiledPatterns.clear();
@@ -592,21 +610,12 @@ export function clearCache(): void {
592
610
  // once, reducing prompt noise in strict mode while keeping the action
593
611
  // explicitly opt-in.
594
612
 
595
- export interface StarterBundleRule {
596
- id: string;
597
- tool: string;
598
- pattern: string;
599
- scope: string;
600
- decision: "allow";
601
- priority: number;
602
- }
603
-
604
613
  /**
605
- * Returns the starter bundle rule definitions. These cover read-only and
614
+ * Returns the starter bundle rule definitions (file backend). These cover read-only and
606
615
  * information-gathering tools that never mutate the filesystem or execute
607
616
  * arbitrary code.
608
617
  */
609
- export function getStarterBundleRules(): StarterBundleRule[] {
618
+ function fileGetStarterBundleRules(): StarterBundleRule[] {
610
619
  return [
611
620
  // Use standalone "**" globstar — minimatch only treats ** as globstar when
612
621
  // it is its own path segment, so a "tool:**" prefix would collapse to
@@ -663,27 +672,21 @@ export function getStarterBundleRules(): StarterBundleRule[] {
663
672
  ];
664
673
  }
665
674
 
666
- /** Whether the user has previously accepted the starter bundle. */
667
- export function isStarterBundleAccepted(): boolean {
675
+ /** Whether the user has previously accepted the starter bundle (file backend). */
676
+ function fileIsStarterBundleAccepted(): boolean {
668
677
  // Ensure rules are loaded (which also loads the flag from disk)
669
678
  getRules();
670
679
  return cachedStarterBundleAccepted === true;
671
680
  }
672
681
 
673
- export interface AcceptStarterBundleResult {
674
- accepted: boolean;
675
- rulesAdded: number;
676
- alreadyAccepted: boolean;
677
- }
678
-
679
682
  /**
680
- * Seed the trust store with the starter bundle rules.
683
+ * Seed the trust store with the starter bundle rules (file backend).
681
684
  *
682
685
  * Idempotent: if the bundle was already accepted, no rules are added and
683
686
  * `alreadyAccepted` is returned as true. Rules whose IDs already exist
684
687
  * (e.g. from a previous partial acceptance) are skipped individually.
685
688
  */
686
- export function acceptStarterBundle(): AcceptStarterBundleResult {
689
+ function fileAcceptStarterBundle(): AcceptStarterBundleResult {
687
690
  // Re-read from disk to avoid lost updates.
688
691
  cachedRules = null;
689
692
  cachedStarterBundleAccepted = null;
@@ -696,7 +699,7 @@ export function acceptStarterBundle(): AcceptStarterBundleResult {
696
699
  const existingIds = new Set(rules.map((r) => r.id));
697
700
  let added = 0;
698
701
 
699
- for (const template of getStarterBundleRules()) {
702
+ for (const template of fileGetStarterBundleRules()) {
700
703
  if (existingIds.has(template.id)) continue;
701
704
  rules.push({
702
705
  id: template.id,
@@ -720,3 +723,492 @@ export function acceptStarterBundle(): AcceptStarterBundleResult {
720
723
 
721
724
  return { accepted: true, rulesAdded: added, alreadyAccepted: false };
722
725
  }
726
+
727
+ // ─── Backend interface ──────────────────────────────────────────────────────
728
+
729
+ /**
730
+ * File-based trust store backend. Wraps the module-level functions into a
731
+ * `TrustStoreBackend` so callers can program against the interface.
732
+ */
733
+ const fileTrustStoreBackend: TrustStoreBackend = {
734
+ getAllRules: fileGetAllRules,
735
+ findHighestPriorityRule: fileFindHighestPriorityRule,
736
+ findMatchingRule: fileFindMatchingRule,
737
+ findDenyRule: fileFindDenyRule,
738
+ addRule: fileAddRule,
739
+ updateRule: fileUpdateRule,
740
+ removeRule: fileRemoveRule,
741
+ clearAllRules: fileClearAllRules,
742
+ acceptStarterBundle: fileAcceptStarterBundle,
743
+ isStarterBundleAccepted: fileIsStarterBundleAccepted,
744
+ onRulesChanged: fileOnRulesChanged,
745
+ clearCache: fileClearCache,
746
+ patternMatchesCandidate: filePatternMatchesCandidate,
747
+ getStarterBundleRules: fileGetStarterBundleRules,
748
+ };
749
+
750
+ // ─── Gateway-backed trust store adapter ─────────────────────────────────────
751
+ //
752
+ // When the daemon runs in a container (IS_CONTAINERIZED=true), trust rules
753
+ // are stored in the gateway — not on the local filesystem. This adapter
754
+ // wraps the async gateway HTTP client into the synchronous TrustStoreBackend
755
+ // interface using an in-memory cache.
756
+ //
757
+ // Read operations serve from the cache. Write operations call the gateway
758
+ // synchronously (via curl), then update the cache from the response.
759
+ // A background timer refreshes the cache every CACHE_TTL_MS.
760
+
761
+ const CACHE_TTL_MS = 5_000;
762
+
763
+ /**
764
+ * Gateway-backed trust store that caches rules in memory and refreshes
765
+ * on a TTL. Satisfies the synchronous TrustStoreBackend interface by
766
+ * reading from cache and writing via synchronous HTTP calls.
767
+ */
768
+ class GatewayTrustStoreAdapter implements TrustStoreBackend {
769
+ private rules: TrustRule[] = [];
770
+ private starterBundleAccepted = false;
771
+ private initialized = false;
772
+ private refreshTimer: ReturnType<typeof setInterval> | null = null;
773
+ private readonly listeners: Array<() => void> = [];
774
+
775
+ /** Pattern cache — mirrors the file-based store's approach. */
776
+ private readonly gwCompiledPatterns = new Map<string, Minimatch>();
777
+ private readonly gwInvalidPatterns = new Set<string>();
778
+
779
+ // ── Initialization ──────────────────────────────────────────────────────
780
+
781
+ /**
782
+ * Ensure the cache is populated. Blocks synchronously on the first call
783
+ * by fetching rules from the gateway via the sync client. Subsequent
784
+ * calls are no-ops because the background refresh timer keeps the cache
785
+ * current.
786
+ */
787
+ private ensureInitialized(): void {
788
+ if (this.initialized) return;
789
+ try {
790
+ this.rules = trustClient.getAllRulesSync();
791
+ this.rules.sort(ruleOrder);
792
+ this.rebuildPatternCache();
793
+ // Infer starterBundleAccepted from the fetched rules — if any starter
794
+ // rule IDs are present, the bundle was accepted.
795
+ const starterIds = new Set(fileGetStarterBundleRules().map((r) => r.id));
796
+ this.starterBundleAccepted = this.rules.some((r) => starterIds.has(r.id));
797
+ } catch (err) {
798
+ log.error(
799
+ { err },
800
+ "Failed to load trust rules from gateway; using empty rule set",
801
+ );
802
+ this.rules = [];
803
+ }
804
+ this.initialized = true;
805
+ this.startRefreshTimer();
806
+ }
807
+
808
+ private startRefreshTimer(): void {
809
+ if (this.refreshTimer != null) return;
810
+ this.refreshTimer = setInterval(() => {
811
+ this.refreshCache();
812
+ }, CACHE_TTL_MS);
813
+ // Unref so the timer doesn't prevent the process from exiting.
814
+ if (
815
+ this.refreshTimer &&
816
+ typeof this.refreshTimer === "object" &&
817
+ "unref" in this.refreshTimer
818
+ ) {
819
+ (this.refreshTimer as NodeJS.Timeout).unref();
820
+ }
821
+ }
822
+
823
+ private refreshCache(): void {
824
+ try {
825
+ const fresh = trustClient.getAllRulesSync();
826
+ fresh.sort(ruleOrder);
827
+ const oldJson = JSON.stringify(this.rules);
828
+ this.rules = fresh;
829
+ this.rebuildPatternCache();
830
+ // Detect starter bundle acceptance
831
+ const starterIds = new Set(fileGetStarterBundleRules().map((r) => r.id));
832
+ this.starterBundleAccepted = this.rules.some((r) => starterIds.has(r.id));
833
+ if (JSON.stringify(fresh) !== oldJson) {
834
+ this.notifyListeners();
835
+ }
836
+ } catch (err) {
837
+ log.warn(
838
+ { err },
839
+ "Failed to refresh trust rules from gateway; using stale cache",
840
+ );
841
+ }
842
+ }
843
+
844
+ private rebuildPatternCache(): void {
845
+ this.gwCompiledPatterns.clear();
846
+ this.gwInvalidPatterns.clear();
847
+ for (const rule of this.rules) {
848
+ if (typeof rule.pattern !== "string") continue;
849
+ if (!this.gwCompiledPatterns.has(rule.pattern)) {
850
+ try {
851
+ this.gwCompiledPatterns.set(
852
+ rule.pattern,
853
+ new Minimatch(rule.pattern),
854
+ );
855
+ } catch {
856
+ // skip invalid patterns
857
+ }
858
+ }
859
+ }
860
+ }
861
+
862
+ private getCompiledPattern(pattern: string): Minimatch | null {
863
+ if (this.gwInvalidPatterns.has(pattern)) return null;
864
+ let compiled = this.gwCompiledPatterns.get(pattern);
865
+ if (!compiled) {
866
+ try {
867
+ compiled = new Minimatch(pattern);
868
+ this.gwCompiledPatterns.set(pattern, compiled);
869
+ } catch {
870
+ this.gwInvalidPatterns.add(pattern);
871
+ return null;
872
+ }
873
+ }
874
+ return compiled;
875
+ }
876
+
877
+ private notifyListeners(): void {
878
+ for (const listener of this.listeners) {
879
+ listener();
880
+ }
881
+ }
882
+
883
+ // ── TrustStoreBackend implementation ────────────────────────────────────
884
+
885
+ getAllRules(): TrustRule[] {
886
+ this.ensureInitialized();
887
+ return [...this.rules];
888
+ }
889
+
890
+ findHighestPriorityRule(
891
+ tool: string,
892
+ commands: string[],
893
+ scope: string,
894
+ ctx?: PolicyContext,
895
+ ): TrustRule | null {
896
+ this.ensureInitialized();
897
+ const ephemeral = ctx?.ephemeralRules ?? [];
898
+ const allRules =
899
+ ephemeral.length > 0
900
+ ? [...ephemeral, ...this.rules].sort(ruleOrder)
901
+ : this.rules;
902
+
903
+ for (const rule of allRules) {
904
+ if (rule.tool !== tool) continue;
905
+ if (!matchesScope(rule.scope, scope)) continue;
906
+ if (!matchesExecutionTarget(rule, ctx)) continue;
907
+ const compiled = this.getCompiledPattern(rule.pattern);
908
+ if (!compiled) continue;
909
+ for (const command of commands) {
910
+ if (compiled.match(command)) {
911
+ return rule;
912
+ }
913
+ }
914
+ }
915
+ return null;
916
+ }
917
+
918
+ findMatchingRule(
919
+ tool: string,
920
+ command: string,
921
+ scope: string,
922
+ ): TrustRule | null {
923
+ this.ensureInitialized();
924
+ for (const rule of this.rules) {
925
+ if (rule.tool !== tool) continue;
926
+ if (rule.decision !== "allow") continue;
927
+ const compiled = this.getCompiledPattern(rule.pattern);
928
+ if (!compiled || !compiled.match(command)) continue;
929
+ if (!matchesScope(rule.scope, scope)) continue;
930
+ return rule;
931
+ }
932
+ return null;
933
+ }
934
+
935
+ findDenyRule(tool: string, command: string, scope: string): TrustRule | null {
936
+ this.ensureInitialized();
937
+ for (const rule of this.rules) {
938
+ if (rule.tool !== tool) continue;
939
+ if (rule.decision !== "deny") continue;
940
+ const compiled = this.getCompiledPattern(rule.pattern);
941
+ if (!compiled || !compiled.match(command)) continue;
942
+ if (!matchesScope(rule.scope, scope)) continue;
943
+ return rule;
944
+ }
945
+ return null;
946
+ }
947
+
948
+ addRule(
949
+ tool: string,
950
+ pattern: string,
951
+ scope: string,
952
+ decision: "allow" | "deny" | "ask" = "allow",
953
+ priority: number = 100,
954
+ options?: {
955
+ allowHighRisk?: boolean;
956
+ executionTarget?: string;
957
+ },
958
+ ): TrustRule {
959
+ if (tool.startsWith("__internal:"))
960
+ throw new Error(
961
+ `Cannot create internal pseudo-rule via addRule: ${tool}`,
962
+ );
963
+ this.ensureInitialized();
964
+ const rule = trustClient.addRuleSync({
965
+ tool,
966
+ pattern,
967
+ scope,
968
+ decision,
969
+ priority,
970
+ allowHighRisk: options?.allowHighRisk,
971
+ executionTarget: options?.executionTarget,
972
+ });
973
+ // Update local cache
974
+ this.rules = [...this.rules, rule].sort(ruleOrder);
975
+ this.rebuildPatternCache();
976
+ this.notifyListeners();
977
+ log.info({ rule }, "Added trust rule via gateway");
978
+ return rule;
979
+ }
980
+
981
+ updateRule(
982
+ id: string,
983
+ updates: {
984
+ tool?: string;
985
+ pattern?: string;
986
+ scope?: string;
987
+ decision?: "allow" | "deny" | "ask";
988
+ priority?: number;
989
+ },
990
+ ): TrustRule {
991
+ if (updates.tool?.startsWith("__internal:"))
992
+ throw new Error(
993
+ `Cannot update tool to internal pseudo-rule: ${updates.tool}`,
994
+ );
995
+ this.ensureInitialized();
996
+ const rule = trustClient.updateRuleSync(id, updates);
997
+ // Update local cache
998
+ const idx = this.rules.findIndex((r) => r.id === id);
999
+ if (idx >= 0) {
1000
+ this.rules[idx] = rule;
1001
+ } else {
1002
+ this.rules.push(rule);
1003
+ }
1004
+ this.rules = [...this.rules].sort(ruleOrder);
1005
+ this.rebuildPatternCache();
1006
+ this.notifyListeners();
1007
+ log.info({ rule }, "Updated trust rule via gateway");
1008
+ return rule;
1009
+ }
1010
+
1011
+ removeRule(id: string): boolean {
1012
+ this.ensureInitialized();
1013
+ const success = trustClient.removeRuleSync(id);
1014
+ if (success) {
1015
+ this.rules = this.rules.filter((r) => r.id !== id);
1016
+ this.rebuildPatternCache();
1017
+ this.notifyListeners();
1018
+ log.info({ id }, "Removed trust rule via gateway");
1019
+ }
1020
+ return success;
1021
+ }
1022
+
1023
+ clearAllRules(): void {
1024
+ this.ensureInitialized();
1025
+ trustClient.clearRulesSync();
1026
+ this.starterBundleAccepted = false;
1027
+ // Re-fetch to get the default rules the gateway preserves
1028
+ try {
1029
+ this.rules = trustClient.getAllRulesSync();
1030
+ this.rules.sort(ruleOrder);
1031
+ } catch {
1032
+ this.rules = [];
1033
+ }
1034
+ this.rebuildPatternCache();
1035
+ this.notifyListeners();
1036
+ log.info("Cleared all user trust rules via gateway");
1037
+ }
1038
+
1039
+ acceptStarterBundle(): AcceptStarterBundleResult {
1040
+ this.ensureInitialized();
1041
+ const result = trustClient.acceptStarterBundleSync();
1042
+ this.starterBundleAccepted = true;
1043
+ // Refresh cache to include the newly added starter rules
1044
+ try {
1045
+ this.rules = trustClient.getAllRulesSync();
1046
+ this.rules.sort(ruleOrder);
1047
+ } catch {
1048
+ // Keep stale cache
1049
+ }
1050
+ this.rebuildPatternCache();
1051
+ this.notifyListeners();
1052
+ log.info(
1053
+ { rulesAdded: result.rulesAdded },
1054
+ "Starter approval bundle accepted via gateway",
1055
+ );
1056
+ return { ...result, alreadyAccepted: result.rulesAdded === 0 };
1057
+ }
1058
+
1059
+ isStarterBundleAccepted(): boolean {
1060
+ this.ensureInitialized();
1061
+ return this.starterBundleAccepted;
1062
+ }
1063
+
1064
+ onRulesChanged(listener: () => void): void {
1065
+ this.listeners.push(listener);
1066
+ }
1067
+
1068
+ clearCache(): void {
1069
+ this.initialized = false;
1070
+ this.rules = [];
1071
+ this.starterBundleAccepted = false;
1072
+ this.gwCompiledPatterns.clear();
1073
+ this.gwInvalidPatterns.clear();
1074
+ if (this.refreshTimer != null) {
1075
+ clearInterval(this.refreshTimer);
1076
+ this.refreshTimer = null;
1077
+ }
1078
+ }
1079
+
1080
+ patternMatchesCandidate(pattern: string, candidate: string): boolean {
1081
+ const compiled = this.getCompiledPattern(pattern);
1082
+ if (!compiled) return false;
1083
+ return compiled.match(candidate);
1084
+ }
1085
+
1086
+ getStarterBundleRules(): StarterBundleRule[] {
1087
+ // Starter bundle definitions are static — same regardless of backend.
1088
+ return fileGetStarterBundleRules();
1089
+ }
1090
+ }
1091
+
1092
+ /** Singleton gateway adapter instance (lazily created). */
1093
+ let gatewayTrustStoreBackend: GatewayTrustStoreAdapter | null = null;
1094
+
1095
+ function getGatewayTrustStore(): GatewayTrustStoreAdapter {
1096
+ if (!gatewayTrustStoreBackend) {
1097
+ gatewayTrustStoreBackend = new GatewayTrustStoreAdapter();
1098
+ }
1099
+ return gatewayTrustStoreBackend;
1100
+ }
1101
+
1102
+ /**
1103
+ * Returns the active trust store backend.
1104
+ *
1105
+ * When `IS_CONTAINERIZED=true`, returns a gateway-backed adapter that
1106
+ * proxies all trust operations through the gateway HTTP API. The daemon
1107
+ * never reads or writes `protected/trust.json` directly in Docker.
1108
+ *
1109
+ * When `IS_CONTAINERIZED=false`, returns the file-based implementation
1110
+ * (no change from previous behavior).
1111
+ */
1112
+ export function getTrustStore(): TrustStoreBackend {
1113
+ if (getIsContainerized()) {
1114
+ return getGatewayTrustStore();
1115
+ }
1116
+ return fileTrustStoreBackend;
1117
+ }
1118
+
1119
+ // ─── Module-level exports that delegate through getTrustStore() ─────────────
1120
+ //
1121
+ // All existing callers import these functions directly. By delegating through
1122
+ // getTrustStore(), they automatically get the right backend (file-based or
1123
+ // gateway-backed) without changing their imports.
1124
+
1125
+ export function addRule(
1126
+ tool: string,
1127
+ pattern: string,
1128
+ scope: string,
1129
+ decision: "allow" | "deny" | "ask" = "allow",
1130
+ priority: number = 100,
1131
+ options?: {
1132
+ allowHighRisk?: boolean;
1133
+ executionTarget?: string;
1134
+ },
1135
+ ): TrustRule {
1136
+ return getTrustStore().addRule(tool, pattern, scope, decision, priority, options);
1137
+ }
1138
+
1139
+ export function updateRule(
1140
+ id: string,
1141
+ updates: {
1142
+ tool?: string;
1143
+ pattern?: string;
1144
+ scope?: string;
1145
+ decision?: "allow" | "deny" | "ask";
1146
+ priority?: number;
1147
+ },
1148
+ ): TrustRule {
1149
+ return getTrustStore().updateRule(id, updates);
1150
+ }
1151
+
1152
+ export function removeRule(id: string): boolean {
1153
+ return getTrustStore().removeRule(id);
1154
+ }
1155
+
1156
+ export function clearAllRules(): void {
1157
+ getTrustStore().clearAllRules();
1158
+ }
1159
+
1160
+ export function getAllRules(): TrustRule[] {
1161
+ return getTrustStore().getAllRules();
1162
+ }
1163
+
1164
+ export function findHighestPriorityRule(
1165
+ tool: string,
1166
+ commands: string[],
1167
+ scope: string,
1168
+ ctx?: PolicyContext,
1169
+ ): TrustRule | null {
1170
+ return getTrustStore().findHighestPriorityRule(tool, commands, scope, ctx);
1171
+ }
1172
+
1173
+ export function findMatchingRule(
1174
+ tool: string,
1175
+ command: string,
1176
+ scope: string,
1177
+ ): TrustRule | null {
1178
+ return getTrustStore().findMatchingRule(tool, command, scope);
1179
+ }
1180
+
1181
+ export function findDenyRule(
1182
+ tool: string,
1183
+ command: string,
1184
+ scope: string,
1185
+ ): TrustRule | null {
1186
+ return getTrustStore().findDenyRule(tool, command, scope);
1187
+ }
1188
+
1189
+ export function acceptStarterBundle(): AcceptStarterBundleResult {
1190
+ return getTrustStore().acceptStarterBundle();
1191
+ }
1192
+
1193
+ export function isStarterBundleAccepted(): boolean {
1194
+ return getTrustStore().isStarterBundleAccepted();
1195
+ }
1196
+
1197
+ export function getStarterBundleRules(): StarterBundleRule[] {
1198
+ return getTrustStore().getStarterBundleRules();
1199
+ }
1200
+
1201
+ export function onRulesChanged(listener: () => void): void {
1202
+ getTrustStore().onRulesChanged(listener);
1203
+ }
1204
+
1205
+ export function clearCache(): void {
1206
+ getTrustStore().clearCache();
1207
+ }
1208
+
1209
+ export function patternMatchesCandidate(
1210
+ pattern: string,
1211
+ candidate: string,
1212
+ ): boolean {
1213
+ return getTrustStore().patternMatchesCandidate(pattern, candidate);
1214
+ }