@vellumai/assistant 0.4.48 → 0.4.49

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 (252) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/README.md +2 -23
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/runbook-trusted-contacts.md +3 -8
  6. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  7. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  8. package/package.json +1 -1
  9. package/src/__tests__/actor-token-service.test.ts +0 -1
  10. package/src/__tests__/anthropic-provider.test.ts +156 -0
  11. package/src/__tests__/approval-cascade.test.ts +810 -0
  12. package/src/__tests__/approval-primitive.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +2 -0
  14. package/src/__tests__/assistant-attachments.test.ts +12 -34
  15. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  16. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  17. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  18. package/src/__tests__/channel-guardian.test.ts +0 -2
  19. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  20. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  21. package/src/__tests__/checker.test.ts +9 -29
  22. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  23. package/src/__tests__/computer-use-tools.test.ts +2 -19
  24. package/src/__tests__/config-watcher.test.ts +0 -1
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  26. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  27. package/src/__tests__/context-token-estimator.test.ts +196 -13
  28. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  29. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  30. package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
  31. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  32. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  33. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  34. package/src/__tests__/credential-vault-unit.test.ts +280 -49
  35. package/src/__tests__/credential-vault.test.ts +138 -16
  36. package/src/__tests__/credentials-cli.test.ts +71 -0
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  38. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  39. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  40. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  41. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  42. package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
  43. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  44. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  45. package/src/__tests__/heartbeat-service.test.ts +0 -1
  46. package/src/__tests__/host-cu-proxy.test.ts +629 -0
  47. package/src/__tests__/host-shell-tool.test.ts +27 -15
  48. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  49. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  50. package/src/__tests__/integration-status.test.ts +32 -51
  51. package/src/__tests__/intent-routing.test.ts +0 -1
  52. package/src/__tests__/invite-routes-http.test.ts +10 -9
  53. package/src/__tests__/keychain-broker-client.test.ts +11 -43
  54. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  55. package/src/__tests__/oauth-cli.test.ts +373 -14
  56. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  57. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  58. package/src/__tests__/oauth-store.test.ts +756 -0
  59. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  60. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  61. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  62. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  63. package/src/__tests__/recording-handler.test.ts +3 -4
  64. package/src/__tests__/registry.test.ts +2 -2
  65. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  66. package/src/__tests__/schedule-store.test.ts +0 -1
  67. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  68. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  69. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  70. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  71. package/src/__tests__/send-endpoint-busy.test.ts +21 -6
  72. package/src/__tests__/sequence-store.test.ts +0 -1
  73. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  74. package/src/__tests__/skill-include-graph.test.ts +66 -0
  75. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  76. package/src/__tests__/skill-load-tool.test.ts +149 -1
  77. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  78. package/src/__tests__/skills-uninstall.test.ts +1 -1
  79. package/src/__tests__/skills.test.ts +3 -3
  80. package/src/__tests__/slack-channel-config.test.ts +67 -3
  81. package/src/__tests__/slack-share-routes.test.ts +17 -19
  82. package/src/__tests__/system-prompt.test.ts +0 -1
  83. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  84. package/src/__tests__/terminal-tools.test.ts +4 -3
  85. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  86. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  87. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  88. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  90. package/src/__tests__/tool-executor.test.ts +0 -1
  91. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  92. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  93. package/src/__tests__/trust-store.test.ts +1 -22
  94. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  95. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  96. package/src/__tests__/twilio-routes.test.ts +0 -16
  97. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  98. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  99. package/src/agent/ax-tree-compaction.test.ts +235 -0
  100. package/src/agent/loop.ts +76 -130
  101. package/src/calls/call-domain.ts +1 -6
  102. package/src/calls/relay-server.ts +9 -13
  103. package/src/calls/twilio-config.ts +2 -7
  104. package/src/calls/twilio-routes.ts +1 -2
  105. package/src/calls/voice-ingress-preflight.ts +1 -1
  106. package/src/cli/commands/browser-relay.ts +18 -12
  107. package/src/cli/commands/completions.ts +0 -3
  108. package/src/cli/commands/credentials.ts +101 -15
  109. package/src/cli/commands/oauth/apps.ts +255 -0
  110. package/src/cli/commands/oauth/connections.ts +299 -0
  111. package/src/cli/commands/oauth/index.ts +52 -0
  112. package/src/cli/commands/oauth/providers.ts +242 -0
  113. package/src/cli/commands/skills.ts +4 -338
  114. package/src/cli/program.ts +1 -5
  115. package/src/cli/reference.ts +1 -3
  116. package/src/config/assistant-feature-flags.ts +0 -3
  117. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  118. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  119. package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
  120. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  121. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  122. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  123. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  124. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  125. package/src/config/env-registry.ts +14 -83
  126. package/src/config/env.ts +11 -50
  127. package/src/config/feature-flag-registry.json +16 -16
  128. package/src/config/loader.ts +0 -6
  129. package/src/config/schema.ts +3 -1
  130. package/src/config/skills.ts +21 -2
  131. package/src/context/image-dimensions.ts +229 -0
  132. package/src/context/token-estimator.ts +75 -12
  133. package/src/context/window-manager.ts +49 -10
  134. package/src/daemon/assistant-attachments.ts +1 -13
  135. package/src/daemon/handlers/config-ingress.ts +8 -33
  136. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  137. package/src/daemon/handlers/config-telegram.ts +32 -16
  138. package/src/daemon/handlers/sessions.ts +10 -24
  139. package/src/daemon/handlers/shared.ts +0 -130
  140. package/src/daemon/host-cu-proxy.ts +401 -0
  141. package/src/daemon/lifecycle.ts +36 -68
  142. package/src/daemon/message-protocol.ts +3 -0
  143. package/src/daemon/message-types/computer-use.ts +2 -119
  144. package/src/daemon/message-types/host-cu.ts +19 -0
  145. package/src/daemon/message-types/messages.ts +3 -0
  146. package/src/daemon/server.ts +14 -21
  147. package/src/daemon/session-agent-loop-handlers.ts +2 -0
  148. package/src/daemon/session-attachments.ts +1 -2
  149. package/src/daemon/session-slash.ts +1 -1
  150. package/src/daemon/session-surfaces.ts +40 -28
  151. package/src/daemon/session-tool-setup.ts +2 -9
  152. package/src/daemon/session.ts +138 -15
  153. package/src/daemon/tool-side-effects.ts +2 -8
  154. package/src/daemon/watch-handler.ts +2 -2
  155. package/src/events/tool-metrics-listener.ts +2 -2
  156. package/src/hooks/manager.ts +1 -4
  157. package/src/inbound/public-ingress-urls.ts +7 -7
  158. package/src/logfire.ts +16 -5
  159. package/src/memory/conversation-key-store.ts +21 -0
  160. package/src/memory/db-init.ts +4 -0
  161. package/src/memory/migrations/149-oauth-tables.ts +60 -0
  162. package/src/memory/migrations/index.ts +1 -0
  163. package/src/memory/schema/index.ts +1 -0
  164. package/src/memory/schema/oauth.ts +65 -0
  165. package/src/messaging/provider.ts +4 -4
  166. package/src/messaging/providers/gmail/client.ts +82 -2
  167. package/src/messaging/providers/gmail/people-client.ts +10 -10
  168. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  169. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  170. package/src/messaging/registry.ts +2 -32
  171. package/src/notifications/copy-composer.ts +0 -5
  172. package/src/notifications/signal.ts +4 -5
  173. package/src/oauth/byo-connection.test.ts +126 -25
  174. package/src/oauth/byo-connection.ts +22 -6
  175. package/src/oauth/connect-orchestrator.ts +113 -57
  176. package/src/oauth/connect-types.ts +17 -23
  177. package/src/oauth/connection-resolver.ts +35 -11
  178. package/src/oauth/connection.ts +1 -1
  179. package/src/oauth/manual-token-connection.ts +104 -0
  180. package/src/oauth/oauth-store.ts +496 -0
  181. package/src/oauth/platform-connection.test.ts +29 -0
  182. package/src/oauth/platform-connection.ts +6 -5
  183. package/src/oauth/provider-behaviors.ts +124 -0
  184. package/src/oauth/scope-policy.ts +9 -2
  185. package/src/oauth/seed-providers.ts +161 -0
  186. package/src/oauth/token-persistence.ts +74 -78
  187. package/src/permissions/checker.ts +3 -3
  188. package/src/permissions/defaults.ts +0 -1
  189. package/src/permissions/prompter.ts +10 -1
  190. package/src/permissions/trust-store.ts +13 -0
  191. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  192. package/src/prompts/system-prompt.ts +28 -40
  193. package/src/providers/anthropic/client.ts +133 -24
  194. package/src/providers/retry.ts +1 -27
  195. package/src/runtime/auth/route-policy.ts +0 -3
  196. package/src/runtime/channel-reply-delivery.ts +0 -40
  197. package/src/runtime/gateway-client.ts +0 -7
  198. package/src/runtime/http-server.ts +8 -6
  199. package/src/runtime/http-types.ts +2 -2
  200. package/src/runtime/middleware/twilio-validation.ts +1 -11
  201. package/src/runtime/pending-interactions.ts +14 -12
  202. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  203. package/src/runtime/routes/conversation-routes.ts +73 -19
  204. package/src/runtime/routes/events-routes.ts +21 -11
  205. package/src/runtime/routes/host-cu-routes.ts +97 -0
  206. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  207. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  208. package/src/runtime/routes/log-export-routes.ts +126 -8
  209. package/src/runtime/routes/settings-routes.ts +55 -48
  210. package/src/runtime/routes/surface-action-routes.ts +1 -1
  211. package/src/runtime/routes/watch-routes.ts +128 -0
  212. package/src/schedule/integration-status.ts +10 -9
  213. package/src/security/credential-key.ts +0 -156
  214. package/src/security/keychain-broker-client.ts +5 -6
  215. package/src/security/oauth2.ts +1 -1
  216. package/src/security/token-manager.ts +119 -46
  217. package/src/skills/catalog-install.ts +358 -0
  218. package/src/skills/include-graph.ts +32 -0
  219. package/src/telegram/bot-username.ts +2 -3
  220. package/src/tools/browser/network-recorder.ts +1 -1
  221. package/src/tools/browser/network-recording-types.ts +1 -1
  222. package/src/tools/computer-use/definitions.ts +46 -11
  223. package/src/tools/computer-use/registry.ts +4 -5
  224. package/src/tools/credentials/broker.ts +1 -2
  225. package/src/tools/credentials/metadata-store.ts +17 -121
  226. package/src/tools/credentials/vault.ts +94 -167
  227. package/src/tools/registry.ts +2 -7
  228. package/src/tools/skills/load.ts +62 -3
  229. package/src/tools/watch/watch-state.ts +0 -12
  230. package/src/util/logger.ts +7 -41
  231. package/src/util/platform.ts +9 -28
  232. package/src/watcher/providers/google-calendar.ts +2 -1
  233. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  234. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  235. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  236. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  237. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  238. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  239. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  240. package/src/cli/commands/dev.ts +0 -129
  241. package/src/cli/commands/map.ts +0 -391
  242. package/src/cli/commands/oauth.ts +0 -77
  243. package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
  244. package/src/daemon/computer-use-session.ts +0 -1026
  245. package/src/daemon/ride-shotgun-handler.ts +0 -569
  246. package/src/oauth/provider-base-urls.ts +0 -21
  247. package/src/oauth/provider-profiles.ts +0 -192
  248. package/src/prompts/computer-use-prompt.ts +0 -98
  249. package/src/runtime/routes/computer-use-routes.ts +0 -641
  250. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  251. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  252. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -14,22 +14,23 @@ import { loadSkillCatalog } from "../../config/skills.js";
