@vellumai/assistant 0.3.19 → 0.3.21

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 (199) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -0,0 +1,122 @@
1
+ ---
2
+ name: "Voice Setup"
3
+ description: "Complete voice configuration in chat — PTT key, wake word, microphone permissions, ElevenLabs TTS, and troubleshooting"
4
+ user-invocable: true
5
+ metadata: {"vellum": {"emoji": "🎙️", "os": ["darwin"]}}
6
+ ---
7
+
8
+ You are helping the user set up and troubleshoot voice features (push-to-talk, wake word, text-to-speech) entirely within this conversation. Do NOT direct the user to the Settings page for initial setup — handle everything in-chat using the tools below.
9
+
10
+ ## Available Tools
11
+
12
+ - `voice_config_update` — Change any voice setting (PTT key, wake word enabled/keyword/timeout)
13
+ - `open_system_settings` — Open macOS System Settings to a specific privacy pane
14
+ - `navigate_settings_tab` — Open the Vellum settings panel to the Voice tab
15
+ - `credential_store` — Collect API keys securely (for ElevenLabs TTS)
16
+
17
+ ## Setup Flow
18
+
19
+ Walk the user through each section in order. Skip sections they don't need. Ask before proceeding to the next section.
20
+
21
+ ### 1. Microphone Permission
22
+
23
+ Check `<channel_capabilities>` for `microphone_permission_granted`.
24
+
25
+ **If `false` or missing:**
26
+ 1. Explain that macOS requires microphone permission for voice features.
27
+ 2. Use `open_system_settings` with `pane: "microphone"` to open the right System Settings pane.
28
+ 3. Tell the user: "I've opened System Settings to the Microphone section. Please toggle **Vellum Assistant** on, then come back here."
29
+ 4. After they confirm, verify by checking capabilities on the next turn.
30
+
31
+ **If `true`:** Tell them microphone is already granted and move on.
32
+
33
+ ### 2. Push-to-Talk Activation Key
34
+
35
+ Present common PTT key options:
36
+ - **Right Option (⌥)** — Default, good general choice
37
+ - **Fn** — Dedicated key on most Mac keyboards
38
+ - **Right Command (⌘)** — Easy to reach
39
+ - **Right Control (⌃)** — Familiar from gaming
40
+
41
+ Ask which key they prefer, then use `voice_config_update` with `setting: "activation_key"` and the chosen value.
42
+
43
+ **Common issues to mention:**
44
+ - If they pick a key that conflicts with their emoji picker (Fn or Globe on newer Macs), warn them and suggest an alternative.
45
+ - If they use a terminal app heavily, warn that some keys may be captured by the terminal.
46
+
47
+ ### 3. Wake Word (Optional)
48
+
49
+ Ask if they want to enable wake word detection (hands-free activation by saying a keyword).
50
+
51
+ **If yes:**
52
+ 1. Use `voice_config_update` with `setting: "wake_word_enabled"`, `value: true`.
53
+ 2. Ask what wake word they want. Common choices: "Hey Vellum", "Computer", "Jarvis", their assistant's name.
54
+ 3. Use `voice_config_update` with `setting: "wake_word_keyword"` and their chosen word.
55
+ 4. Ask about timeout (how long the mic stays active after wake word). Options: 5s, 10s (default), 15s, 30s, 60s.
56
+ 5. Use `voice_config_update` with `setting: "wake_word_timeout"` and their chosen value.
57
+
58
+ **Speech Recognition permission:** Wake word requires Speech Recognition access. Check capabilities — if not granted, use `open_system_settings` with `pane: "speech_recognition"`.
59
+
60
+ ### 4. Text-to-Speech / ElevenLabs (Optional)
61
+
62
+ Ask if they want high-quality text-to-speech voices via ElevenLabs (optional — standard TTS works without it).
63
+
64
+ **If yes:**
65
+ 1. Tell them they need an ElevenLabs API key. They can get one at https://elevenlabs.io (free tier available).
66
+ 2. Use `credential_store` with `action: "prompt"`, `service: "elevenlabs"`, `field: "api_key"` to show a secure input dialog.
67
+ 3. After the key is stored, confirm success.
68
+
69
+ ### 5. Verification
70
+
71
+ After setup is complete:
72
+ 1. Summarize what was configured.
73
+ 2. Suggest they test by pressing their PTT key (or saying their wake word) and speaking.
74
+ 3. Offer to open the Voice settings tab if they want to review: use `navigate_settings_tab` with `tab: "Voice"`.
75
+
76
+ ## Troubleshooting Decision Trees
77
+
78
+ When the user reports a problem, follow the appropriate decision tree:
79
+
80
+ ### "PTT isn't working" / "Can't record"
81
+ 1. **Microphone permission** — Check `microphone_permission_granted` in capabilities. If false, guide through granting it.
82
+ 2. **Key check** — Ask what key they're using. Confirm it matches their configured PTT key.
83
+ 3. **Emoji picker conflict** — On newer Macs, Fn/Globe opens the emoji picker. If they're using Fn, suggest switching to Right Option or Right Command.
84
+ 4. **Speech Recognition permission** — Some voice features need this. Use `open_system_settings` with `pane: "speech_recognition"`.
85
+ 5. **App focus** — PTT may not work when Vellum is not the frontmost app or if another app has captured the key.
86
+
87
+ ### "Recording but no text" / "Transcription not working"
88
+ 1. **Speech Recognition permission** — Must be granted for transcription.
89
+ 2. **Microphone input** — Ask if they see the recording indicator. If yes, the mic works but transcription is failing.
90
+ 3. **Locale/language** — Speech recognition works best with the system language. Ask if they're speaking in a different language.
91
+ 4. **Background noise** — Excessive noise can prevent transcription. Suggest a quieter environment or a closer microphone.
92
+
93
+ ### "Wake word not detecting"
94
+ 1. **Enabled check** — Confirm wake word is enabled in their settings.
95
+ 2. **Keyword** — Confirm what keyword they're using. Shorter or common words may have lower accuracy.
96
+ 3. **Ambient noise** — Wake word detection is sensitive to background noise.
97
+ 4. **Permissions** — Both Microphone and Speech Recognition permissions are required.
98
+ 5. **Timeout** — If wake word activates but cuts off too quickly, increase the timeout.
99
+
100
+ ### "Changed a setting but it didn't work"
101
+ 1. **IPC broadcast** — The setting should take effect immediately. If it didn't, suggest restarting the assistant.
102
+ 2. **Verify** — Open the Voice settings tab with `navigate_settings_tab` to confirm the setting was persisted.
103
+
104
+ ## Deep Debugging
105
+
106
+ For persistent issues, suggest checking system logs:
107
+
108
+ ```bash
109
+ log stream --predicate 'subsystem == "com.vellum.assistant"' --level debug
110
+ ```
111
+
112
+ Key log categories:
113
+ - `voice` — PTT activation, recording state
114
+ - `wake-word` — Wake word detection events
115
+ - `speech` — Speech recognition results
116
+
117
+ ## Rules
118
+
119
+ - Always handle setup conversationally in-chat. Do NOT tell the user to go to Settings for initial configuration.
120
+ - Use `navigate_settings_tab` only for review/verification after in-chat setup, not as the primary setup method.
121
+ - Be concise. Don't explain every option exhaustively — present the most common choices and let the user ask for more.
122
+ - If a permission is denied, acknowledge it gracefully and explain what features won't work without it.
@@ -124,7 +124,7 @@ export const ContextWindowConfigSchema = z.object({
124
124
  .number({ error: 'contextWindow.maxInputTokens must be a number' })
125
125
  .int('contextWindow.maxInputTokens must be an integer')
126
126
  .positive('contextWindow.maxInputTokens must be a positive integer')
127
- .default(180000),
127
+ .default(200000),
128
128
  targetInputTokens: z
129
129
  .number({ error: 'contextWindow.targetInputTokens must be a number' })
130
130
  .int('contextWindow.targetInputTokens must be an integer')
@@ -124,6 +124,16 @@ export function getEnableMonitoring(): boolean {
124
124
  return flag('VELLUM_ENABLE_MONITORING');
125
125
  }
126
126
 
127
+ /**
128
+ * IS_CONTAINERIZED — boolean, default: false
129
+ * When true, indicates the assistant is running inside a container (e.g. Docker).
130
+ * Any new data that needs to survive restarts must be written to BASE_DATA_DIR,
131
+ * which is mapped to a persistent volume.
132
+ */
133
+ export function getIsContainerized(): boolean {
134
+ return flag('IS_CONTAINERIZED');
135
+ }
136
+
127
137
  // ── Known env var names ──────────────────────────────────────────────────────
128
138
 
129
139
  /**
@@ -0,0 +1,61 @@
1
+ {
2
+ "version": 1,
3
+ "flags": [
4
+ {
5
+ "id": "sms",
6
+ "scope": "assistant",
7
+ "key": "feature_flags.sms.enabled",
8
+ "label": "SMS",
9
+ "description": "Show SMS setup in Settings > Connect and enable the SMS setup skill",
10
+ "defaultEnabled": true
11
+ },
12
+ {
13
+ "id": "hatch-new-assistant",
14
+ "scope": "assistant",
15
+ "key": "feature_flags.hatch-new-assistant.enabled",
16
+ "label": "Hatch New Assistant",
17
+ "description": "Show Hatch New Assistant action in Settings > Account",
18
+ "defaultEnabled": true
19
+ },
20
+ {
21
+ "id": "guardian-verify-setup",
22
+ "scope": "assistant",
23
+ "key": "feature_flags.guardian-verify-setup.enabled",
24
+ "label": "Guardian Verification Setup",
25
+ "description": "Enable guardian verification routing in the system prompt",
26
+ "defaultEnabled": true
27
+ },
28
+ {
29
+ "id": "browser",
30
+ "scope": "assistant",
31
+ "key": "feature_flags.browser.enabled",
32
+ "label": "Browser",
33
+ "description": "Enable browser skill prerequisites section in the system prompt",
34
+ "defaultEnabled": true
35
+ },
36
+ {
37
+ "id": "twitter",
38
+ "scope": "assistant",
39
+ "key": "feature_flags.twitter.enabled",
40
+ "label": "Twitter",
41
+ "description": "Enable X (Twitter) skill section in the system prompt",
42
+ "defaultEnabled": true
43
+ },
44
+ {
45
+ "id": "user-hosted-enabled",
46
+ "scope": "macos",
47
+ "key": "user_hosted_enabled",
48
+ "label": "User Hosted Enabled",
49
+ "description": "Enable user-hosted onboarding flow",
50
+ "defaultEnabled": false
51
+ },
52
+ {
53
+ "id": "local-http-enabled",
54
+ "scope": "macos",
55
+ "key": "local_http_enabled",
56
+ "label": "Local HTTP Enabled",
57
+ "description": "Enable local HTTP transport mode in macOS app",
58
+ "defaultEnabled": false
59
+ }
60
+ ]
61
+ }
@@ -1,4 +1,5 @@
1
- import { existsSync, readFileSync, statSync,writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
2
3
 
3
4
  import { deleteSecureKey,getSecureKey, setSecureKey } from '../security/secure-keys.js';
4
5
  import { ConfigError } from '../util/errors.js';
@@ -113,6 +114,7 @@ export function loadConfig(): AssistantConfig {
113
114
  const configPath = getConfigPath();
114
115
 
115
116
  let fileConfig: Record<string, unknown> = {};
117
+ let configFileExisted = true;
116
118
  if (existsSync(configPath)) {
117
119
  const mode = statSync(configPath).mode;
118
120
  if (mode & 0o077) {
@@ -127,6 +129,8 @@ export function loadConfig(): AssistantConfig {
127
129
  } catch (err) {
128
130
  throw new ConfigError(`Failed to parse config at ${configPath}: ${err}`);
129
131
  }
132
+ } else {
133
+ configFileExisted = false;
130
134
  }
131
135
 
132
136
  // Pre-validate apiKeys shape before migration (must be a plain object)
@@ -182,6 +186,23 @@ export function loadConfig(): AssistantConfig {
182
186
  // Validate and apply defaults via Zod schema
183
187
  const config = validateWithSchema(fileConfig);
184
188
 
189
+ // If the config file didn't exist, write the full defaults to disk so
190
+ // users can discover and edit all available options.
191
+ if (!configFileExisted) {
192
+ try {
193
+ const dir = dirname(configPath);
194
+ if (!existsSync(dir)) {
195
+ mkdirSync(dir, { recursive: true });
196
+ }
197
+ // Strip apiKeys (managed in secure storage) and dataDir (runtime-derived)
198
+ const { apiKeys: _, dataDir: _d, ...persistable } = config;
199
+ writeFileSync(configPath, JSON.stringify(persistable, null, 2) + '\n');
200
+ log.info('Wrote default config to %s', configPath);
201
+ } catch (err) {
202
+ log.warn({ err }, 'Failed to write default config file');
203
+ }
204
+ }
205
+
185
206
  // Set cached before secure-key/env overrides so re-entrant calls
186
207
  // return the in-flight config instead of bare defaults.
187
208
  cached = config;
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+
3
+ const McpStdioTransportSchema = z.object({
4
+ type: z.literal('stdio'),
5
+ command: z.string({ error: 'mcp transport command must be a string' }),
6
+ args: z.array(z.string()).default([]),
7
+ env: z.record(z.string(), z.string()).optional(),
8
+ });
9
+
10
+ const McpSseTransportSchema = z.object({
11
+ type: z.literal('sse'),
12
+ url: z.string({ error: 'mcp transport url must be a string' }),
13
+ headers: z.record(z.string(), z.string()).optional(),
14
+ });
15
+
16
+ const McpStreamableHttpTransportSchema = z.object({
17
+ type: z.literal('streamable-http'),
18
+ url: z.string({ error: 'mcp transport url must be a string' }),
19
+ headers: z.record(z.string(), z.string()).optional(),
20
+ });
21
+
22
+ export const McpTransportSchema = z.discriminatedUnion('type', [
23
+ McpStdioTransportSchema,
24
+ McpSseTransportSchema,
25
+ McpStreamableHttpTransportSchema,
26
+ ]);
27
+
28
+ export const McpServerConfigSchema = z.object({
29
+ transport: McpTransportSchema,
30
+ enabled: z.boolean({ error: 'mcp server enabled must be a boolean' }).default(true),
31
+ defaultRiskLevel: z.enum(['low', 'medium', 'high'], {
32
+ error: 'mcp server defaultRiskLevel must be one of: low, medium, high',
33
+ }).default('high'),
34
+ maxTools: z.number({ error: 'mcp server maxTools must be a number' }).int().positive().default(20),
35
+ allowedTools: z.array(z.string()).optional(),
36
+ blockedTools: z.array(z.string()).optional(),
37
+ });
38
+
39
+ export const McpConfigSchema = z.object({
40
+ servers: z.record(z.string(), McpServerConfigSchema).default({} as any),
41
+ globalMaxTools: z.number({ error: 'mcp globalMaxTools must be a number' }).int().positive().default(50),
42
+ });
43
+
44
+ export type McpTransport = z.infer<typeof McpTransportSchema>;
45
+ export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;
46
+ export type McpConfig = z.infer<typeof McpConfigSchema>;
@@ -1,48 +1,9 @@
1
1
  import { z } from 'zod';
2
2
 
3
- const VALID_SANDBOX_BACKENDS = ['native', 'docker'] as const;
4
- const VALID_DOCKER_NETWORKS = ['none', 'bridge'] as const;
5
-
6
- export const DockerConfigSchema = z.object({
7
- image: z
8
- .string({ error: 'sandbox.docker.image must be a string' })
9
- .default('vellum-sandbox:latest'),
10
- shell: z
11
- .string({ error: 'sandbox.docker.shell must be a string' })
12
- .default('bash'),
13
- cpus: z
14
- .number({ error: 'sandbox.docker.cpus must be a number' })
15
- .finite('sandbox.docker.cpus must be finite')
16
- .positive('sandbox.docker.cpus must be a positive number')
17
- .default(1),
18
- memoryMb: z
19
- .number({ error: 'sandbox.docker.memoryMb must be a number' })
20
- .int('sandbox.docker.memoryMb must be an integer')
21
- .positive('sandbox.docker.memoryMb must be a positive integer')
22
- .default(512),
23
- pidsLimit: z
24
- .number({ error: 'sandbox.docker.pidsLimit must be a number' })
25
- .int('sandbox.docker.pidsLimit must be an integer')
26
- .positive('sandbox.docker.pidsLimit must be a positive integer')
27
- .default(256),
28
- network: z
29
- .enum(VALID_DOCKER_NETWORKS, {
30
- error: `sandbox.docker.network must be one of: ${VALID_DOCKER_NETWORKS.join(', ')}`,
31
- })
32
- .default('none'),
33
- });
34
-
35
3
  export const SandboxConfigSchema = z.object({
36
4
  enabled: z
37
5
  .boolean({ error: 'sandbox.enabled must be a boolean' })
38
6
  .default(true),
39
- backend: z
40
- .enum(VALID_SANDBOX_BACKENDS, {
41
- error: `sandbox.backend must be one of: ${VALID_SANDBOX_BACKENDS.join(', ')}`,
42
- })
43
- .default('docker'),
44
- docker: DockerConfigSchema.default({} as any),
45
7
  });
46
8
 
47
9
  export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
48
- export type DockerConfig = z.infer<typeof DockerConfigSchema>;
@@ -109,13 +109,21 @@ export {
109
109
  NotificationsConfigSchema,
110
110
  } from './notifications-schema.js';
111
111
  export type {
112
- DockerConfig,
113
112
  SandboxConfig,
114
113
  } from './sandbox-schema.js';
115
114
  export {
116
- DockerConfigSchema,
117
115
  SandboxConfigSchema,
118
116
  } from './sandbox-schema.js';
117
+ export type {
118
+ McpConfig,
119
+ McpServerConfig,
120
+ McpTransport,
121
+ } from './mcp-schema.js';
122
+ export {
123
+ McpConfigSchema,
124
+ McpServerConfigSchema,
125
+ McpTransportSchema,
126
+ } from './mcp-schema.js';
119
127
  export type {
120
128
  RemotePolicyConfig,
121
129
  RemoteProviderConfig,
@@ -154,6 +162,7 @@ import {
154
162
  TimeoutConfigSchema,
155
163
  UiConfigSchema,
156
164
  } from './core-schema.js';
165
+ import { McpConfigSchema } from './mcp-schema.js';
157
166
  import { MemoryConfigSchema } from './memory-schema.js';
158
167
  import { NotificationsConfigSchema } from './notifications-schema.js';
159
168
  import { SandboxConfigSchema } from './sandbox-schema.js';
@@ -215,6 +224,7 @@ export const AssistantConfigSchema = z.object({
215
224
  .default([]),
216
225
  heartbeat: HeartbeatConfigSchema.default({} as any),
217
226
  swarm: SwarmConfigSchema.default({} as any),
227
+ mcp: McpConfigSchema.default({} as any),
218
228
  skills: SkillsConfigSchema.default({} as any),
219
229
  workspaceGit: WorkspaceGitConfigSchema.default({} as any),
220
230
  calls: CallsConfigSchema.default({} as any),
@@ -224,6 +234,12 @@ export const AssistantConfigSchema = z.object({
224
234
  daemon: DaemonConfigSchema.default({} as any),
225
235
  notifications: NotificationsConfigSchema.default({} as any),
226
236
  ui: UiConfigSchema.default({} as any),
237
+ featureFlags: z
238
+ .record(z.string(), z.boolean({ error: 'featureFlags values must be booleans' }))
239
+ .default({} as any),
240
+ assistantFeatureFlagValues: z
241
+ .record(z.string(), z.boolean({ error: 'assistantFeatureFlagValues values must be booleans' }))
242
+ .optional(),
227
243
  }).superRefine((config, ctx) => {
228
244
  if (config.contextWindow?.targetInputTokens != null && config.contextWindow?.maxInputTokens != null &&
229
245
  config.contextWindow.targetInputTokens >= config.contextWindow.maxInputTokens) {
@@ -1,3 +1,4 @@
1
+ import { isAssistantFeatureFlagEnabled } from './assistant-feature-flags.js';
1
2
  import type { AssistantConfig, SkillEntryConfig } from './schema.js';
2
3
  import type { SkillSummary } from './skills.js';
3
4
  import { checkSkillRequirements } from './skills.js';
@@ -12,6 +13,34 @@ export interface ResolvedSkill {
12
13
  configEntry?: SkillEntryConfig;
13
14
  }
14
15
 
16
+ // Skill IDs whose feature flag key differs from the default
17
+ // `feature_flags.<skillId>.enabled` derivation. The sms-setup skill is
18
+ // gated by the `sms` flag so that the macOS Settings SMS toggle controls
19
+ // both the channel UI and the setup skill.
20
+ const SKILL_FLAG_KEY_OVERRIDES: Record<string, string> = {
21
+ 'sms-setup': 'feature_flags.sms.enabled',
22
+ };
23
+
24
+ /**
25
+ * Derive the feature flag key for a given skill ID, respecting overrides.
26
+ *
27
+ * Exported so other modules (system-prompt, session-skill-tools, skill loader)
28
+ * can perform the same mapping without duplicating the override table.
29
+ */
30
+ export function skillFlagKey(skillId: string): string {
31
+ return SKILL_FLAG_KEY_OVERRIDES[skillId] ?? `feature_flags.${skillId}.enabled`;
32
+ }
33
+
34
+ /**
35
+ * @deprecated Use `isAssistantFeatureFlagEnabled` from `./assistant-feature-flags.js` instead.
36
+ *
37
+ * Thin backward-compatible wrapper that delegates to the canonical resolver.
38
+ * Kept to avoid breaking existing call sites during migration.
39
+ */
40
+ export function isSkillFeatureEnabled(skillId: string, config: AssistantConfig): boolean {
41
+ return isAssistantFeatureFlagEnabled(skillFlagKey(skillId), config);
42
+ }
43
+
15
44
  export function resolveSkillStates(
16
45
  catalog: SkillSummary[],
17
46
  config: AssistantConfig,
@@ -20,6 +49,11 @@ export function resolveSkillStates(
20
49
  const { entries, allowBundled } = config.skills ?? { entries: {}, allowBundled: null };
21
50
 
22
51
  for (const skill of catalog) {
52
+ // Assistant feature flag gate: if the flag is explicitly OFF, skip this skill entirely
53
+ if (!isAssistantFeatureFlagEnabled(skillFlagKey(skill.id), config)) {
54
+ continue;
55
+ }
56
+
23
57
  // Filter bundled skills by allowlist
24
58
  if (skill.source === 'bundled' && allowBundled != null && !allowBundled.includes(skill.id)) {
25
59
  continue;
@@ -28,7 +28,6 @@ export const RemoteProvidersConfigSchema = z.object({
28
28
  clawhub: RemoteProviderConfigSchema.default({} as any),
29
29
  });
30
30
 
31
- const VALID_SKILLS_SH_RISK_LEVELS = ['safe', 'low', 'medium', 'high', 'critical', 'unknown'] as const;
32
31
  // 'unknown' is valid as a risk label on a skill but not as a threshold — setting the threshold
33
32
  // to 'unknown' would silently disable fail-closed behavior since nothing can exceed it.
34
33
  const VALID_MAX_RISK_LEVELS = ['safe', 'low', 'medium', 'high', 'critical'] as const;
@@ -13,12 +13,21 @@ const log = getLogger('skills');
13
13
 
14
14
  // ─── New interfaces for extended skill metadata ──────────────────────────────
15
15
 
16
+ export interface SkillCliSpec {
17
+ /** CLI command name (e.g. "doordash"). Used as the launcher script name in ~/.vellum/bin/. */
18
+ command: string;
19
+ /** Entry point filename relative to the skill directory (e.g. "doordash-entry.ts"). */
20
+ entry: string;
21
+ }
22
+
16
23
  export interface VellumMetadata {
17
24
  emoji?: string;
18
25
  os?: string[];
19
26
  requires?: SkillRequirements;
20
27
  primaryEnv?: string;
21
28
  install?: InstallerSpec[];
29
+ /** Declares a standalone CLI entry point for this skill. */
30
+ cli?: SkillCliSpec;
22
31
  }
23
32
 
24
33
  export interface SkillRequirements {