@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.
Files changed (239) hide show
  1. package/ARCHITECTURE.md +24 -33
  2. package/README.md +3 -3
  3. package/docs/architecture/memory.md +180 -119
  4. package/package.json +2 -2
  5. package/src/__tests__/agent-loop.test.ts +3 -1
  6. package/src/__tests__/anthropic-provider.test.ts +114 -23
  7. package/src/__tests__/approval-cascade.test.ts +1 -15
  8. package/src/__tests__/approval-routes-http.test.ts +2 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  10. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  11. package/src/__tests__/checker.test.ts +13 -0
  12. package/src/__tests__/config-schema.test.ts +1 -68
  13. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  14. package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
  15. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  16. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  17. package/src/__tests__/credential-vault-unit.test.ts +4 -0
  18. package/src/__tests__/credential-vault.test.ts +13 -1
  19. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  20. package/src/__tests__/date-context.test.ts +93 -77
  21. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  22. package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
  23. package/src/__tests__/history-repair.test.ts +245 -0
  24. package/src/__tests__/host-cu-proxy.test.ts +165 -3
  25. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  26. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  27. package/src/__tests__/keychain-broker-client.test.ts +4 -4
  28. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  29. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  30. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  31. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  32. package/src/__tests__/memory-regressions.test.ts +477 -2841
  33. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  34. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  35. package/src/__tests__/mime-builder.test.ts +28 -0
  36. package/src/__tests__/native-web-search.test.ts +1 -0
  37. package/src/__tests__/oauth-cli.test.ts +572 -5
  38. package/src/__tests__/oauth-store.test.ts +120 -6
  39. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  40. package/src/__tests__/registry.test.ts +0 -1
  41. package/src/__tests__/relay-server.test.ts +46 -1
  42. package/src/__tests__/schedule-tools.test.ts +32 -0
  43. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  44. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  45. package/src/__tests__/secure-keys.test.ts +7 -2
  46. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  47. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  48. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  49. package/src/__tests__/session-agent-loop.test.ts +19 -15
  50. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  51. package/src/__tests__/session-error.test.ts +124 -2
  52. package/src/__tests__/session-history-web-search.test.ts +918 -0
  53. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  54. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  55. package/src/__tests__/session-queue.test.ts +37 -27
  56. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  57. package/src/__tests__/session-slash-known.test.ts +1 -15
  58. package/src/__tests__/session-slash-queue.test.ts +1 -15
  59. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  60. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  61. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  62. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  63. package/src/__tests__/skills-install-extract.test.ts +93 -0
  64. package/src/__tests__/skillssh-registry.test.ts +451 -0
  65. package/src/__tests__/trust-store.test.ts +15 -0
  66. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  67. package/src/agent/ax-tree-compaction.test.ts +51 -0
  68. package/src/agent/loop.ts +39 -12
  69. package/src/approvals/AGENTS.md +1 -1
  70. package/src/approvals/guardian-request-resolvers.ts +14 -2
  71. package/src/bundler/compiler-tools.ts +66 -2
  72. package/src/calls/call-domain.ts +132 -0
  73. package/src/calls/call-store.ts +6 -0
  74. package/src/calls/relay-server.ts +43 -5
  75. package/src/calls/relay-setup-router.ts +17 -1
  76. package/src/calls/twilio-config.ts +1 -1
  77. package/src/calls/types.ts +3 -1
  78. package/src/cli/commands/doctor.ts +4 -3
  79. package/src/cli/commands/mcp.ts +46 -59
  80. package/src/cli/commands/memory.ts +16 -165
  81. package/src/cli/commands/oauth/apps.ts +31 -2
  82. package/src/cli/commands/oauth/connections.ts +431 -97
  83. package/src/cli/commands/oauth/providers.ts +15 -1
  84. package/src/cli/commands/sessions.ts +5 -2
  85. package/src/cli/commands/skills.ts +173 -1
  86. package/src/cli/http-client.ts +0 -20
  87. package/src/cli/main-screen.tsx +2 -2
  88. package/src/cli/program.ts +5 -6
  89. package/src/cli.ts +4 -10
  90. package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
  91. package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
  92. package/src/config/bundled-tool-registry.ts +2 -5
  93. package/src/config/schema.ts +1 -12
  94. package/src/config/schemas/memory-lifecycle.ts +0 -9
  95. package/src/config/schemas/memory-processing.ts +0 -180
  96. package/src/config/schemas/memory-retrieval.ts +32 -104
  97. package/src/config/schemas/memory.ts +0 -10
  98. package/src/config/types.ts +0 -4
  99. package/src/context/window-manager.ts +4 -1
  100. package/src/daemon/config-watcher.ts +61 -3
  101. package/src/daemon/daemon-control.ts +1 -1
  102. package/src/daemon/date-context.ts +114 -31
  103. package/src/daemon/handlers/sessions.ts +18 -13
  104. package/src/daemon/handlers/skills.ts +20 -1
  105. package/src/daemon/history-repair.ts +72 -8
  106. package/src/daemon/host-cu-proxy.ts +55 -26
  107. package/src/daemon/lifecycle.ts +31 -3
  108. package/src/daemon/mcp-reload-service.ts +2 -2
  109. package/src/daemon/message-types/computer-use.ts +1 -12
  110. package/src/daemon/message-types/memory.ts +4 -16
  111. package/src/daemon/message-types/messages.ts +1 -0
  112. package/src/daemon/message-types/sessions.ts +4 -0
  113. package/src/daemon/server.ts +12 -1
  114. package/src/daemon/session-agent-loop-handlers.ts +38 -0
  115. package/src/daemon/session-agent-loop.ts +334 -48
  116. package/src/daemon/session-error.ts +89 -6
  117. package/src/daemon/session-history.ts +17 -7
  118. package/src/daemon/session-media-retry.ts +6 -2
  119. package/src/daemon/session-memory.ts +69 -149
  120. package/src/daemon/session-process.ts +10 -1
  121. package/src/daemon/session-runtime-assembly.ts +49 -19
  122. package/src/daemon/session-surfaces.ts +4 -1
  123. package/src/daemon/session-tool-setup.ts +7 -1
  124. package/src/daemon/session.ts +12 -2
  125. package/src/instrument.ts +61 -1
  126. package/src/memory/admin.ts +2 -191
  127. package/src/memory/canonical-guardian-store.ts +38 -2
  128. package/src/memory/conversation-crud.ts +0 -33
  129. package/src/memory/conversation-queries.ts +22 -3
  130. package/src/memory/db-init.ts +28 -0
  131. package/src/memory/embedding-backend.ts +84 -8
  132. package/src/memory/embedding-types.ts +9 -1
  133. package/src/memory/indexer.ts +7 -46
  134. package/src/memory/items-extractor.ts +274 -76
  135. package/src/memory/job-handlers/backfill.ts +2 -127
  136. package/src/memory/job-handlers/cleanup.ts +2 -16
  137. package/src/memory/job-handlers/extraction.ts +2 -138
  138. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  139. package/src/memory/job-handlers/summarization.ts +3 -148
  140. package/src/memory/job-utils.ts +21 -59
  141. package/src/memory/jobs-store.ts +1 -159
  142. package/src/memory/jobs-worker.ts +9 -52
  143. package/src/memory/migrations/104-core-indexes.ts +3 -3
  144. package/src/memory/migrations/149-oauth-tables.ts +2 -0
  145. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  146. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  147. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  148. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  149. package/src/memory/migrations/154-drop-fts.ts +20 -0
  150. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  151. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  152. package/src/memory/migrations/index.ts +7 -0
  153. package/src/memory/qdrant-client.ts +148 -51
  154. package/src/memory/raw-query.ts +1 -1
  155. package/src/memory/retriever.test.ts +294 -273
  156. package/src/memory/retriever.ts +421 -645
  157. package/src/memory/schema/calls.ts +2 -0
  158. package/src/memory/schema/memory-core.ts +3 -48
  159. package/src/memory/schema/oauth.ts +2 -0
  160. package/src/memory/search/formatting.ts +263 -176
  161. package/src/memory/search/lexical.ts +1 -254
  162. package/src/memory/search/ranking.ts +0 -455
  163. package/src/memory/search/semantic.ts +100 -14
  164. package/src/memory/search/staleness.ts +47 -0
  165. package/src/memory/search/tier-classifier.ts +21 -0
  166. package/src/memory/search/types.ts +15 -77
  167. package/src/memory/task-memory-cleanup.ts +4 -6
  168. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  169. package/src/oauth/byo-connection.test.ts +8 -1
  170. package/src/oauth/oauth-store.ts +113 -27
  171. package/src/oauth/seed-providers.ts +6 -0
  172. package/src/oauth/token-persistence.ts +11 -3
  173. package/src/permissions/defaults.ts +1 -0
  174. package/src/permissions/trust-store.ts +23 -1
  175. package/src/playbooks/playbook-compiler.ts +1 -1
  176. package/src/prompts/system-prompt.ts +18 -2
  177. package/src/providers/anthropic/client.ts +56 -126
  178. package/src/providers/types.ts +7 -1
  179. package/src/runtime/AGENTS.md +9 -0
  180. package/src/runtime/auth/route-policy.ts +6 -3
  181. package/src/runtime/guardian-reply-router.ts +24 -22
  182. package/src/runtime/http-server.ts +2 -2
  183. package/src/runtime/invite-redemption-service.ts +19 -1
  184. package/src/runtime/invite-service.ts +25 -0
  185. package/src/runtime/pending-interactions.ts +2 -2
  186. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  187. package/src/runtime/routes/conversation-routes.ts +9 -1
  188. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  189. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  190. package/src/runtime/routes/memory-item-routes.ts +503 -0
  191. package/src/runtime/routes/session-management-routes.ts +3 -3
  192. package/src/runtime/routes/settings-routes.ts +2 -2
  193. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  194. package/src/runtime/routes/workspace-routes.ts +2 -1
  195. package/src/security/keychain-broker-client.ts +17 -4
  196. package/src/security/secure-keys.ts +25 -3
  197. package/src/security/token-manager.ts +36 -36
  198. package/src/skills/catalog-install.ts +74 -18
  199. package/src/skills/skillssh-registry.ts +503 -0
  200. package/src/tools/assets/search.ts +5 -1
  201. package/src/tools/computer-use/definitions.ts +0 -10
  202. package/src/tools/computer-use/registry.ts +1 -1
  203. package/src/tools/credentials/vault.ts +1 -3
  204. package/src/tools/memory/definitions.ts +4 -13
  205. package/src/tools/memory/handlers.test.ts +83 -103
  206. package/src/tools/memory/handlers.ts +50 -85
  207. package/src/tools/schedule/create.ts +8 -1
  208. package/src/tools/schedule/update.ts +8 -1
  209. package/src/tools/skills/load.ts +25 -2
  210. package/src/__tests__/clarification-resolver.test.ts +0 -193
  211. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  212. package/src/__tests__/conflict-policy.test.ts +0 -269
  213. package/src/__tests__/conflict-store.test.ts +0 -372
  214. package/src/__tests__/contradiction-checker.test.ts +0 -361
  215. package/src/__tests__/entity-extractor.test.ts +0 -211
  216. package/src/__tests__/entity-search.test.ts +0 -1117
  217. package/src/__tests__/profile-compiler.test.ts +0 -392
  218. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  219. package/src/__tests__/session-profile-injection.test.ts +0 -557
  220. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  221. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  222. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  223. package/src/daemon/session-conflict-gate.ts +0 -167
  224. package/src/daemon/session-dynamic-profile.ts +0 -77
  225. package/src/memory/clarification-resolver.ts +0 -417
  226. package/src/memory/conflict-intent.ts +0 -205
  227. package/src/memory/conflict-policy.ts +0 -127
  228. package/src/memory/conflict-store.ts +0 -410
  229. package/src/memory/contradiction-checker.ts +0 -508
  230. package/src/memory/entity-extractor.ts +0 -535
  231. package/src/memory/format-recall.ts +0 -47
  232. package/src/memory/fts-reconciler.ts +0 -165
  233. package/src/memory/job-handlers/conflict.ts +0 -200
  234. package/src/memory/profile-compiler.ts +0 -195
  235. package/src/memory/recall-cache.ts +0 -117
  236. package/src/memory/search/entity.ts +0 -535
  237. package/src/memory/search/query-expansion.test.ts +0 -70
  238. package/src/memory/search/query-expansion.ts +0 -118
  239. 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
