@vellumai/assistant 0.5.4 → 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.
- package/Dockerfile +18 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/config-watcher.ts +4 -1
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/lifecycle.ts +7 -1
- package/src/daemon/providers-setup.ts +2 -1
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/trust-client.ts +343 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +523 -36
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
576
|
+
function fileGetAllRules(): TrustRule[] {
|
|
564
577
|
return [...getRules()];
|
|
565
578
|
}
|
|
566
579
|
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|