@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.
- package/Dockerfile +17 -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__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- 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/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- 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/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- 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/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- 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/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- 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/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- 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
|
-
|
|
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 {
|
|
@@ -172,9 +185,10 @@ function backfillDefaults(rules: TrustRule[]): boolean {
|
|
|
172
185
|
}
|
|
173
186
|
}
|
|
174
187
|
|
|
175
|
-
// Migrate existing default rules whose priority, pattern, decision,
|
|
176
|
-
// allowHighRisk has changed in the template (e.g. host_bash pattern
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
581
|
+
function fileGetAllRules(): TrustRule[] {
|
|
564
582
|
return [...getRules()];
|
|
565
583
|
}
|
|
566
584
|
|
|
567
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|