@vellumai/assistant 0.5.3 → 0.5.5

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