- | "lexical"
4
- | "semantic"
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 = "lexical" | "recency" | "direct_item" | "entity";
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` and `extract_entities` jobs whose messageId
100
- * belongs to the given conversation. This drains the queue so the worker never
101
- * processes them, complementing the runtime check in the extraction handler.
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', 'extract_entities')
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: ${to}`,
42
- `Subject: ${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 (cc) headers.push(`Cc: ${cc}`);
47
- if (bcc) headers.push(`Bcc: ${bcc}`);
48
- if (inReplyTo) {
49
- headers.push(`In-Reply-To: ${inReplyTo}`);
50
- headers.push(`References: ${inReplyTo}`);
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
- { id: string; providerKey: string; clientId: string }
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
@@ -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
- clientSecret?: string,
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 existing = db
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 (existing) {
197
- if (clientSecret) {
227
+ if (existingRow) {
228
+ if (clientSecretValue) {
198
229
  const stored = await setSecureKeyAsync(
199
- `oauth_app/${existing.id}/client_secret`,
200
- clientSecret,
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
- return existing;
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
- if (!deleted) return false;
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(providerKey?: string): OAuthConnectionRow[] {
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
- return db
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
- return db.select().from(oauthConnections).all();
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
- ? { id: params.oauthAppId }
105
- : await upsertApp(service, clientId, clientSecret);
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
- `oauth_app/${params.oauthAppId}/client_secret`,
118
+ app.clientSecretCredentialPath,
111
119
  clientSecret,
112
120
  );
113
121
  if (!stored) {
@@ -20,6 +20,7 @@ const HOST_FILE_TOOLS = [
20
20
  "host_file_edit",
21
21
  ] as const;
22
22
  const COMPUTER_USE_TOOLS = [
23
+ "computer_use_observe",
23
24
  "computer_use_click",
24
25
  "computer_use_type_text",
25
26
  "computer_use_key",
@@ -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 = rawRules;
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;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Compile all active playbook memory items into a triage context block
3
3
  * that can be injected into the system prompt alongside the contact
4
- * graph and dynamic profile.
4
+ * graph.
5
5
  */
6
6
 
7
7
  import { and, desc, eq, isNull } from "drizzle-orm";