@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
@@ -1,17 +1,22 @@
1
1
  import { getConfig } from "../../config/loader.js";
2
2
  import { orchestrateOAuthConnect } from "../../oauth/connect-orchestrator.js";
3
3
  import {
4
- getProviderProfile,
4
+ disconnectOAuthProvider,
5
+ getAppByProviderAndClientId,
6
+ getMostRecentAppByProvider,
7
+ getProvider,
8
+ } from "../../oauth/oauth-store.js";
9
+ import {
10
+ getProviderBehavior,
5
11
  resolveService,
6
12
  SERVICE_ALIASES,
7
- } from "../../oauth/provider-profiles.js";
13
+ } from "../../oauth/provider-behaviors.js";
8
14
  import { RiskLevel } from "../../permissions/types.js";
9
15
  import type { ToolDefinition } from "../../providers/types.js";
10
16
  import { buildAssistantEvent } from "../../runtime/assistant-event.js";
11
17
  import { assistantEventHub } from "../../runtime/assistant-event-hub.js";
12
18
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
13
- import { credentialKey, migrateKeys } from "../../security/credential-key.js";
14
- import type { TokenEndpointAuthMethod } from "../../security/oauth2.js";
19
+ import { credentialKey } from "../../security/credential-key.js";
15
20
  import {
16
21
  deleteSecureKeyAsync,
17
22
  getSecureKey,
@@ -36,50 +41,6 @@ import { toPolicyFromInput, validatePolicyInput } from "./policy-validate.js";
36
41
 
37
42
  const log = getLogger("credential-vault");
38
43
 
39
- /**
40
- * Look up a stored OAuth secret (e.g. client_secret) for a service from the
41
- * secure store. Checks both the canonical and alias service names.
42
- */
43
- function findStoredOAuthSecret(
44
- service: string,
45
- field: string,
46
- ): string | undefined {
47
- const servicesToCheck = [service];
48
- // Also check the alias if the input is the canonical name, or vice versa
49
- for (const [alias, canonical] of Object.entries(SERVICE_ALIASES)) {
50
- if (canonical === service) servicesToCheck.push(alias);
51
- if (alias === service) servicesToCheck.push(canonical);
52
- }
53
- for (const svc of servicesToCheck) {
54
- const value = getSecureKey(credentialKey(svc, field));
55
- if (value) return value;
56
- }
57
- return undefined;
58
- }
59
-
60
- /**
61
- * Look up the stored OAuth client_id for a service from credential metadata.
62
- * Checks both the canonical and alias service names.
63
- */
64
- function findStoredOAuthClientId(service: string): string | undefined {
65
- const servicesToCheck = [service];
66
- for (const [alias, canonical] of Object.entries(SERVICE_ALIASES)) {
67
- if (canonical === service) servicesToCheck.push(alias);
68
- if (alias === service) servicesToCheck.push(canonical);
69
- }
70
- // Check metadata first (written by oauth2_connect after successful auth)
71
- for (const svc of servicesToCheck) {
72
- const meta = getCredentialMetadata(svc, "access_token");
73
- if (meta?.oauth2ClientId) return meta.oauth2ClientId;
74
- }
75
- // Fall back to secure key store (written by credential_store prompt action)
76
- for (const svc of servicesToCheck) {
77
- const value = getSecureKey(credentialKey(svc, "client_id"));
78
- if (value) return value;
79
- }
80
- return undefined;
81
- }
82
-
83
44
  class CredentialStoreTool implements Tool {
84
45
  name = "credential_store";
85
46
  description =
@@ -151,16 +112,6 @@ class CredentialStoreTool implements Tool {
151
112
  description:
152
113
  'Human-readable description of intended usage (for store/prompt actions), e.g. "GitHub login for pushing changes"',
153
114
  },
154
- auth_url: {
155
- type: "string",
156
- description:
157
- "OAuth2 authorization endpoint (only for oauth2_connect action). Auto-filled for well-known services (gmail, slack).",
158
- },
159
- token_url: {
160
- type: "string",
161
- description:
162
- "OAuth2 token endpoint (only for oauth2_connect action). Auto-filled for well-known services (gmail, slack).",
163
- },
164
115
  scopes: {
165
116
  type: "array",
166
117
  items: { type: "string" },
@@ -172,27 +123,11 @@ class CredentialStoreTool implements Tool {
172
123
  description:
173
124
  "OAuth2 client ID (only for oauth2_connect action). If omitted, looked up from previously stored credentials.",
174
125
  },
175
- extra_params: {
176
- type: "object",
177
- description:
178
- "Extra query params for OAuth2 auth URL (only for oauth2_connect action)",
179
- },
180
- userinfo_url: {
181
- type: "string",
182
- description:
183
- "Endpoint to fetch account info after OAuth2 auth (only for oauth2_connect action)",
184
- },
185
126
  client_secret: {
186
127
  type: "string",
187
128
  description:
188
129
  "OAuth2 client secret for providers that require it (e.g. Google, Slack). If omitted, looked up from previously stored credentials; if still absent, PKCE-only is used (only for oauth2_connect action)",
189
130
  },
190
- token_endpoint_auth_method: {
191
- type: "string",
192
- enum: ["client_secret_basic", "client_secret_post"],
193
- description:
194
- 'How to send client credentials at the token endpoint: "client_secret_post" (default, in POST body) or "client_secret_basic" (HTTP Basic Auth header). Only for oauth2_connect action.',
195
- },
196
131
  alias: {
197
132
  type: "string",
198
133
  description:
@@ -243,7 +178,6 @@ class CredentialStoreTool implements Tool {
243
178
  input: Record<string, unknown>,
244
179
  context: ToolContext,
245
180
  ): Promise<ToolExecutionResult> {
246
- migrateKeys();
247
181
  const action = input.action as string;
248
182
 
249
183
  switch (action) {
@@ -517,6 +451,21 @@ class CredentialStoreTool implements Tool {
517
451
  "metadata delete failed after removing credential",
518
452
  );
519
453
  }
454
+ // Also clean up any OAuth connection for this service (best-effort)
455
+ try {
456
+ const oauthResult = await disconnectOAuthProvider(service);
457
+ if (oauthResult === "error") {
458
+ log.warn(
459
+ { service },
460
+ "OAuth disconnect failed after removing credential — secure key deletion error",
461
+ );
462
+ }
463
+ } catch (err) {
464
+ log.warn(
465
+ { service, err },
466
+ "OAuth disconnect failed after removing credential",
467
+ );
468
+ }
520
469
  return {
521
470
  content: `Deleted credential for ${service}/${field}.`,
522
471
  isError: false,
@@ -777,51 +726,44 @@ class CredentialStoreTool implements Tool {
777
726
  // Resolve aliases (e.g. "gmail" → "integration:gmail")
778
727
  const service = resolveService(rawService);
779
728
 
780
- // Fill missing params from provider profile
781
- const profile = getProviderProfile(service);
782
-
783
- // Look up client_id from metadata and client_secret from secure store
784
- const clientId =
785
- (input.client_id as string | undefined) ??
786
- findStoredOAuthClientId(service);
787
- const clientSecret =
788
- (input.client_secret as string | undefined) ??
789
- findStoredOAuthSecret(service, "client_secret");
729
+ // Code-side behavioral fields (identityVerifier, setup, etc.)
730
+ const behavior = getProviderBehavior(service);
731
+ // Protocol-level config from the DB (authUrl, tokenUrl, scopes, etc.)
732
+ const providerRow = getProvider(service);
733
+
734
+ // Resolve client_id and client_secret.
735
+ // Priority:
736
+ // 1. Explicit input from the caller
737
+ // 2. oauth-store DB when clientId is already known, look up the
738
+ // matching app so the secret comes from the same app. Only fall
739
+ // back to the most-recent-app heuristic when clientId is unknown.
740
+ let clientId = input.client_id as string | undefined;
741
+ let clientSecret = input.client_secret as string | undefined;
742
+
743
+ if (!clientId || !clientSecret) {
744
+ const dbApp = clientId
745
+ ? getAppByProviderAndClientId(service, clientId)
746
+ : getMostRecentAppByProvider(service);
747
+ if (dbApp) {
748
+ if (!clientId) clientId = dbApp.clientId;
749
+ if (!clientSecret) {
750
+ clientSecret = getSecureKey(
751
+ `oauth_app/${dbApp.id}/client_secret`,
752
+ );
753
+ }
754
+ }
755
+ }
790
756
 
791
757
  // Early guardrails that stay in vault.ts (credential resolution is vault-specific)
792
758
  const inputScopes = input.scopes as string[] | undefined;
793
759
 
794
- if (profile) {
795
- // Profile-based provider (well-known like gmail, slack, twitter):
796
- // Don't pass authUrl/tokenUrl the orchestrator resolves those from the profile.
797
- // Pass user-provided scopes as requestedScopes so the scope policy engine is invoked.
798
- // If no scopes provided, pass neither — let the orchestrator use profile defaults via scope policy.
799
- } else {
800
- // Custom/unknown provider: require authUrl, tokenUrl, scopes from input
801
- if (!input.auth_url)
802
- return {
803
- content:
804
- "Error: auth_url is required for oauth2_connect action (no well-known config for this service)",
805
- isError: true,
806
- };
807
- if (!input.token_url)
808
- return {
809
- content:
810
- "Error: token_url is required for oauth2_connect action (no well-known config for this service)",
811
- isError: true,
812
- };
813
- if (!inputScopes)
814
- return {
815
- content:
816
- "Error: scopes is required for oauth2_connect action (no well-known config for this service)",
817
- isError: true,
818
- };
760
+ if (!providerRow) {
761
+ return {
762
+ content: `Error: no OAuth provider registered for "${service}". Ensure the provider is seeded in the database.`,
763
+ isError: true,
764
+ };
819
765
  }
820
766
 
821
- const authUrl =
822
- (input.auth_url as string | undefined) ?? profile?.authUrl;
823
- const tokenUrl =
824
- (input.token_url as string | undefined) ?? profile?.tokenUrl;
825
767
  if (!clientId)
826
768
  return {
827
769
  content:
@@ -833,10 +775,10 @@ class CredentialStoreTool implements Tool {
833
775
  // agent to collect it from the user rather than letting it improvise
834
776
  // browser-automation workarounds that inevitably fail.
835
777
  const requiresSecret =
836
- profile?.setup?.requiresClientSecret ??
837
- !!(profile?.tokenEndpointAuthMethod || profile?.extraParams);
778
+ behavior?.setup?.requiresClientSecret ??
779
+ !!(providerRow.tokenEndpointAuthMethod || providerRow.extraParams);
838
780
  if (requiresSecret && !clientSecret) {
839
- const skillId = profile?.setupSkillId;
781
+ const skillId = behavior?.setupSkillId;
840
782
  const skillHint = skillId
841
783
  ? `\n\nLoad the "${skillId}" skill for provider-specific instructions on obtaining the client secret.`
842
784
  : '\n\nUse credential_store with action "prompt" to securely collect the client_secret from the user before calling oauth2_connect again.';
@@ -846,25 +788,8 @@ class CredentialStoreTool implements Tool {
846
788
  };
847
789
  }
848
790
 
849
- try {
850
- assertMetadataWritable();
851
- } catch {
852
- return {
853
- content:
854
- "Error: credential metadata file has an unrecognized version; cannot store credentials",
855
- isError: true,
856
- };
857
- }
858
-
859
- const tokenEndpointAuthMethod =
860
- (input.token_endpoint_auth_method as
861
- | TokenEndpointAuthMethod
862
- | undefined) ?? profile?.tokenEndpointAuthMethod;
863
-
864
- // Delegate to the shared orchestrator.
865
- // For profile-based providers, pass user scopes as requestedScopes so the
866
- // scope policy engine (resolveScopes) is invoked. For custom providers,
867
- // pass scopes directly as an explicit override.
791
+ // Delegate to the shared orchestrator — it resolves authUrl, tokenUrl,
792
+ // extraParams, userinfoUrl, and tokenEndpointAuthMethod from the DB.
868
793
  const result = await orchestrateOAuthConnect({
869
794
  service: rawService,
870
795
  clientId,
@@ -872,24 +797,7 @@ class CredentialStoreTool implements Tool {
872
797
  isInteractive: !!context.isInteractive,
873
798
  sendToClient: context.sendToClient,
874
799
  allowedTools: input.allowed_tools as string[] | undefined,
875
- authUrl,
876
- tokenUrl,
877
- ...(profile
878
- ? {
879
- // Profile-based: let orchestrator resolve scopes via policy engine.
880
- // Only pass requestedScopes if the user explicitly provided scopes.
881
- ...(inputScopes ? { requestedScopes: inputScopes } : {}),
882
- }
883
- : {
884
- // Custom provider: explicit scopes override (bypasses policy engine)
885
- scopes: inputScopes,
886
- }),
887
- extraParams:
888
- (input.extra_params as Record<string, string> | undefined) ??
889
- profile?.extraParams,
890
- userinfoUrl:
891
- (input.userinfo_url as string | undefined) ?? profile?.userinfoUrl,
892
- tokenEndpointAuthMethod,
800
+ ...(inputScopes ? { requestedScopes: inputScopes } : {}),
893
801
  onDeferredComplete: (deferredResult) => {
894
802
  // Emit oauth_connect_result to all connected SSE clients so the
895
803
  // UI can update immediately when the deferred browser flow completes.
@@ -958,8 +866,8 @@ class CredentialStoreTool implements Tool {
958
866
  };
959
867
  }
960
868
  const resolvedService = resolveService(rawService);
961
- const profile = getProviderProfile(resolvedService);
962
- if (!profile) {
869
+ const descProviderRow = getProvider(resolvedService);
870
+ if (!descProviderRow) {
963
871
  return {
964
872
  content: `No well-known OAuth config found for "${rawService}". Available services: ${Object.keys(
965
873
  SERVICE_ALIASES,
@@ -968,11 +876,17 @@ class CredentialStoreTool implements Tool {
968
876
  };
969
877
  }
970
878
 
879
+ const descBehavior = getProviderBehavior(resolvedService);
880
+
971
881
  // Compute the redirect URI based on callback transport
972
882
  let redirectUri: string;
973
- const transport = profile.callbackTransport ?? "gateway";
974
- if (transport === "loopback" && profile.loopbackPort) {
975
- redirectUri = `http://127.0.0.1:${profile.loopbackPort}/oauth/callback`;
883
+ const transport =
884
+ (descProviderRow.callbackTransport as
885
+ | "loopback"
886
+ | "gateway"
887
+ | null) ?? "gateway";
888
+ if (transport === "loopback" && descProviderRow.loopbackPort) {
889
+ redirectUri = `http://127.0.0.1:${descProviderRow.loopbackPort}/oauth/callback`;
976
890
  } else if (transport === "loopback") {
977
891
  redirectUri =
978
892
  "(automatic — no redirect URI needed, uses random localhost port)";
@@ -986,26 +900,39 @@ class CredentialStoreTool implements Tool {
986
900
  redirectUri = `${baseUrl}/webhooks/oauth/callback`;
987
901
  } catch {
988
902
  redirectUri =
989
- "(requires INGRESS_PUBLIC_BASE_URL — not currently configured)";
903
+ "(requires ingress.publicBaseUrl — not currently configured)";
990
904
  }
991
905
  }
992
906
 
993
907
  // Prefer explicit setup metadata, fall back to heuristic
994
908
  const requiresClientSecret =
995
- profile.setup?.requiresClientSecret ??
996
- !!(profile.tokenEndpointAuthMethod || profile.extraParams);
909
+ descBehavior?.setup?.requiresClientSecret ??
910
+ !!(
911
+ descProviderRow.tokenEndpointAuthMethod ||
912
+ descProviderRow.extraParams
913
+ );
914
+
915
+ const descDefaultScopes: string[] = descProviderRow.defaultScopes
916
+ ? JSON.parse(descProviderRow.defaultScopes)
917
+ : [];
997
918
 
998
919
  const info: Record<string, unknown> = {
999
920
  service: resolvedService,
1000
- authUrl: profile.authUrl,
1001
- tokenUrl: profile.tokenUrl,
1002
- scopes: profile.defaultScopes,
921
+ authUrl: descProviderRow.authUrl,
922
+ tokenUrl: descProviderRow.tokenUrl,
923
+ scopes: descDefaultScopes,
1003
924
  callbackTransport: transport,
1004
925
  redirectUri,
1005
926
  requiresClientSecret,
1006
927
  };
1007
- if (profile.setup) info.setup = profile.setup;
1008
- if (profile.extraParams) info.extraParams = profile.extraParams;
928
+ if (descBehavior?.setup) info.setup = descBehavior.setup;
929
+ if (descProviderRow.extraParams) {
930
+ try {
931
+ info.extraParams = JSON.parse(descProviderRow.extraParams);
932
+ } catch {
933
+ // Non-fatal
934
+ }
935
+ }
1009
936
 
1010
937
  return { content: JSON.stringify(info, null, 2), isError: false };
1011
938
  }
@@ -4,7 +4,6 @@ import { getLogger } from "../util/logger.js";
4
4
  import { coreAppProxyTools } from "./apps/definitions.js";
5
5
  import { registerAppTools } from "./apps/registry.js";
6
6
  import { allComputerUseTools } from "./computer-use/definitions.js";
7
- import { requestComputerControlTool } from "./computer-use/request-computer-control.js";
8
7
  import { hostFileEditTool } from "./host-filesystem/edit.js";
9
8
  import { hostFileReadTool } from "./host-filesystem/read.js";
10
9
  import { hostFileWriteTool } from "./host-filesystem/write.js";
@@ -300,8 +299,8 @@ export function getSkillRefCount(skillId: string): number {
300
299
  }
301
300
 
302
301
  export function getAllToolDefinitions(): ToolDefinition[] {
303
- // Exclude proxy tools (e.g. computer_use_* tools) — they are only used
304
- // by ComputerUseSession which builds its own tool definitions list.
302
+ // Exclude proxy tools (e.g. computer_use_* tools) — they are projected
303
+ // into sessions by the skill system, not via the global tool list.
305
304
  // Exclude skill-origin tools — they are managed by the session-level
306
305
  // skill projection system (projectSkillTools) and must not leak into
307
306
  // the base tool list, which is shared across sessions via the global
@@ -341,9 +340,6 @@ export async function initializeTools(): Promise<void> {
341
340
  registerTool(tool);
342
341
  }
343
342
 
344
- // The escalation tool is registered in core so text_qa sessions can execute it.
345
- // The 12 action tools are provided by the bundled computer-use skill.
346
- registerTool(requestComputerControlTool);
347
343
  registerUiSurfaceTools();
348
344
  registerAppTools();
349
345
 
@@ -367,7 +363,6 @@ export async function initializeTools(): Promise<void> {
367
363
  ...lazyTools.map((t: LazyToolDescriptor) => t.name),
368
364
  ...hostTools.map((t: Tool) => t.name),
369
365
  ...allComputerUseTools.map((t: Tool) => t.name),
370
- requestComputerControlTool.name,
371
366
  ...allUiSurfaceTools.map((t: Tool) => t.name),
372
367
  ...coreAppProxyTools.map((t: Tool) => t.name),
373
368
  ]);
@@ -8,7 +8,9 @@ import type { SkillSummary, SkillToolManifest } from "../../config/skills.js";
8
8
  import { loadSkillBySelector, loadSkillCatalog } from "../../config/skills.js";
9
9
  import { RiskLevel } from "../../permissions/types.js";
10
10
  import type { ToolDefinition } from "../../providers/types.js";
11
+ import { autoInstallFromCatalog } from "../../skills/catalog-install.js";
11
12
  import {
13
+ collectAllMissing,
12
14
  indexCatalogById,
13
15
  validateIncludes,
14
16
  } from "../../skills/include-graph.js";
@@ -137,7 +139,32 @@ export class SkillLoadTool implements Tool {
137
139
  };
138
140
  }
139
141
 
140
- const loaded = loadSkillBySelector(selector);
142
+ let loaded = loadSkillBySelector(selector);
143
+
144
+ // Auto-install from catalog if the skill isn't found locally
145
+ if (
146
+ !loaded.skill &&
147
+ (loaded.errorCode === "not_found" || loaded.errorCode === "empty_catalog")
148
+ ) {
149
+ try {
150
+ const installed = await autoInstallFromCatalog(selector);
151
+ if (installed) {
152
+ log.info({ skillId: selector }, "Auto-installed skill from catalog");
153
+ loaded = loadSkillBySelector(selector);
154
+ }
155
+ } catch (err) {
156
+ const installError = err instanceof Error ? err.message : String(err);
157
+ log.warn(
158
+ { err, skillId: selector },
159
+ "Auto-install from catalog failed",
160
+ );
161
+ return {
162
+ content: `Error: skill "${selector}" was found in the catalog but installation failed: ${installError}`,
163
+ isError: true,
164
+ };
165
+ }
166
+ }
167
+
141
168
  if (!loaded.skill) {
142
169
  return {
143
170
  content: `Error: ${loaded.error ?? "Failed to load skill"}`,
@@ -160,10 +187,42 @@ export class SkillLoadTool implements Tool {
160
187
  // Load catalog for include validation and child metadata output
161
188
  let catalogIndex: Map<string, SkillSummary> | undefined;
162
189
  if (skill.includes && skill.includes.length > 0) {
163
- const catalog = loadSkillCatalog();
190
+ let catalog = loadSkillCatalog();
164
191
  catalogIndex = indexCatalogById(catalog);
165
192
 
166
- // Validate recursive includes (fail-closed)
193
+ // Auto-install missing includes before validation (max 5 rounds for transitive deps)
194
+ const MAX_INSTALL_ROUNDS = 5;
195
+ for (let round = 0; round < MAX_INSTALL_ROUNDS; round++) {
196
+ const missing = collectAllMissing(skill.id, catalogIndex);
197
+ if (missing.size === 0) break;
198
+
199
+ let installedAny = false;
200
+ for (const missingId of missing) {
201
+ try {
202
+ const installed = await autoInstallFromCatalog(missingId);
203
+ if (installed) {
204
+ log.info(
205
+ { skillId: missingId, parentSkillId: skill.id },
206
+ "Auto-installed missing include",
207
+ );
208
+ installedAny = true;
209
+ }
210
+ } catch (err) {
211
+ log.warn(
212
+ { err, skillId: missingId },
213
+ "Failed to auto-install missing include",
214
+ );
215
+ }
216
+ }
217
+
218
+ if (!installedAny) break; // Nothing could be installed, stop trying
219
+
220
+ // Reload catalog to pick up newly installed skills
221
+ catalog = loadSkillCatalog();
222
+ catalogIndex = indexCatalogById(catalog);
223
+ }
224
+
225
+ // Validate (fail-closed — catches genuinely missing deps + cycles)
167
226
  const validation = validateIncludes(skill.id, catalogIndex);
168
227
  if (!validation.ok) {
169
228
  if (validation.error === "missing") {
@@ -24,18 +24,6 @@ export interface WatchSession {
24
24
  timeoutHandle?: ReturnType<typeof setTimeout>;
25
25
  /** Guards against concurrent generateSummary calls */
26
26
  summaryInFlight?: boolean;
27
- /** Whether this session was started via ride shotgun (no live commentary) */
28
- isRideShotgun?: boolean;
29
- /** Learn mode records network traffic alongside screen observations */
30
- isLearnMode?: boolean;
31
- /** Domain filter for network recording in learn mode */
32
- targetDomain?: string;
33
- /** Recording ID for learn mode sessions */
34
- recordingId?: string;
35
- /** Path where the learn recording was successfully saved (undefined if save failed) */
36
- savedRecordingPath?: string;
37
- /** Reason the learn-mode bootstrap failed (CDP launch vs recorder attach) */
38
- bootstrapFailureReason?: string;
39
27
  }
40
28
 
41
29
  /** Module-level map of watch sessions keyed by watchId. */
@@ -12,11 +12,7 @@ import pino from "pino";
12
12
  import type { PrettyOptions } from "pino-pretty";
13
13
  import pinoPretty from "pino-pretty";
14
14
 
15
- import {
16
- getDebugMode,
17
- getDebugStdoutLogs,
18
- getLogStderr,
19
- } from "../config/env-registry.js";
15
+ import { getDebugStdoutLogs } from "../config/env-registry.js";
20
16
  import { logSerializers } from "./log-redact.js";
21
17
  import { getLogPath } from "./platform.js";
22
18
 
@@ -110,31 +106,18 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
110
106
  activeLogDate = today;
111
107
  activeLogFileConfig = config;
112
108
 
113
- const level = getDebugMode() ? "debug" : "info";
114
-
115
- if (getDebugMode()) {
116
- const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
117
- return pino(
118
- { name: "assistant", level, serializers: logSerializers },
119
- pino.multistream([
120
- { stream: fileStream, level: "info" as const },
121
- { stream: prettyStream, level: "debug" as const },
122
- ]),
123
- );
124
- }
125
-
126
109
  // When stdout is not a TTY (e.g. desktop app redirects to a hatch log file),
127
110
  // write to the rotating file only — the hatch log already captured early
128
111
  // startup output and echoing pino output there is unnecessary duplication.
129
112
  if (!process.stdout.isTTY) {
130
113
  return pino(
131
- { name: "assistant", level, serializers: logSerializers },
114
+ { name: "assistant", level: "info", serializers: logSerializers },
132
115
  fileStream,
133
116
  );
134
117
  }
135
118
 
136
119
  return pino(
137
- { name: "assistant", level, serializers: logSerializers },
120
+ { name: "assistant", level: "info", serializers: logSerializers },
138
121
  pino.multistream([
139
122
  { stream: fileStream, level: "info" as const },
140
123
  {
@@ -173,13 +156,11 @@ function getRootLogger(): pino.Logger {
173
156
  }
174
157
  if (!rootLogger) {
175
158
  const forceStderr =
176
- process.env.BUN_TEST === "1" ||
177
- process.env.NODE_ENV === "test" ||
178
- getLogStderr();
159
+ process.env.BUN_TEST === "1" || process.env.NODE_ENV === "test";
179
160
  if (forceStderr) {
180
161
  rootLogger = pino(
181
162
  {
182
- level: getDebugMode() ? "debug" : "info",
163
+ level: "info",
183
164
  serializers: logSerializers,
184
165
  },
185
166
  pino.destination(2),
@@ -208,17 +189,7 @@ function getRootLogger(): pino.Logger {
208
189
  prettyOpts({ destination: fileDest, colorize: false }),
209
190
  );
210
191
 
211
- if (getDebugMode()) {
212
- const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
213
- const multi = pino.multistream([
214
- { stream: fileStream, level: "info" as const },
215
- { stream: prettyStream, level: "debug" as const },
216
- ]);
217
- rootLogger = pino(
218
- { level: "debug", serializers: logSerializers },
219
- multi,
220
- );
221
- } else if (getDebugStdoutLogs()) {
192
+ if (getDebugStdoutLogs()) {
222
193
  rootLogger = pino(
223
194
  { level: "info", serializers: logSerializers },
224
195
  pino.multistream([
@@ -238,7 +209,7 @@ function getRootLogger(): pino.Logger {
238
209
  } catch {
239
210
  rootLogger = pino(
240
211
  {
241
- level: getDebugMode() ? "debug" : "info",
212
+ level: "info",
242
213
  serializers: logSerializers,
243
214
  },
244
215
  pinoPretty(prettyOpts({ destination: 2 })),
@@ -248,11 +219,6 @@ function getRootLogger(): pino.Logger {
248
219
  return rootLogger;
249
220
  }
250
221
 
251
- /** Returns true when VELLUM_DEBUG=1 is set. */
252
- export function isDebug(): boolean {
253
- return getDebugMode();
254
- }
255
-
256
222
  /**
257
223
  * Truncate a string for debug logging. Returns the original if under maxLen,
258
224
  * otherwise returns the first maxLen chars with a suffix indicating how much was cut.