14
14
  import { normalizeActivationKey } from "../../daemon/handlers/config-voice.js";
15
15
  import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
16
16
  import {
17
- getProviderProfile,
17
+ getApp,
18
+ getConnectionByProvider,
19
+ getMostRecentAppByProvider,
20
+ getProvider,
21
+ } from "../../oauth/oauth-store.js";
22
+ import {
23
+ getProviderBehavior,
18
24
  resolveService,
19
- } from "../../oauth/provider-profiles.js";
25
+ } from "../../oauth/provider-behaviors.js";
20
26
  import {
21
27
  check,
22
28
  classifyRisk,
23
29
  generateAllowlistOptions,
24
30
  generateScopeOptions,
25
31
  } from "../../permissions/checker.js";
26
- import { credentialKey } from "../../security/credential-key.js";
27
32
  import { getSecureKey } from "../../security/secure-keys.js";
28
33
  import { parseToolManifestFile } from "../../skills/tool-manifest.js";
29
- import {
30
- assertMetadataWritable,
31
- getCredentialMetadata,
32
- } from "../../tools/credentials/metadata-store.js";
33
34
  import {
34
35
  type ManifestOverride,
35
36
  resolveExecutionTarget,
@@ -139,50 +140,39 @@ function sanitizeOAuthError(message: string): string {
139
140
  return "OAuth authentication failed. Please try again.";
140
141
  }
141
142
 
142
- /** Resolve client_secret from the keychain, checking canonical then alias service name. */
143
- function getClientSecret(
144
- resolvedService: string,
145
- rawService: string,
146
- ): string | undefined {
147
- return (
148
- getSecureKey(credentialKey(resolvedService, "client_secret")) ??
149
- (resolvedService !== rawService
150
- ? getSecureKey(credentialKey(rawService, "client_secret"))
151
- : undefined) ??
152
- undefined
153
- );
154
- }
155
-
156
143
  async function handleOAuthConnectStart(body: {
157
144
  service?: string;
158
145
  requestedScopes?: string[];
159
146
  }): Promise<Response> {
160
- try {
161
- assertMetadataWritable();
162
- } catch {
163
- return httpError(
164
- "UNPROCESSABLE_ENTITY",
165
- "Credential metadata file has an unrecognized version. Cannot store OAuth credentials.",
166
- 422,
167
- );
168
- }
169
-
170
147
  if (!body.service) {
171
148
  return httpError("BAD_REQUEST", "Missing required field: service", 400);
172
149
  }
173
150
 
174
151
  const resolvedService = resolveService(body.service);
175
152
 
176
- // client_id is stored in metadata (oauth2ClientId), not the secure store.
177
- let clientId = getCredentialMetadata(
178
- resolvedService,
179
- "access_token",
180
- )?.oauth2ClientId;
181
- if (!clientId && resolvedService !== body.service) {
182
- clientId = getCredentialMetadata(
183
- body.service,
184
- "access_token",
185
- )?.oauth2ClientId;
153
+ // Resolve client_id and client_secret from oauth-store.
154
+ let clientId: string | undefined;
155
+ let clientSecret: string | undefined;
156
+
157
+ // Try existing connection first (re-auth flow)
158
+ const conn = getConnectionByProvider(resolvedService);
159
+ if (conn) {
160
+ const app = getApp(conn.oauthAppId);
161
+ if (app) {
162
+ clientId = app.clientId;
163
+ clientSecret = getSecureKey(`oauth_app/${app.id}/client_secret`);
164
+ }
165
+ }
166
+
167
+ // Fall back to most recent app for this provider (first-time connect with stored app)
168
+ if (!clientId) {
169
+ const dbApp = getMostRecentAppByProvider(resolvedService);
170
+ if (dbApp) {
171
+ clientId = dbApp.clientId;
172
+ if (!clientSecret) {
173
+ clientSecret = getSecureKey(`oauth_app/${dbApp.id}/client_secret`);
174
+ }
175
+ }
186
176
  }
187
177
 
188
178
  if (!clientId) {
@@ -193,12 +183,11 @@ async function handleOAuthConnectStart(body: {
193
183
  );
194
184
  }
195
185
 
196
- const clientSecret = getClientSecret(resolvedService, body.service);
197
-
198
- const profile = getProviderProfile(resolvedService);
186
+ const behavior = getProviderBehavior(resolvedService);
187
+ const providerRow = getProvider(resolvedService);
199
188
  const requiresSecret =
200
- profile?.setup?.requiresClientSecret ??
201
- !!(profile?.tokenEndpointAuthMethod || profile?.extraParams);
189
+ behavior?.setup?.requiresClientSecret ??
190
+ !!(providerRow?.tokenEndpointAuthMethod || providerRow?.extraParams);
202
191
  if (requiresSecret && !clientSecret) {
203
192
  return httpError(
204
193
  "BAD_REQUEST",
@@ -222,6 +211,15 @@ async function handleOAuthConnectStart(body: {
222
211
  authUrl = url;
223
212
  },
224
213
  onDeferredComplete: (deferredResult) => {
214
+ // Prefer accountInfo from oauth-store when available.
215
+ let accountInfo = deferredResult.accountInfo;
216
+ try {
217
+ const conn = getConnectionByProvider(resolvedService);
218
+ if (conn?.accountInfo) accountInfo = conn.accountInfo;
219
+ } catch {
220
+ // DB not ready — use orchestrator value
221
+ }
222
+
225
223
  // Emit oauth_connect_result to all connected SSE clients so the
226
224
  // UI can update immediately when the deferred browser flow completes.
227
225
  assistantEventHub
@@ -230,7 +228,7 @@ async function handleOAuthConnectStart(body: {
230
228
  type: "oauth_connect_result",
231
229
  success: deferredResult.success,
232
230
  service: deferredResult.service,
233
- accountInfo: deferredResult.accountInfo,
231
+ accountInfo,
234
232
  error: deferredResult.error,
235
233
  }),
236
234
  )
@@ -273,10 +271,19 @@ async function handleOAuthConnectStart(body: {
273
271
  });
274
272
  }
275
273
 
274
+ // Prefer accountInfo from oauth-store when available.
275
+ let responseAccountInfo = result.accountInfo;
276
+ try {
277
+ const conn = getConnectionByProvider(resolvedService);
278
+ if (conn?.accountInfo) responseAccountInfo = conn.accountInfo;
279
+ } catch {
280
+ // DB not ready — use orchestrator value
281
+ }
282
+
276
283
  return Response.json({
277
284
  ok: true,
278
285
  grantedScopes: result.grantedScopes,
279
- accountInfo: result.accountInfo,
286
+ accountInfo: responseAccountInfo,
280
287
  ...(authUrl ? { authUrl } : {}),
281
288
  });
282
289
  } catch (err) {
@@ -10,7 +10,7 @@ import type { RouteDefinition } from "../http-router.js";
10
10
 
11
11
  const log = getLogger("surface-action-routes");
12
12
 
13
- /** Any object that can handle a surface action (Session or ComputerUseSession). */
13
+ /** Any object that can handle a surface action. */
14
14
  interface SurfaceActionTarget {
15
15
  handleSurfaceAction(
16
16
  surfaceId: string,
@@ -0,0 +1,128 @@
1
+ /**
2
+ * HTTP route handler for watch (ambient observation) functionality.
3
+ *
4
+ * Decoupled from computer-use routes so that the watch endpoint has
5
+ * zero dependency on CU session state.
6
+ */
7
+
8
+ import { getLogger } from "../../util/logger.js";
9
+ import { httpError } from "../http-errors.js";
10
+ import type { RouteDefinition } from "../http-router.js";
11
+
12
+ const log = getLogger("watch-routes");
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Dependency injection interface
16
+ // ---------------------------------------------------------------------------
17
+
18
+ /**
19
+ * Minimal interface for watch observation handling.
20
+ * The daemon wires a concrete implementation at startup.
21
+ */
22
+ export interface WatchDeps {
23
+ /** Handle a watch observation. */
24
+ handleWatchObservation: (params: {
25
+ watchId: string;
26
+ sessionId: string;
27
+ ocrText: string;
28
+ appName?: string;
29
+ windowTitle?: string;
30
+ bundleIdentifier?: string;
31
+ timestamp: number;
32
+ captureIndex: number;
33
+ totalExpected: number;
34
+ }) => Promise<void>;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Route handler
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * POST /v1/computer-use/watch — send a watch observation.
43
+ *
44
+ * Body: { watchId, sessionId, ocrText, appName?, windowTitle?,
45
+ * bundleIdentifier?, timestamp, captureIndex, totalExpected }
46
+ */
47
+ async function handleWatchObservationRoute(
48
+ req: Request,
49
+ deps: WatchDeps,
50
+ ): Promise<Response> {
51
+ const body = (await req.json()) as {
52
+ watchId?: string;
53
+ sessionId?: string;
54
+ ocrText?: string;
55
+ appName?: string;
56
+ windowTitle?: string;
57
+ bundleIdentifier?: string;
58
+ timestamp?: number;
59
+ captureIndex?: number;
60
+ totalExpected?: number;
61
+ };
62
+
63
+ if (!body.watchId || typeof body.watchId !== "string") {
64
+ return httpError("BAD_REQUEST", "watchId is required", 400);
65
+ }
66
+ if (!body.sessionId || typeof body.sessionId !== "string") {
67
+ return httpError("BAD_REQUEST", "sessionId is required", 400);
68
+ }
69
+ if (!body.ocrText || typeof body.ocrText !== "string") {
70
+ return httpError("BAD_REQUEST", "ocrText is required", 400);
71
+ }
72
+ if (typeof body.timestamp !== "number") {
73
+ return httpError("BAD_REQUEST", "timestamp is required", 400);
74
+ }
75
+ if (typeof body.captureIndex !== "number") {
76
+ return httpError("BAD_REQUEST", "captureIndex is required", 400);
77
+ }
78
+ if (typeof body.totalExpected !== "number") {
79
+ return httpError("BAD_REQUEST", "totalExpected is required", 400);
80
+ }
81
+
82
+ try {
83
+ await deps.handleWatchObservation({
84
+ watchId: body.watchId,
85
+ sessionId: body.sessionId,
86
+ ocrText: body.ocrText,
87
+ appName: body.appName,
88
+ windowTitle: body.windowTitle,
89
+ bundleIdentifier: body.bundleIdentifier,
90
+ timestamp: body.timestamp,
91
+ captureIndex: body.captureIndex,
92
+ totalExpected: body.totalExpected,
93
+ });
94
+
95
+ return Response.json({ ok: true });
96
+ } catch (err) {
97
+ const message = err instanceof Error ? err.message : String(err);
98
+ log.error(
99
+ { err, watchId: body.watchId },
100
+ "Failed to handle watch observation via HTTP",
101
+ );
102
+ return httpError("INTERNAL_ERROR", message, 500);
103
+ }
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Route definitions
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export function watchRouteDefinitions(deps: {
111
+ getWatchDeps?: () => WatchDeps;
112
+ }): RouteDefinition[] {
113
+ const getDeps = (): WatchDeps => {
114
+ if (!deps.getWatchDeps) {
115
+ throw new Error("Watch deps not available");
116
+ }
117
+ return deps.getWatchDeps();
118
+ };
119
+
120
+ return [
121
+ {
122
+ endpoint: "computer-use/watch",
123
+ method: "POST",
124
+ policyKey: "computer-use/watch",
125
+ handler: async ({ req }) => handleWatchObservationRoute(req, getDeps()),
126
+ },
127
+ ];
128
+ }
@@ -1,6 +1,8 @@
1
1
  import { hasTwilioCredentials } from "../calls/twilio-rest.js";
2
- import { credentialKey } from "../security/credential-key.js";
3
- import { getSecureKey } from "../security/secure-keys.js";
2
+ import {
3
+ getConnectionByProvider,
4
+ isProviderConnected,
5
+ } from "../oauth/oauth-store.js";
4
6
 
5
7
  interface IntegrationProbe {
6
8
  name: string;
@@ -13,14 +15,12 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
13
15
  {
14
16
  name: "Gmail",
15
17
  category: "email",
16
- isConnected: () =>
17
- !!getSecureKey(credentialKey("integration:gmail", "access_token")),
18
+ isConnected: () => isProviderConnected("integration:gmail"),
18
19
  },
19
20
  {
20
21
  name: "Slack",
21
22
  category: "messaging",
22
- isConnected: () =>
23
- !!getSecureKey(credentialKey("integration:slack", "access_token")),
23
+ isConnected: () => isProviderConnected("integration:slack"),
24
24
  },
25
25
  {
26
26
  name: "Twilio",
@@ -30,9 +30,10 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
30
30
  {
31
31
  name: "Telegram",
32
32
  category: "messaging",
33
- isConnected: () =>
34
- !!getSecureKey(credentialKey("telegram", "bot_token")) &&
35
- !!getSecureKey(credentialKey("telegram", "webhook_secret")),
33
+ isConnected: () => {
34
+ const conn = getConnectionByProvider("telegram");
35
+ return !!(conn && conn.status === "active");
36
+ },
36
37
  },
37
38
  ];
38
39
 
@@ -2,23 +2,8 @@
2
2
  * Single source of truth for credential key format in the secure store.
3
3
  *
4
4
  * Keys follow the pattern: credential/{service}/{field}
5
- *
6
- * Previously, keys used colons as delimiters (credential:service:field),
7
- * which was ambiguous when service names contained colons (e.g.
8
- * "integration:gmail"). The slash-delimited format avoids this.
9
5
  */
10
6
 
11
- import { listCredentialMetadata } from "../tools/credentials/metadata-store.js";
12
- import { getLogger } from "../util/logger.js";
13
- import {
14
- deleteSecureKey,
15
- getSecureKey,
16
- listSecureKeys,
17
- setSecureKey,
18
- } from "./secure-keys.js";
19
-
20
- const log = getLogger("credential-key");
21
-
22
7
  /**
23
8
  * Build a credential key for the secure store.
24
9
  *
@@ -27,144 +12,3 @@ const log = getLogger("credential-key");
27
12
  export function credentialKey(service: string, field: string): string {
28
13
  return `credential/${service}/${field}`;
29
14
  }
30
-
31
- // ---------------------------------------------------------------------------
32
- // Migration from colon-delimited keys
33
- // ---------------------------------------------------------------------------
34
-
35
- let migrated = false;
36
-
37
- /**
38
- * Migrate any legacy colon-delimited credential keys to the new
39
- * slash-delimited format. Idempotent: skips keys that already exist
40
- * under the new format, and only runs once per process (guarded by a
41
- * module-level flag).
42
- *
43
- * Legacy key format: `credential:<service>:<field>`
44
- * New key format: `credential/<service>/<field>`
45
- *
46
- * The old colon-delimited format is ambiguous when either the service
47
- * or field name contains colons — for `credential:A:B:C:D`, you can't
48
- * tell where the service ends and the field begins without external
49
- * context.
50
- *
51
- * To resolve this, the function first consults the credential metadata
52
- * store to find which (service, field) pair matches a valid split.
53
- * If no metadata match is found, it falls back to splitting on the
54
- * **first** colon after the prefix — this handles the common case
55
- * where service names are simple (e.g. "doordash.com") and field
56
- * names may contain colons (e.g. "session:cookies").
57
- */
58
- export function migrateKeys(): void {
59
- if (migrated) return;
60
- migrated = true;
61
-
62
- let allKeys: string[];
63
- try {
64
- allKeys = listSecureKeys();
65
- } catch (err) {
66
- log.warn({ err }, "Failed to list secure keys during migration");
67
- return;
68
- }
69
-
70
- const colonKeys = allKeys.filter(
71
- (k) => k.startsWith("credential:") && !k.startsWith("credential/"),
72
- );
73
- if (colonKeys.length === 0) return;
74
-
75
- log.info(
76
- { count: colonKeys.length },
77
- "Migrating colon-delimited credential keys to slash-delimited format",
78
- );
79
-
80
- // Build a set of known (service, field) pairs from credential metadata
81
- // to disambiguate colon-delimited keys.
82
- const knownPairs = new Set<string>();
83
- try {
84
- for (const meta of listCredentialMetadata()) {
85
- knownPairs.add(`${meta.service}\0${meta.field}`);
86
- }
87
- } catch {
88
- // If metadata is unavailable, we'll rely on the first-colon fallback.
89
- }
90
-
91
- for (const oldKey of colonKeys) {
92
- // Strip the "credential:" prefix — `rest` is "service:field" with
93
- // potential colons in either part.
94
- const rest = oldKey.slice("credential:".length);
95
-
96
- const parsed = parseServiceField(rest, knownPairs);
97
- if (parsed === undefined) {
98
- log.warn({ key: oldKey }, "Skipping malformed credential key");
99
- continue;
100
- }
101
-
102
- const { service, field } = parsed;
103
- const newKey = credentialKey(service, field);
104
-
105
- // Skip if the new key already exists (idempotent)
106
- if (getSecureKey(newKey) !== undefined) {
107
- // Clean up old key
108
- deleteSecureKey(oldKey);
109
- continue;
110
- }
111
-
112
- const value = getSecureKey(oldKey);
113
- if (value === undefined) {
114
- continue;
115
- }
116
-
117
- const ok = setSecureKey(newKey, value);
118
- if (ok) {
119
- deleteSecureKey(oldKey);
120
- } else {
121
- log.warn(
122
- { oldKey, newKey },
123
- "Failed to write migrated key; keeping old key",
124
- );
125
- }
126
- }
127
- }
128
-
129
- /**
130
- * Parse a "service:field" string, using known metadata pairs to
131
- * disambiguate when colons appear in either part.
132
- *
133
- * Strategy:
134
- * 1. Try every possible split position and check against metadata.
135
- * 2. If no metadata match, fall back to splitting on the first colon
136
- * (field names with colons are more common than service names with colons).
137
- *
138
- * Returns undefined for malformed keys that have no colon.
139
- */
140
- function parseServiceField(
141
- rest: string,
142
- knownPairs: Set<string>,
143
- ): { service: string; field: string } | undefined {
144
- const firstColon = rest.indexOf(":");
145
- if (firstColon <= 0) return undefined;
146
-
147
- // Try each possible split position against metadata
148
- if (knownPairs.size > 0) {
149
- for (let i = firstColon; i < rest.length; i++) {
150
- if (rest[i] !== ":") continue;
151
- const service = rest.slice(0, i);
152
- const field = rest.slice(i + 1);
153
- if (field.length > 0 && knownPairs.has(`${service}\0${field}`)) {
154
- return { service, field };
155
- }
156
- }
157
- }
158
-
159
- // Fallback: split on first colon — handles simple services with
160
- // compound field names (e.g. "doordash.com:session:cookies").
161
- return {
162
- service: rest.slice(0, firstColon),
163
- field: rest.slice(firstColon + 1),
164
- };
165
- }
166
-
167
- /** @internal Test-only: reset the migration guard so migrateKeys() runs again. */
168
- export function _resetMigrationFlag(): void {
169
- migrated = false;
170
- }
@@ -6,7 +6,7 @@
6
6
  * provides a graceful-fallback interface: every public method returns a
7
7
  * safe default on failure and never throws.
8
8
  *
9
- * Socket path: read from VELLUM_KEYCHAIN_BROKER_SOCKET env var.
9
+ * Socket path: derived from getRootDir() as `~/.vellum/keychain-broker.sock`.
10
10
  * Auth token: read from ~/.vellum/protected/keychain-broker.token on first
11
11
  * connection, cached for process lifetime.
12
12
  */
@@ -70,8 +70,8 @@ function getTokenPath(): string {
70
70
  return join(getRootDir(), "protected", "keychain-broker.token");
71
71
  }
72
72
 
73
- function getSocketPath(): string | undefined {
74
- return process.env.VELLUM_KEYCHAIN_BROKER_SOCKET;
73
+ function getSocketPath(): string {
74
+ return join(getRootDir(), "keychain-broker.sock");
75
75
  }
76
76
 
77
77
  // ---------------------------------------------------------------------------
@@ -172,7 +172,7 @@ export function createBrokerClient(): KeychainBrokerClient {
172
172
  function connect(): Promise<Socket> {
173
173
  return new Promise((resolve, reject) => {
174
174
  const socketPath = getSocketPath();
175
- if (!socketPath) {
175
+ if (!pathExists(socketPath)) {
176
176
  reject(new Error("No socket path"));
177
177
  return;
178
178
  }
@@ -328,8 +328,7 @@ export function createBrokerClient(): KeychainBrokerClient {
328
328
  return {
329
329
  isAvailable(): boolean {
330
330
  if (permanentlyUnavailable) return false;
331
- const socketPath = getSocketPath();
332
- if (!socketPath) return false;
331
+ if (!pathExists(getSocketPath())) return false;
333
332
  return pathExists(getTokenPath());
334
333
  },
335
334
 
@@ -691,7 +691,7 @@ export async function startOAuth2Flow(
691
691
  if (transport === "gateway") {
692
692
  if (!hasPublicUrl) {
693
693
  throw new Error(
694
- "Gateway transport requires a public ingress URL. Set ingress.publicBaseUrl or INGRESS_PUBLIC_BASE_URL, or use loopback transport.",
694
+ "Gateway transport requires a public ingress URL. Set ingress.publicBaseUrl, or use loopback transport.",
695
695
  );
696
696
  }
697
697
  log.debug({ transport: "gateway" }, "OAuth2 flow starting");