@vellumai/assistant 0.4.49 → 0.4.50
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/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/memory.md +180 -119
- package/package.json +2 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -0
- package/src/__tests__/credential-vault.test.ts +13 -1
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/invite-redemption-service.test.ts +65 -1
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +572 -5
- package/src/__tests__/oauth-store.test.ts +120 -6
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +46 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secure-keys.test.ts +7 -2
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/voice-invite-redemption.test.ts +32 -1
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +132 -0
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +43 -5
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/doctor.ts +4 -3
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +31 -2
- package/src/cli/commands/oauth/connections.ts +431 -97
- package/src/cli/commands/oauth/providers.ts +15 -1
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +4 -10
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +61 -3
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +31 -3
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -0
- package/src/daemon/server.ts +12 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/instrument.ts +61 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +22 -3
- package/src/memory/db-init.ts +28 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/index.ts +7 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/oauth/byo-connection.test.ts +8 -1
- package/src/oauth/oauth-store.ts +113 -27
- package/src/oauth/seed-providers.ts +6 -0
- package/src/oauth/token-persistence.ts +11 -3
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/invite-redemption-service.ts +19 -1
- package/src/runtime/invite-service.ts +25 -0
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/conversation-routes.ts +9 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +2 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +2 -1
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/secure-keys.ts +25 -3
- package/src/security/token-manager.ts +36 -36
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +1 -3
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/schedule/create.ts +8 -1
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
export type CandidateType = "segment" | "item" | "summary" | "media";
|
|
2
|
-
export type CandidateSource =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
| "recency"
|
|
6
|
-
| "entity_direct"
|
|
7
|
-
| "entity_relation"
|
|
8
|
-
| "item_direct";
|
|
2
|
+
export type CandidateSource = "semantic" | "recency";
|
|
3
|
+
|
|
4
|
+
export type StalenessLevel = "fresh" | "aging" | "stale" | "very_stale";
|
|
9
5
|
|
|
10
6
|
export interface Candidate {
|
|
11
7
|
key: string;
|
|
@@ -18,10 +14,11 @@ export interface Candidate {
|
|
|
18
14
|
confidence: number;
|
|
19
15
|
importance: number;
|
|
20
16
|
createdAt: number;
|
|
21
|
-
lexical: number;
|
|
22
17
|
semantic: number;
|
|
23
18
|
recency: number;
|
|
24
19
|
finalScore: number;
|
|
20
|
+
tier?: 1 | 2 | null;
|
|
21
|
+
staleness?: StalenessLevel;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export interface MemoryRecallCandiateDebug {
|
|
@@ -29,7 +26,6 @@ export interface MemoryRecallCandiateDebug {
|
|
|
29
26
|
type: CandidateType;
|
|
30
27
|
kind: string;
|
|
31
28
|
finalScore: number;
|
|
32
|
-
lexical: number;
|
|
33
29
|
semantic: number;
|
|
34
30
|
recency: number;
|
|
35
31
|
}
|
|
@@ -39,7 +35,7 @@ export type DegradationReason =
|
|
|
39
35
|
| "qdrant_unavailable"
|
|
40
36
|
| "embedding_generation_failed";
|
|
41
37
|
|
|
42
|
-
export type FallbackSource = "
|
|
38
|
+
export type FallbackSource = "recency";
|
|
43
39
|
|
|
44
40
|
export interface DegradationStatus {
|
|
45
41
|
semanticUnavailable: boolean;
|
|
@@ -54,22 +50,22 @@ export interface MemoryRecallResult {
|
|
|
54
50
|
reason?: string;
|
|
55
51
|
provider?: string;
|
|
56
52
|
model?: string;
|
|
57
|
-
lexicalHits: number;
|
|
58
53
|
semanticHits: number;
|
|
59
54
|
recencyHits: number;
|
|
60
|
-
entityHits: number;
|
|
61
|
-
relationSeedEntityCount: number;
|
|
62
|
-
relationTraversedEdgeCount: number;
|
|
63
|
-
relationNeighborEntityCount: number;
|
|
64
|
-
relationExpandedItemCount: number;
|
|
65
|
-
earlyTerminated: boolean;
|
|
66
55
|
mergedCount: number;
|
|
67
56
|
selectedCount: number;
|
|
68
|
-
rerankApplied: boolean;
|
|
69
57
|
injectedTokens: number;
|
|
70
58
|
injectedText: string;
|
|
71
59
|
latencyMs: number;
|
|
72
60
|
topCandidates: MemoryRecallCandiateDebug[];
|
|
61
|
+
/** Count of tier 1 candidates after demotion. */
|
|
62
|
+
tier1Count?: number;
|
|
63
|
+
/** Count of tier 2 candidates after demotion. */
|
|
64
|
+
tier2Count?: number;
|
|
65
|
+
/** Milliseconds spent in the hybrid search step. */
|
|
66
|
+
hybridSearchMs?: number;
|
|
67
|
+
/** Whether sparse vectors were used in the hybrid search. */
|
|
68
|
+
sparseVectorUsed?: boolean;
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
/**
|
|
@@ -99,67 +95,9 @@ export interface MemoryRecallOptions {
|
|
|
99
95
|
maxInjectTokensOverride?: number;
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
export interface CollectedCandidates {
|
|
103
|
-
lexical: Candidate[];
|
|
104
|
-
recency: Candidate[];
|
|
105
|
-
semantic: Candidate[];
|
|
106
|
-
entity: Candidate[];
|
|
107
|
-
relationSeedEntityCount: number;
|
|
108
|
-
relationTraversedEdgeCount: number;
|
|
109
|
-
relationNeighborEntityCount: number;
|
|
110
|
-
relationExpandedItemCount: number;
|
|
111
|
-
earlyTerminated: boolean;
|
|
112
|
-
/** True when semantic search was attempted but threw an error. */
|
|
113
|
-
semanticSearchFailed: boolean;
|
|
114
|
-
/** True when semantic search was known to be unavailable before retrieval (no vector or breaker open). */
|
|
115
|
-
semanticUnavailable: boolean;
|
|
116
|
-
/** The error that caused semantic search to fail, if any. */
|
|
117
|
-
semanticSearchError?: unknown;
|
|
118
|
-
merged: Candidate[];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface EntitySearchResult {
|
|
122
|
-
candidates: Candidate[];
|
|
123
|
-
relationSeedEntityCount: number;
|
|
124
|
-
relationTraversedEdgeCount: number;
|
|
125
|
-
relationNeighborEntityCount: number;
|
|
126
|
-
relationExpandedItemCount: number;
|
|
127
|
-
candidateDepths?: Map<string, number>; // candidate key → BFS hop depth (1-based)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export interface MatchedEntityRow {
|
|
131
|
-
id: string;
|
|
132
|
-
name: string;
|
|
133
|
-
type: string;
|
|
134
|
-
aliases: string | null;
|
|
135
|
-
mention_count: number;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
98
|
export interface ItemMetadata {
|
|
139
99
|
accessCount: number;
|
|
140
100
|
lastUsedAt: number | null;
|
|
141
101
|
verificationState: string;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
import type { EntityRelationType, EntityType } from "../entity-extractor.js";
|
|
145
|
-
|
|
146
|
-
export interface TraversalOptions {
|
|
147
|
-
maxEdges: number;
|
|
148
|
-
maxNeighborEntities: number;
|
|
149
|
-
maxDepth?: number; // default 3
|
|
150
|
-
relationTypes?: EntityRelationType[];
|
|
151
|
-
entityTypes?: EntityType[];
|
|
152
|
-
/** When true, only follow source→target edges (frontier must be on source side). */
|
|
153
|
-
directed?: boolean;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface TraversalResult {
|
|
157
|
-
neighborEntityIds: string[];
|
|
158
|
-
traversedEdgeCount: number;
|
|
159
|
-
neighborDepths: Map<string, number>; // entityId → depth (1-based)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface TraversalStep {
|
|
163
|
-
relationTypes?: EntityRelationType[];
|
|
164
|
-
entityTypes?: EntityType[];
|
|
102
|
+
sourceConversationCount?: number;
|
|
165
103
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { getLogger } from "../util/logger.js";
|
|
2
2
|
import { rawGet, rawRun } from "./raw-query.js";
|
|
3
|
-
import { bumpMemoryVersion } from "./recall-cache.js";
|
|
4
3
|
|
|
5
4
|
const log = getLogger("task-memory-cleanup");
|
|
6
5
|
|
|
@@ -85,7 +84,6 @@ export function invalidateAssistantInferredItemsForConversation(
|
|
|
85
84
|
);
|
|
86
85
|
|
|
87
86
|
if (affected > 0) {
|
|
88
|
-
bumpMemoryVersion();
|
|
89
87
|
log.info(
|
|
90
88
|
{ conversationId, affected },
|
|
91
89
|
"Invalidated assistant-inferred memory items after task failure",
|
|
@@ -96,9 +94,9 @@ export function invalidateAssistantInferredItemsForConversation(
|
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
/**
|
|
99
|
-
* Cancel pending `extract_items`
|
|
100
|
-
*
|
|
101
|
-
*
|
|
97
|
+
* Cancel pending `extract_items` jobs whose messageId belongs to the given
|
|
98
|
+
* conversation. This drains the queue so the worker never processes them,
|
|
99
|
+
* complementing the runtime check in the extraction handler.
|
|
102
100
|
*/
|
|
103
101
|
function cancelPendingExtractionJobsForConversation(
|
|
104
102
|
conversationId: string,
|
|
@@ -109,7 +107,7 @@ function cancelPendingExtractionJobsForConversation(
|
|
|
109
107
|
SET status = 'failed',
|
|
110
108
|
last_error = 'conversation_failed',
|
|
111
109
|
updated_at = ?
|
|
112
|
-
WHERE type IN ('extract_items'
|
|
110
|
+
WHERE type IN ('extract_items')
|
|
113
111
|
AND status IN ('pending', 'running')
|
|
114
112
|
AND json_extract(payload, '$.messageId') IN (
|
|
115
113
|
SELECT id FROM messages WHERE conversation_id = ?
|
|
@@ -21,6 +21,10 @@ export interface MimeMessageOptions {
|
|
|
21
21
|
attachments: MimeAttachment[];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function sanitizeHeaderValue(value: string): string {
|
|
25
|
+
return value.replace(/[\r\n]+/g, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
function toBase64Url(input: Buffer): string {
|
|
25
29
|
return input
|
|
26
30
|
.toString("base64")
|
|
@@ -37,17 +41,23 @@ export function buildMultipartMime(options: MimeMessageOptions): string {
|
|
|
37
41
|
const { to, subject, body, inReplyTo, cc, bcc, attachments } = options;
|
|
38
42
|
const boundary = `----=_Part_${randomBytes(16).toString("hex")}`;
|
|
39
43
|
|
|
44
|
+
const sanitizedTo = sanitizeHeaderValue(to);
|
|
45
|
+
const sanitizedSubject = sanitizeHeaderValue(subject);
|
|
46
|
+
const sanitizedCc = cc ? sanitizeHeaderValue(cc) : undefined;
|
|
47
|
+
const sanitizedBcc = bcc ? sanitizeHeaderValue(bcc) : undefined;
|
|
48
|
+
const sanitizedInReplyTo = inReplyTo ? sanitizeHeaderValue(inReplyTo) : undefined;
|
|
49
|
+
|
|
40
50
|
const headers = [
|
|
41
|
-
`To: ${
|
|
42
|
-
`Subject: ${
|
|
51
|
+
`To: ${sanitizedTo}`,
|
|
52
|
+
`Subject: ${sanitizedSubject}`,
|
|
43
53
|
"MIME-Version: 1.0",
|
|
44
54
|
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
|
45
55
|
];
|
|
46
|
-
if (
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
49
|
-
headers.push(`In-Reply-To: ${
|
|
50
|
-
headers.push(`References: ${
|
|
56
|
+
if (sanitizedCc) headers.push(`Cc: ${sanitizedCc}`);
|
|
57
|
+
if (sanitizedBcc) headers.push(`Bcc: ${sanitizedBcc}`);
|
|
58
|
+
if (sanitizedInReplyTo) {
|
|
59
|
+
headers.push(`In-Reply-To: ${sanitizedInReplyTo}`);
|
|
60
|
+
headers.push(`References: ${sanitizedInReplyTo}`);
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
const parts: string[] = [];
|
|
@@ -81,7 +81,12 @@ const mockConnections = new Map<
|
|
|
81
81
|
>();
|
|
82
82
|
const mockApps = new Map<
|
|
83
83
|
string,
|
|
84
|
-
{
|
|
84
|
+
{
|
|
85
|
+
id: string;
|
|
86
|
+
providerKey: string;
|
|
87
|
+
clientId: string;
|
|
88
|
+
clientSecretCredentialPath: string;
|
|
89
|
+
}
|
|
85
90
|
>();
|
|
86
91
|
const mockProviders = new Map<
|
|
87
92
|
string,
|
|
@@ -192,6 +197,7 @@ function setupCredential(
|
|
|
192
197
|
id: appId,
|
|
193
198
|
providerKey: service,
|
|
194
199
|
clientId: "test-client-id",
|
|
200
|
+
clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
|
|
195
201
|
});
|
|
196
202
|
mockConnections.set(service, {
|
|
197
203
|
id: connId,
|
|
@@ -514,6 +520,7 @@ describe("resolveOAuthConnection", () => {
|
|
|
514
520
|
id: appId,
|
|
515
521
|
providerKey: "github",
|
|
516
522
|
clientId: "test-client-id",
|
|
523
|
+
clientSecretCredentialPath: `oauth_app/${appId}/client_secret`,
|
|
517
524
|
});
|
|
518
525
|
|
|
519
526
|
// Connection uses the custom credential service as its providerKey
|
package/src/oauth/oauth-store.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
deleteSecureKeyAsync,
|
|
19
19
|
getSecureKey,
|
|
20
|
+
getSecureKeyAsync,
|
|
20
21
|
setSecureKeyAsync,
|
|
21
22
|
} from "../security/secure-keys.js";
|
|
22
23
|
import { getLogger } from "../util/logger.js";
|
|
@@ -47,6 +48,7 @@ export function seedProviders(
|
|
|
47
48
|
tokenUrl: string;
|
|
48
49
|
tokenEndpointAuthMethod?: string;
|
|
49
50
|
userinfoUrl?: string;
|
|
51
|
+
pingUrl?: string;
|
|
50
52
|
baseUrl?: string;
|
|
51
53
|
defaultScopes: string[];
|
|
52
54
|
scopePolicy: Record<string, unknown>;
|
|
@@ -62,6 +64,7 @@ export function seedProviders(
|
|
|
62
64
|
const tokenUrl = p.tokenUrl;
|
|
63
65
|
const tokenEndpointAuthMethod = p.tokenEndpointAuthMethod ?? null;
|
|
64
66
|
const userinfoUrl = p.userinfoUrl ?? null;
|
|
67
|
+
const pingUrl = p.pingUrl ?? null;
|
|
65
68
|
const baseUrl = p.baseUrl ?? null;
|
|
66
69
|
const defaultScopes = JSON.stringify(p.defaultScopes);
|
|
67
70
|
const scopePolicy = JSON.stringify(p.scopePolicy);
|
|
@@ -82,6 +85,7 @@ export function seedProviders(
|
|
|
82
85
|
extraParams,
|
|
83
86
|
callbackTransport,
|
|
84
87
|
loopbackPort,
|
|
88
|
+
pingUrl,
|
|
85
89
|
createdAt: now,
|
|
86
90
|
updatedAt: now,
|
|
87
91
|
})
|
|
@@ -98,6 +102,7 @@ export function seedProviders(
|
|
|
98
102
|
extraParams,
|
|
99
103
|
callbackTransport,
|
|
100
104
|
loopbackPort,
|
|
105
|
+
pingUrl,
|
|
101
106
|
updatedAt: now,
|
|
102
107
|
},
|
|
103
108
|
})
|
|
@@ -131,6 +136,7 @@ export function registerProvider(params: {
|
|
|
131
136
|
tokenUrl: string;
|
|
132
137
|
tokenEndpointAuthMethod?: string;
|
|
133
138
|
userinfoUrl?: string;
|
|
139
|
+
pingUrl?: string;
|
|
134
140
|
baseUrl?: string;
|
|
135
141
|
defaultScopes: string[];
|
|
136
142
|
scopePolicy: Record<string, unknown>;
|
|
@@ -158,6 +164,7 @@ export function registerProvider(params: {
|
|
|
158
164
|
extraParams: params.extraParams ? JSON.stringify(params.extraParams) : null,
|
|
159
165
|
callbackTransport: params.callbackTransport ?? null,
|
|
160
166
|
loopbackPort: params.loopbackPort ?? null,
|
|
167
|
+
pingUrl: params.pingUrl ?? null,
|
|
161
168
|
createdAt: now,
|
|
162
169
|
updatedAt: now,
|
|
163
170
|
};
|
|
@@ -178,11 +185,35 @@ export function registerProvider(params: {
|
|
|
178
185
|
export async function upsertApp(
|
|
179
186
|
providerKey: string,
|
|
180
187
|
clientId: string,
|
|
181
|
-
|
|
188
|
+
clientSecretOpts?: {
|
|
189
|
+
clientSecretValue?: string;
|
|
190
|
+
clientSecretCredentialPath?: string;
|
|
191
|
+
},
|
|
182
192
|
): Promise<OAuthAppRow> {
|
|
193
|
+
const { clientSecretValue, clientSecretCredentialPath } =
|
|
194
|
+
clientSecretOpts ?? {};
|
|
195
|
+
|
|
196
|
+
if (clientSecretValue && clientSecretCredentialPath) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
"Cannot provide both clientSecretValue and clientSecretCredentialPath",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const defaultCredPath = (appId: string) => `oauth_app/${appId}/client_secret`;
|
|
203
|
+
|
|
204
|
+
// Verify the credential path points to an existing secret.
|
|
205
|
+
if (clientSecretCredentialPath) {
|
|
206
|
+
const existing = await getSecureKeyAsync(clientSecretCredentialPath);
|
|
207
|
+
if (existing === undefined) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`No secret found at credential path: ${clientSecretCredentialPath}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
183
214
|
const db = getDb();
|
|
184
215
|
|
|
185
|
-
const
|
|
216
|
+
const existingRow = db
|
|
186
217
|
.select()
|
|
187
218
|
.from(oauthApps)
|
|
188
219
|
.where(
|
|
@@ -193,41 +224,55 @@ export async function upsertApp(
|
|
|
193
224
|
)
|
|
194
225
|
.get();
|
|
195
226
|
|
|
196
|
-
if (
|
|
197
|
-
if (
|
|
227
|
+
if (existingRow) {
|
|
228
|
+
if (clientSecretValue) {
|
|
198
229
|
const stored = await setSecureKeyAsync(
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
existingRow.clientSecretCredentialPath,
|
|
231
|
+
clientSecretValue,
|
|
201
232
|
);
|
|
202
233
|
if (!stored) {
|
|
203
234
|
throw new Error("Failed to store client_secret in secure storage");
|
|
204
235
|
}
|
|
205
236
|
}
|
|
206
|
-
|
|
237
|
+
if (clientSecretCredentialPath) {
|
|
238
|
+
db.update(oauthApps)
|
|
239
|
+
.set({
|
|
240
|
+
clientSecretCredentialPath,
|
|
241
|
+
updatedAt: Date.now(),
|
|
242
|
+
})
|
|
243
|
+
.where(eq(oauthApps.id, existingRow.id))
|
|
244
|
+
.run();
|
|
245
|
+
return db
|
|
246
|
+
.select()
|
|
247
|
+
.from(oauthApps)
|
|
248
|
+
.where(eq(oauthApps.id, existingRow.id))
|
|
249
|
+
.get()!;
|
|
250
|
+
}
|
|
251
|
+
return existingRow;
|
|
207
252
|
}
|
|
208
253
|
|
|
209
254
|
const now = Date.now();
|
|
210
255
|
const id = uuid();
|
|
256
|
+
const credPath = clientSecretCredentialPath ?? defaultCredPath(id);
|
|
257
|
+
|
|
258
|
+
if (clientSecretValue) {
|
|
259
|
+
const stored = await setSecureKeyAsync(credPath, clientSecretValue);
|
|
260
|
+
if (!stored) {
|
|
261
|
+
throw new Error("Failed to store client_secret in secure storage");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
211
265
|
const row = {
|
|
212
266
|
id,
|
|
213
267
|
providerKey,
|
|
214
268
|
clientId,
|
|
269
|
+
clientSecretCredentialPath: credPath,
|
|
215
270
|
createdAt: now,
|
|
216
271
|
updatedAt: now,
|
|
217
272
|
};
|
|
218
273
|
|
|
219
274
|
db.insert(oauthApps).values(row).run();
|
|
220
275
|
|
|
221
|
-
if (clientSecret) {
|
|
222
|
-
const stored = await setSecureKeyAsync(
|
|
223
|
-
`oauth_app/${id}/client_secret`,
|
|
224
|
-
clientSecret,
|
|
225
|
-
);
|
|
226
|
-
if (!stored) {
|
|
227
|
-
throw new Error("Failed to store client_secret in secure storage");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
276
|
return row;
|
|
232
277
|
}
|
|
233
278
|
|
|
@@ -280,15 +325,16 @@ export function listApps(): OAuthAppRow[] {
|
|
|
280
325
|
|
|
281
326
|
/** Delete an app by ID. Cleans up the client_secret from secure storage. Returns true if a row was deleted. */
|
|
282
327
|
export async function deleteApp(id: string): Promise<boolean> {
|
|
328
|
+
const db = getDb();
|
|
329
|
+
|
|
330
|
+
const app = db.select().from(oauthApps).where(eq(oauthApps.id, id)).get();
|
|
331
|
+
if (!app) return false;
|
|
332
|
+
|
|
283
333
|
// Delete the DB row first so that if it fails (e.g. FK constraint from
|
|
284
334
|
// existing connections), the secret in secure storage remains intact.
|
|
285
|
-
const db = getDb();
|
|
286
335
|
db.delete(oauthApps).where(eq(oauthApps.id, id)).run();
|
|
287
|
-
const deleted = rawChanges() > 0;
|
|
288
336
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const result = await deleteSecureKeyAsync(`oauth_app/${id}/client_secret`);
|
|
337
|
+
const result = await deleteSecureKeyAsync(app.clientSecretCredentialPath);
|
|
292
338
|
if (result === "error") {
|
|
293
339
|
throw new Error(
|
|
294
340
|
`Deleted app ${id} but failed to remove client_secret from secure storage`,
|
|
@@ -354,12 +400,33 @@ export function getConnection(id: string): OAuthConnectionRow | undefined {
|
|
|
354
400
|
|
|
355
401
|
/**
|
|
356
402
|
* Get the most recent active connection for a provider.
|
|
403
|
+
* When `clientId` is provided, only connections linked to the matching app are considered.
|
|
357
404
|
* Returns undefined if no active connection exists.
|
|
358
405
|
*/
|
|
359
406
|
export function getConnectionByProvider(
|
|
360
407
|
providerKey: string,
|
|
408
|
+
clientId?: string,
|
|
361
409
|
): OAuthConnectionRow | undefined {
|
|
362
410
|
const db = getDb();
|
|
411
|
+
|
|
412
|
+
if (clientId) {
|
|
413
|
+
const app = getAppByProviderAndClientId(providerKey, clientId);
|
|
414
|
+
if (!app) return undefined;
|
|
415
|
+
return db
|
|
416
|
+
.select()
|
|
417
|
+
.from(oauthConnections)
|
|
418
|
+
.where(
|
|
419
|
+
and(
|
|
420
|
+
eq(oauthConnections.providerKey, providerKey),
|
|
421
|
+
eq(oauthConnections.oauthAppId, app.id),
|
|
422
|
+
eq(oauthConnections.status, "active"),
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
.orderBy(desc(oauthConnections.createdAt), sql`rowid DESC`)
|
|
426
|
+
.limit(1)
|
|
427
|
+
.get();
|
|
428
|
+
}
|
|
429
|
+
|
|
363
430
|
return db
|
|
364
431
|
.select()
|
|
365
432
|
.from(oauthConnections)
|
|
@@ -429,19 +496,37 @@ export function updateConnection(
|
|
|
429
496
|
return rawChanges() > 0;
|
|
430
497
|
}
|
|
431
498
|
|
|
432
|
-
/** List connections, optionally filtered by provider key. */
|
|
433
|
-
export function listConnections(
|
|
499
|
+
/** List connections, optionally filtered by provider key and/or client ID. */
|
|
500
|
+
export function listConnections(
|
|
501
|
+
providerKey?: string,
|
|
502
|
+
clientId?: string,
|
|
503
|
+
): OAuthConnectionRow[] {
|
|
434
504
|
const db = getDb();
|
|
435
505
|
|
|
506
|
+
let rows: OAuthConnectionRow[];
|
|
436
507
|
if (providerKey) {
|
|
437
|
-
|
|
508
|
+
rows = db
|
|
438
509
|
.select()
|
|
439
510
|
.from(oauthConnections)
|
|
440
511
|
.where(eq(oauthConnections.providerKey, providerKey))
|
|
441
512
|
.all();
|
|
513
|
+
} else {
|
|
514
|
+
rows = db.select().from(oauthConnections).all();
|
|
442
515
|
}
|
|
443
516
|
|
|
444
|
-
|
|
517
|
+
if (clientId) {
|
|
518
|
+
const matchingAppIds = new Set(
|
|
519
|
+
db
|
|
520
|
+
.select({ id: oauthApps.id })
|
|
521
|
+
.from(oauthApps)
|
|
522
|
+
.where(eq(oauthApps.clientId, clientId))
|
|
523
|
+
.all()
|
|
524
|
+
.map((a) => a.id),
|
|
525
|
+
);
|
|
526
|
+
return rows.filter((r) => matchingAppIds.has(r.oauthAppId));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return rows;
|
|
445
530
|
}
|
|
446
531
|
|
|
447
532
|
/** Delete a connection by ID. Returns true if a row was deleted. */
|
|
@@ -466,8 +551,9 @@ export function deleteConnection(id: string): boolean {
|
|
|
466
551
|
*/
|
|
467
552
|
export async function disconnectOAuthProvider(
|
|
468
553
|
providerKey: string,
|
|
554
|
+
clientId?: string,
|
|
469
555
|
): Promise<"disconnected" | "not-found" | "error"> {
|
|
470
|
-
const conn = getConnectionByProvider(providerKey);
|
|
556
|
+
const conn = getConnectionByProvider(providerKey, clientId);
|
|
471
557
|
if (!conn) return "not-found";
|
|
472
558
|
|
|
473
559
|
const r1 = await deleteSecureKeyAsync(
|
|
@@ -17,6 +17,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
17
17
|
tokenUrl: string;
|
|
18
18
|
tokenEndpointAuthMethod?: string;
|
|
19
19
|
userinfoUrl?: string;
|
|
20
|
+
pingUrl?: string;
|
|
20
21
|
baseUrl?: string;
|
|
21
22
|
defaultScopes: string[];
|
|
22
23
|
scopePolicy: {
|
|
@@ -34,6 +35,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
34
35
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
35
36
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
36
37
|
userinfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
38
|
+
pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
|
|
37
39
|
baseUrl: "https://gmail.googleapis.com/gmail/v1/users/me",
|
|
38
40
|
defaultScopes: [
|
|
39
41
|
"https://www.googleapis.com/auth/gmail.readonly",
|
|
@@ -57,6 +59,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
57
59
|
providerKey: "integration:slack",
|
|
58
60
|
authUrl: "https://slack.com/oauth/v2/authorize",
|
|
59
61
|
tokenUrl: "https://slack.com/api/oauth.v2.access",
|
|
62
|
+
pingUrl: "https://slack.com/api/auth.test",
|
|
60
63
|
baseUrl: "https://slack.com/api",
|
|
61
64
|
defaultScopes: [
|
|
62
65
|
"channels:read",
|
|
@@ -90,6 +93,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
90
93
|
providerKey: "integration:notion",
|
|
91
94
|
authUrl: "https://api.notion.com/v1/oauth/authorize",
|
|
92
95
|
tokenUrl: "https://api.notion.com/v1/oauth/token",
|
|
96
|
+
pingUrl: "https://api.notion.com/v1/users/me",
|
|
93
97
|
baseUrl: "https://api.notion.com",
|
|
94
98
|
defaultScopes: [],
|
|
95
99
|
scopePolicy: {
|
|
@@ -105,6 +109,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
105
109
|
providerKey: "integration:twitter",
|
|
106
110
|
authUrl: "https://twitter.com/i/oauth2/authorize",
|
|
107
111
|
tokenUrl: "https://api.x.com/2/oauth2/token",
|
|
112
|
+
pingUrl: "https://api.x.com/2/users/me",
|
|
108
113
|
baseUrl: "https://api.x.com",
|
|
109
114
|
defaultScopes: [
|
|
110
115
|
"tweet.read",
|
|
@@ -128,6 +133,7 @@ const PROVIDER_SEED_DATA: Record<
|
|
|
128
133
|
providerKey: "slack_channel",
|
|
129
134
|
authUrl: "urn:manual-token",
|
|
130
135
|
tokenUrl: "urn:manual-token",
|
|
136
|
+
pingUrl: "https://slack.com/api/auth.test",
|
|
131
137
|
baseUrl: "https://slack.com/api",
|
|
132
138
|
defaultScopes: [],
|
|
133
139
|
scopePolicy: {
|
|
@@ -22,6 +22,7 @@ import type { CredentialInjectionTemplate } from "../tools/credentials/policy-ty
|
|
|
22
22
|
import { runPostConnectHook } from "../tools/credentials/post-connect-hooks.js";
|
|
23
23
|
import {
|
|
24
24
|
createConnection,
|
|
25
|
+
getApp,
|
|
25
26
|
getConnectionByProvider,
|
|
26
27
|
updateConnection,
|
|
27
28
|
upsertApp,
|
|
@@ -101,13 +102,20 @@ export async function storeOAuth2Tokens(
|
|
|
101
102
|
|
|
102
103
|
// 1. Upsert the oauth_app row (or use the pre-resolved ID).
|
|
103
104
|
const app = params.oauthAppId
|
|
104
|
-
?
|
|
105
|
-
|
|
105
|
+
? (getApp(params.oauthAppId) ?? {
|
|
106
|
+
id: params.oauthAppId,
|
|
107
|
+
clientSecretCredentialPath: `oauth_app/${params.oauthAppId}/client_secret`,
|
|
108
|
+
})
|
|
109
|
+
: await upsertApp(
|
|
110
|
+
service,
|
|
111
|
+
clientId,
|
|
112
|
+
clientSecret ? { clientSecretValue: clientSecret } : undefined,
|
|
113
|
+
);
|
|
106
114
|
|
|
107
115
|
// When oauthAppId is pre-resolved, still persist clientSecret if provided.
|
|
108
116
|
if (params.oauthAppId && clientSecret) {
|
|
109
117
|
const stored = await setSecureKeyAsync(
|
|
110
|
-
|
|
118
|
+
app.clientSecretCredentialPath,
|
|
111
119
|
clientSecret,
|
|
112
120
|
);
|
|
113
121
|
if (!stored) {
|
|
@@ -283,12 +283,28 @@ function loadFromDisk(): TrustRule[] {
|
|
|
283
283
|
// Restore persisted starter bundle flag
|
|
284
284
|
cachedStarterBundleAccepted = data.starterBundleAccepted === true;
|
|
285
285
|
|
|
286
|
+
// Defense-in-depth: strip any __internal: prefixed rules that may have
|
|
287
|
+
// been hand-edited into trust.json.
|
|
288
|
+
const sanitizedRules = rawRules.filter((r) => {
|
|
289
|
+
if (typeof r.tool === "string" && r.tool.startsWith("__internal:")) {
|
|
290
|
+
log.warn(
|
|
291
|
+
{ ruleId: r.id, tool: r.tool },
|
|
292
|
+
"Stripping __internal: rule from trust file on load",
|
|
293
|
+
);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
});
|
|
298
|
+
|
|
286
299
|
if (
|
|
287
300
|
data.version === TRUST_FILE_VERSION ||
|
|
288
301
|
data.version === 1 ||
|
|
289
302
|
data.version === 2
|
|
290
303
|
) {
|
|
291
|
-
rules =
|
|
304
|
+
rules = sanitizedRules;
|
|
305
|
+
if (sanitizedRules.length < rawRules.length) {
|
|
306
|
+
needsSave = true;
|
|
307
|
+
}
|
|
292
308
|
if (data.version !== TRUST_FILE_VERSION) {
|
|
293
309
|
needsSave = true;
|
|
294
310
|
log.info(
|
|
@@ -395,6 +411,8 @@ export function addRule(
|
|
|
395
411
|
executionTarget?: string;
|
|
396
412
|
},
|
|
397
413
|
): TrustRule {
|
|
414
|
+
if (tool.startsWith("__internal:"))
|
|
415
|
+
throw new Error(`Cannot create internal pseudo-rule via addRule: ${tool}`);
|
|
398
416
|
// Re-read from disk to avoid lost updates if another call modified rules
|
|
399
417
|
// between our last read and now (e.g. two rapid trust rule additions).
|
|
400
418
|
cachedRules = null;
|
|
@@ -437,6 +455,10 @@ export function updateRule(
|
|
|
437
455
|
const defaultIds = new Set(getDefaultRuleTemplates().map((t) => t.id));
|
|
438
456
|
if (defaultIds.has(id))
|
|
439
457
|
throw new Error(`Cannot modify default trust rule: ${id}`);
|
|
458
|
+
if (updates.tool?.startsWith("__internal:"))
|
|
459
|
+
throw new Error(
|
|
460
|
+
`Cannot update tool to internal pseudo-rule: ${updates.tool}`,
|
|
461
|
+
);
|
|
440
462
|
|
|
441
463
|
// Re-read from disk to avoid lost updates from concurrent modifications.
|
|
442
464
|
cachedRules = null;
|