@vellumai/assistant 0.3.19 → 0.3.20

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 (189) hide show
  1. package/ARCHITECTURE.md +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/docs/architecture/integrations.md +7 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  7. package/src/__tests__/approval-primitive.test.ts +540 -0
  8. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  9. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  11. package/src/__tests__/call-controller.test.ts +439 -108
  12. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  13. package/src/__tests__/cli.test.ts +42 -1
  14. package/src/__tests__/config-schema.test.ts +11 -127
  15. package/src/__tests__/config-watcher.test.ts +0 -8
  16. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  17. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  18. package/src/__tests__/diff.test.ts +22 -0
  19. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  20. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  21. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  22. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  23. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  24. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  25. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  26. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  27. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  28. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  29. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  30. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  31. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  32. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  33. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  34. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  35. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  36. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  37. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  38. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  39. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  40. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  41. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  42. package/src/__tests__/system-prompt.test.ts +1 -1
  43. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  44. package/src/__tests__/terminal-tools.test.ts +2 -93
  45. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  46. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  47. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  48. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  49. package/src/agent/loop.ts +36 -1
  50. package/src/approvals/approval-primitive.ts +381 -0
  51. package/src/approvals/guardian-decision-primitive.ts +191 -0
  52. package/src/calls/call-controller.ts +252 -209
  53. package/src/calls/call-domain.ts +44 -6
  54. package/src/calls/guardian-dispatch.ts +48 -0
  55. package/src/calls/types.ts +1 -1
  56. package/src/calls/voice-session-bridge.ts +46 -30
  57. package/src/cli/core-commands.ts +0 -4
  58. package/src/cli.ts +76 -34
  59. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  60. package/src/config/assistant-feature-flags.ts +162 -0
  61. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  62. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  63. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  64. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  65. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  66. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  67. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  68. package/src/config/core-schema.ts +1 -1
  69. package/src/config/env-registry.ts +10 -0
  70. package/src/config/feature-flag-registry.json +61 -0
  71. package/src/config/loader.ts +22 -1
  72. package/src/config/sandbox-schema.ts +0 -39
  73. package/src/config/schema.ts +6 -2
  74. package/src/config/skill-state.ts +34 -0
  75. package/src/config/skills-schema.ts +0 -1
  76. package/src/config/skills.ts +9 -0
  77. package/src/config/system-prompt.ts +110 -46
  78. package/src/config/templates/SOUL.md +1 -1
  79. package/src/config/types.ts +19 -1
  80. package/src/config/vellum-skills/catalog.json +1 -1
  81. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  82. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  83. package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
  84. package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  86. package/src/daemon/config-watcher.ts +0 -1
  87. package/src/daemon/daemon-control.ts +1 -1
  88. package/src/daemon/guardian-invite-intent.ts +124 -0
  89. package/src/daemon/handlers/avatar.ts +68 -0
  90. package/src/daemon/handlers/browser.ts +2 -2
  91. package/src/daemon/handlers/guardian-actions.ts +120 -0
  92. package/src/daemon/handlers/index.ts +4 -0
  93. package/src/daemon/handlers/sessions.ts +19 -0
  94. package/src/daemon/handlers/shared.ts +3 -1
  95. package/src/daemon/install-cli-launchers.ts +58 -13
  96. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  97. package/src/daemon/ipc-contract/sessions.ts +8 -2
  98. package/src/daemon/ipc-contract/settings.ts +25 -2
  99. package/src/daemon/ipc-contract-inventory.json +10 -0
  100. package/src/daemon/ipc-contract.ts +4 -0
  101. package/src/daemon/lifecycle.ts +6 -2
  102. package/src/daemon/main.ts +1 -0
  103. package/src/daemon/server.ts +1 -0
  104. package/src/daemon/session-lifecycle.ts +52 -7
  105. package/src/daemon/session-memory.ts +45 -0
  106. package/src/daemon/session-process.ts +258 -432
  107. package/src/daemon/session-runtime-assembly.ts +12 -0
  108. package/src/daemon/session-skill-tools.ts +14 -1
  109. package/src/daemon/session-tool-setup.ts +5 -0
  110. package/src/daemon/session.ts +11 -0
  111. package/src/daemon/tool-side-effects.ts +35 -9
  112. package/src/index.ts +0 -2
  113. package/src/memory/conversation-display-order-migration.ts +44 -0
  114. package/src/memory/conversation-queries.ts +2 -0
  115. package/src/memory/conversation-store.ts +91 -0
  116. package/src/memory/db-init.ts +5 -1
  117. package/src/memory/embedding-local.ts +13 -8
  118. package/src/memory/guardian-action-store.ts +125 -2
  119. package/src/memory/ingress-invite-store.ts +95 -1
  120. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  121. package/src/memory/migrations/index.ts +2 -1
  122. package/src/memory/schema.ts +5 -1
  123. package/src/memory/scoped-approval-grants.ts +14 -5
  124. package/src/messaging/providers/slack/client.ts +12 -0
  125. package/src/messaging/providers/slack/types.ts +5 -0
  126. package/src/notifications/decision-engine.ts +49 -12
  127. package/src/notifications/emit-signal.ts +7 -0
  128. package/src/notifications/signal.ts +7 -0
  129. package/src/notifications/thread-seed-composer.ts +2 -1
  130. package/src/runtime/channel-approval-types.ts +16 -6
  131. package/src/runtime/channel-approvals.ts +19 -15
  132. package/src/runtime/channel-invite-transport.ts +85 -0
  133. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  134. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  135. package/src/runtime/guardian-action-message-composer.ts +30 -0
  136. package/src/runtime/guardian-decision-types.ts +91 -0
  137. package/src/runtime/http-server.ts +23 -1
  138. package/src/runtime/ingress-service.ts +22 -0
  139. package/src/runtime/invite-redemption-service.ts +181 -0
  140. package/src/runtime/invite-redemption-templates.ts +39 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  143. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  144. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  145. package/src/runtime/routes/pairing-routes.ts +4 -0
  146. package/src/security/encrypted-store.ts +31 -17
  147. package/src/security/keychain.ts +176 -2
  148. package/src/security/secure-keys.ts +97 -0
  149. package/src/security/tool-approval-digest.ts +1 -1
  150. package/src/tools/browser/browser-execution.ts +2 -2
  151. package/src/tools/browser/browser-manager.ts +46 -32
  152. package/src/tools/browser/browser-screencast.ts +2 -2
  153. package/src/tools/calls/call-start.ts +1 -1
  154. package/src/tools/executor.ts +22 -17
  155. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  156. package/src/tools/skills/load.ts +22 -8
  157. package/src/tools/system/avatar-generator.ts +119 -0
  158. package/src/tools/system/navigate-settings.ts +65 -0
  159. package/src/tools/system/open-system-settings.ts +75 -0
  160. package/src/tools/system/voice-config.ts +121 -32
  161. package/src/tools/terminal/backends/native.ts +40 -19
  162. package/src/tools/terminal/backends/types.ts +3 -3
  163. package/src/tools/terminal/parser.ts +1 -1
  164. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  165. package/src/tools/terminal/sandbox.ts +1 -12
  166. package/src/tools/terminal/shell.ts +3 -31
  167. package/src/tools/tool-approval-handler.ts +141 -3
  168. package/src/tools/tool-manifest.ts +6 -0
  169. package/src/tools/types.ts +6 -0
  170. package/src/util/diff.ts +36 -13
  171. package/Dockerfile.sandbox +0 -5
  172. package/src/__tests__/doordash-client.test.ts +0 -187
  173. package/src/__tests__/doordash-session.test.ts +0 -154
  174. package/src/__tests__/signup-e2e.test.ts +0 -354
  175. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  176. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  177. package/src/cli/doordash.ts +0 -1057
  178. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  179. package/src/config/templates/LOOKS.md +0 -25
  180. package/src/doordash/cart-queries.ts +0 -787
  181. package/src/doordash/client.ts +0 -1016
  182. package/src/doordash/order-queries.ts +0 -85
  183. package/src/doordash/queries.ts +0 -13
  184. package/src/doordash/query-extractor.ts +0 -94
  185. package/src/doordash/search-queries.ts +0 -203
  186. package/src/doordash/session.ts +0 -84
  187. package/src/doordash/store-queries.ts +0 -246
  188. package/src/doordash/types.ts +0 -367
  189. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -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,11 +109,9 @@ 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';
119
117
  export type {
@@ -224,6 +222,12 @@ export const AssistantConfigSchema = z.object({
224
222
  daemon: DaemonConfigSchema.default({} as any),
225
223
  notifications: NotificationsConfigSchema.default({} as any),
226
224
  ui: UiConfigSchema.default({} as any),
225
+ featureFlags: z
226
+ .record(z.string(), z.boolean({ error: 'featureFlags values must be booleans' }))
227
+ .default({} as any),
228
+ assistantFeatureFlagValues: z
229
+ .record(z.string(), z.boolean({ error: 'assistantFeatureFlagValues values must be booleans' }))
230
+ .optional(),
227
231
  }).superRefine((config, ctx) => {
228
232
  if (config.contextWindow?.targetInputTokens != null && config.contextWindow?.maxInputTokens != null &&
229
233
  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 {
@@ -6,13 +6,16 @@ import { getParentalControlSettings } from '../security/parental-control-store.j
6
6
  import { listCredentialMetadata } from '../tools/credentials/metadata-store.js';
7
7
  import { getLogger } from '../util/logger.js';
8
8
  import { getWorkspaceDir, getWorkspacePromptPath, isMacOS } from '../util/platform.js';
9
+ import { isAssistantFeatureFlagEnabled } from './assistant-feature-flags.js';
10
+ import { getBaseDataDir, getIsContainerized } from './env-registry.js';
9
11
  import { getConfig } from './loader.js';
12
+ import { skillFlagKey } from './skill-state.js';
10
13
  import { loadSkillCatalog, type SkillSummary } from './skills.js';
11
14
  import { resolveUserReference } from './user-reference.js';
12
15
 
13
16
  const log = getLogger('system-prompt');
14
17
 
15
- const PROMPT_FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'LOOKS.md'] as const;
18
+ const PROMPT_FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md'] as const;
16
19
 
17
20
  /**
18
21
  * Copy template prompt files into the data directory if they don't already exist.
@@ -91,14 +94,11 @@ export function buildSystemPrompt(tier: ResponseTier = 'high'): string {
91
94
  const userPath = getWorkspacePromptPath('USER.md');
92
95
  const bootstrapPath = getWorkspacePromptPath('BOOTSTRAP.md');
93
96
 
94
- const looksPath = getWorkspacePromptPath('LOOKS.md');
95
-
96
97
  const updatesPath = getWorkspacePromptPath('UPDATES.md');
97
98
 
98
99
  const soul = readPromptFile(soulPath);
99
100
  const identity = readPromptFile(identityPath);
100
101
  const user = readPromptFile(userPath);
101
- const looks = readPromptFile(looksPath);
102
102
  const bootstrap = readPromptFile(bootstrapPath);
103
103
  const updates = readPromptFile(updatesPath);
104
104
 
@@ -107,7 +107,6 @@ export function buildSystemPrompt(tier: ResponseTier = 'high'): string {
107
107
  if (identity) parts.push(identity);
108
108
  if (soul) parts.push(soul);
109
109
  if (user) parts.push(user);
110
- if (looks) parts.push(looks);
111
110
  if (bootstrap) {
112
111
  parts.push(
113
112
  '# First-Run Ritual\n\n'
@@ -130,6 +129,7 @@ export function buildSystemPrompt(tier: ResponseTier = 'high'): string {
130
129
  '- When you are satisfied all updates have been actioned or communicated, delete `UPDATES.md` to signal completion.',
131
130
  ].join('\n'));
132
131
  }
132
+ if (getIsContainerized()) parts.push(buildContainerizedSection());
133
133
  parts.push(buildConfigSection());
134
134
  parts.push(buildPostToolResponseSection());
135
135
  parts.push(buildExternalCommsIdentitySection());
@@ -140,11 +140,15 @@ export function buildSystemPrompt(tier: ResponseTier = 'high'): string {
140
140
 
141
141
  // ── Extended sections (medium + high) ──
142
142
  if (tier !== 'low') {
143
+ const config = getConfig();
143
144
  parts.push(buildToolPermissionSection());
144
145
  parts.push(buildTaskScheduleReminderRoutingSection());
145
- parts.push(buildGuardianVerificationRoutingSection());
146
+ if (isAssistantFeatureFlagEnabled('feature_flags.guardian-verify-setup.enabled', config)) {
147
+ parts.push(buildGuardianVerificationRoutingSection());
148
+ }
146
149
  parts.push(buildAttachmentSection());
147
150
  parts.push(buildInChatConfigurationSection());
151
+ parts.push(buildVoiceSetupRoutingSection());
148
152
  parts.push(buildChannelCommandIntentSection());
149
153
  }
150
154
 
@@ -326,6 +330,50 @@ function buildInChatConfigurationSection(): string {
326
330
  '**After saving a value**, confirm success with a message like: "Great, saved! You can always update this from the Settings page."',
327
331
  '',
328
332
  '**Never tell the user to go to Settings to enter a value.** The Settings page is for reviewing and updating existing configuration, not for initial setup. Always prefer the in-chat flow for first-time configuration.',
333
+ '',
334
+ '### Avatar Customisation',
335
+ '',
336
+ 'You can change your avatar appearance using the `set_avatar` tool. When the user asks to change, update, or customise your avatar, use `set_avatar` with a `description` parameter describing the desired appearance (e.g. "a friendly purple cat with green eyes wearing a tiny hat"). The tool generates an avatar image via Gemini and updates all connected clients automatically. Requires a Gemini API key (the same one used for image generation).',
337
+ '',
338
+ '**After generating a new avatar**, always update the `## Avatar` section in `IDENTITY.md` with a brief description of the current avatar appearance. This ensures you remember what you look like across sessions. Example:',
339
+ '```',
340
+ '## Avatar',
341
+ 'A friendly purple cat with green eyes wearing a tiny hat',
342
+ '```',
343
+ ].join('\n');
344
+ }
345
+
346
+ export function buildVoiceSetupRoutingSection(): string {
347
+ return [
348
+ '## Routing: Voice Setup & Troubleshooting',
349
+ '',
350
+ 'Voice features include push-to-talk (PTT), wake word detection, and text-to-speech.',
351
+ '',
352
+ '### Quick changes — use `voice_config_update` directly',
353
+ '- "Change my PTT key to ctrl" — call `voice_config_update` with `setting: "activation_key"`',
354
+ '- "Enable wake word" — call `voice_config_update` with `setting: "wake_word_enabled"`, `value: true`',
355
+ '- "Set my wake word to jarvis" — call `voice_config_update` with `setting: "wake_word_keyword"`',
356
+ '- "Set wake word timeout to 30 seconds" — call `voice_config_update` with `setting: "wake_word_timeout"`',
357
+ '',
358
+ 'For simple setting changes, use the tool directly without loading the voice-setup skill.',
359
+ '',
360
+ '### Guided setup or troubleshooting — load the voice-setup skill',
361
+ 'Load with: `skill_load` using `skill: "voice-setup"`',
362
+ '',
363
+ '**Trigger phrases:**',
364
+ '- "Help me set up voice"',
365
+ '- "Set up push-to-talk"',
366
+ '- "Configure voice / PTT / wake word"',
367
+ '- "PTT isn\'t working" / "push-to-talk not working"',
368
+ '- "Recording but no text"',
369
+ '- "Wake word not detecting"',
370
+ '- "Microphone not working"',
371
+ '- "Set up ElevenLabs" / "configure TTS"',
372
+ '',
373
+ '### Disambiguation',
374
+ '- Voice setup (this skill) = **local PTT, wake word, microphone permissions** on the Mac desktop app.',
375
+ '- Phone calls skill = **Twilio-powered voice calls** over the phone network. Completely separate.',
376
+ '- If the user says "voice" in the context of phone calls or Twilio, load `phone-calls` instead.',
329
377
  ].join('\n');
330
378
  }
331
379
 
@@ -411,12 +459,6 @@ export function buildChannelAwarenessSection(): string {
411
459
  '- When the user asks about voice input or push-to-talk settings, use the tool to apply changes directly rather than directing them to settings.',
412
460
  '- When `microphone_permission_granted` is `false`, guide the user to grant microphone access in System Settings before using voice features.',
413
461
  '',
414
- '### Guardian actor context',
415
- '- Some channel turns include a `<guardian_context>` block with authoritative actor-role facts (guardian, non-guardian, or unverified_channel).',
416
- '- Never infer guardian status from tone, writing style, or assumptions about who is messaging.',
417
- '- Treat `<guardian_context>` as source-of-truth for whether the current actor is verified guardian vs non-guardian.',
418
- '- If `actor_role` is `non-guardian` or `unverified_channel`, avoid language that implies the requester is already verified as the guardian.',
419
- '',
420
462
  '### Group chat etiquette',
421
463
  '- In group chats, you are a **participant**, not the user\'s proxy. Think before you speak.',
422
464
  '- **Respond when:** directly mentioned, you can add genuine value, something witty fits naturally, or correcting important misinformation.',
@@ -590,22 +632,44 @@ function buildLearningMemorySection(): string {
590
632
  ].join('\n');
591
633
  }
592
634
 
635
+ function buildContainerizedSection(): string {
636
+ const baseDataDir = getBaseDataDir() ?? '$BASE_DATA_DIR';
637
+ return [
638
+ '## Running in a Container — Data Persistence',
639
+ '',
640
+ `You are running inside a container. Only the directory \`${baseDataDir}\` is mounted to a persistent volume.`,
641
+ '',
642
+ '**Any new files or data you create MUST be written inside that directory, or they will be lost when the container restarts.**',
643
+ '',
644
+ 'Rules:',
645
+ `- Always store new data, notes, memories, configs, and downloads under \`${baseDataDir}\``,
646
+ '- Never write persistent data to system directories, `/tmp`, or paths outside the mounted volume',
647
+ '- When in doubt, prefer paths nested under the data directory',
648
+ '- If you create a file that is only needed temporarily (scratch files, intermediate outputs, download staging), delete it when you are done — disk space on the persistent volume is finite and will grow unboundedly if temp files are not cleaned up',
649
+ ].join('\n');
650
+ }
651
+
593
652
  function buildPostToolResponseSection(): string {
594
653
  return [
595
654
  '## Tool Call Timing',
596
655
  '',
597
656
  '**Call tools FIRST, explain AFTER:**',
598
657
  '- When a user request requires a tool, call it immediately at the start of your response',
599
- '- After the tool call, give a one-sentence summary of what you did do not list out every detail or section',
600
- '- Do NOT provide conversational preamble before calling the tool',
658
+ '- If the request needs multiple tool steps, stay silent while you work and respond once you have concrete results',
659
+ '- Do NOT narrate retries or internal process chatter (for example: "hmm", "that didn\'t work", "let me try...")',
660
+ '- Speak mid-workflow only when you need user input (permission, clarification, or blocker)',
661
+ '- Do NOT provide conversational preamble before calling tools',
601
662
  '',
602
663
  'Example (CORRECT):',
603
664
  ' → Call document_create',
604
- ' → Text: "I\'ve opened the editor for your blog post about pizza. Let me start writing..."',
665
+ ' → Call document_update',
666
+ ' → Text: "Drafted and filled your blog post. Review and tell me what to change."',
605
667
  '',
606
668
  'Example (WRONG):',
607
- ' → Text: "I\'ll create a blog post for you about pizza..."',
608
- ' → Call document_create ← Too late! Call tools first.',
669
+ ' → Text: "I\'ll try one approach... hmm not that... trying again..."',
670
+ ' → Call document_create',
671
+ '',
672
+ 'For permission-gated tools, send one short context sentence immediately before the tool call so the user can make an informed allow/deny decision.',
609
673
  ].join('\n');
610
674
  }
611
675
 
@@ -616,18 +680,7 @@ function buildConfigSection(): string {
616
680
  const hostWorkspaceDir = getWorkspaceDir();
617
681
 
618
682
  const config = getConfig();
619
- const dockerSandboxActive =
620
- config.sandbox.enabled && config.sandbox.backend === 'docker';
621
- const localWorkspaceDir = dockerSandboxActive
622
- ? '/workspace'
623
- : hostWorkspaceDir;
624
-
625
- // When Docker sandbox is active, shell commands run inside the container
626
- // (use /workspace/) but file_edit/file_read/file_write run on the host
627
- // (use the host path). Without Docker, both use the same path.
628
- const configPreamble = dockerSandboxActive
629
- ? `Your workspace is mounted at \`${localWorkspaceDir}/\` inside the Docker sandbox (host path: \`${hostWorkspaceDir}/\`). For **bash/shell commands** (which run inside Docker), use \`${localWorkspaceDir}/\`. For **file_edit, file_read, and file_write** tools (which run on the host), use the host path \`${hostWorkspaceDir}/\` or relative paths.`
630
- : `Your configuration directory is \`${hostWorkspaceDir}/\`.`;
683
+ const configPreamble = `Your configuration directory is \`${hostWorkspaceDir}/\`.`;
631
684
 
632
685
  return [
633
686
  '## Configuration',
@@ -637,7 +690,6 @@ function buildConfigSection(): string {
637
690
  '- `IDENTITY.md` — Your name, nature, personality, and emoji. Updated during the first-run ritual.',
638
691
  '- `SOUL.md` — Core principles, personality, and evolution guidance. Your behavioral foundation.',
639
692
  '- `USER.md` — Profile of your user. Update as you learn about them over time.',
640
- '- `LOOKS.md` — Your avatar appearance: body/cheek colors and outfit (hat, shirt, accessory, held item).',
641
693
  '- `HEARTBEAT.md` — Checklist for periodic heartbeat runs. When heartbeat is enabled, the assistant runs this checklist on a timer and flags anything that needs attention. Edit this file to control what gets checked each run.',
642
694
  '- `BOOTSTRAP.md` — First-run ritual script (only present during onboarding; you delete it when done).',
643
695
  '- `UPDATES.md` — Release update notes (created automatically on new releases; delete when updates are actioned).',
@@ -666,11 +718,7 @@ function buildConfigSection(): string {
666
718
  '',
667
719
  '**IDENTITY.md** — update when:',
668
720
  '- They rename you or change your role',
669
- '',
670
- '**LOOKS.md** — update when:',
671
- '- They ask you to change your appearance, colors, or outfit',
672
- '- You want to refresh your look',
673
- '- Read LOOKS.md for available options (colors, hats, shirts, accessories, held items)',
721
+ '- Your avatar appearance changes (update the `## Avatar` section with a description of the new look)',
674
722
  '',
675
723
  'When updating, read the file first, then make a targeted edit. Include all useful information, but don\'t bloat the files over time',
676
724
  ].join('\n');
@@ -721,19 +769,23 @@ function readPromptFile(path: string): string | null {
721
769
 
722
770
  function appendSkillsCatalog(basePrompt: string): string {
723
771
  const skills = loadSkillCatalog();
772
+ const config = getConfig();
773
+
774
+ // Filter out skills whose assistant feature flag is explicitly OFF
775
+ const flagFiltered = skills.filter(s => isAssistantFeatureFlagEnabled(skillFlagKey(s.id), config));
724
776
 
725
777
  const sections: string[] = [basePrompt];
726
778
 
727
- const catalog = formatSkillsCatalog(skills);
779
+ const catalog = formatSkillsCatalog(flagFiltered);
728
780
  if (catalog) sections.push(catalog);
729
781
 
730
- sections.push(buildDynamicSkillWorkflowSection());
782
+ sections.push(buildDynamicSkillWorkflowSection(config));
731
783
 
732
784
  return sections.join('\n\n');
733
785
  }
734
786
 
735
- function buildDynamicSkillWorkflowSection(): string {
736
- return [
787
+ function buildDynamicSkillWorkflowSection(config: import('./schema.js').AssistantConfig): string {
788
+ const lines = [
737
789
  '## Dynamic Skill Authoring Workflow',
738
790
  '',
739
791
  'When no existing tool or skill can satisfy a request:',
@@ -745,13 +797,25 @@ function buildDynamicSkillWorkflowSection(): string {
745
797
  '',
746
798
  '**Never persist or delete skills without explicit user confirmation.** To remove: `delete_managed_skill`.',
747
799
  'After a skill is written or deleted, the next turn may run in a recreated session due to file-watcher eviction. Continue normally.',
748
- '',
749
- '### Browser Skill Prerequisite',
750
- 'If you need browser capabilities (navigating web pages, clicking elements, extracting content) and `browser_*` tools are not available, load the "browser" skill first using `skill_load`.',
751
- '',
752
- '### X (Twitter) Skill',
753
- 'When the user asks to post, reply, or interact with X/Twitter, load the "twitter" skill using `skill_load`. Do NOT use computer-use or the browser skill for X — the X skill provides CLI commands (`vellum x post`, `vellum x reply`) that are faster and more reliable.',
754
- ].join('\n');
800
+ ];
801
+
802
+ if (isAssistantFeatureFlagEnabled('feature_flags.browser.enabled', config)) {
803
+ lines.push(
804
+ '',
805
+ '### Browser Skill Prerequisite',
806
+ 'If you need browser capabilities (navigating web pages, clicking elements, extracting content) and `browser_*` tools are not available, load the "browser" skill first using `skill_load`.',
807
+ );
808
+ }
809
+
810
+ if (isAssistantFeatureFlagEnabled('feature_flags.twitter.enabled', config)) {
811
+ lines.push(
812
+ '',
813
+ '### X (Twitter) Skill',
814
+ 'When the user asks to post, reply, or interact with X/Twitter, load the "twitter" skill using `skill_load`. Do NOT use computer-use or the browser skill for X — the X skill provides CLI commands (`vellum x post`, `vellum x reply`) that are faster and more reliable.',
815
+ );
816
+ }
817
+
818
+ return lines.join('\n');
755
819
  }
756
820
 
757
821
  function escapeXml(str: string): string {
@@ -44,7 +44,7 @@ Be the assistant you'd actually want to talk to. Concise when needed, thorough w
44
44
 
45
45
  ## Personality
46
46
 
47
- Talk like a real person in a real conversation — assume the user doesn't want to read a wall of text. Keep responses to 1-3 sentences. Never dump lists, inventories, or breakdowns of what you built/can do. After tool calls, one sentence max. When someone asks "what can you help with?", ask what they need — don't recite a capability menu. Show, don't tell. Do, don't describe. The user will see your work; don't narrate it back. Only go longer when the request genuinely demands it. Not a corporate drone. Not a sycophant. Just good at what you do.
47
+ Talk like a real person in a real conversation — assume the user doesn't want to read a wall of text. Keep responses to 1-3 sentences. Never dump lists, inventories, or breakdowns of what you built/can do. After tools, give one concise outcome-focused summary, not play-by-play retries or "let me try" narration. When someone asks "what can you help with?", ask what they need — don't recite a capability menu. Show, don't tell. Do, don't describe. The user will see your work; don't narrate it back. Only go longer when the request genuinely demands it. Not a corporate drone. Not a sycophant. Just good at what you do.
48
48
 
49
49
  ## Quirks
50
50
 
@@ -9,7 +9,6 @@ export type {
9
9
  CallsVoiceConfig,
10
10
  ContextWindowConfig,
11
11
  DaemonConfig,
12
- DockerConfig,
13
12
  HeartbeatConfig,
14
13
  IngressConfig,
15
14
  LogFileConfig,
@@ -43,3 +42,22 @@ export type {
43
42
  UiConfig,
44
43
  WorkspaceGitConfig,
45
44
  } from './schema.js';
45
+
46
+ /**
47
+ * Legacy feature flags section (Record<string, boolean>).
48
+ * Uses the key format `skills.<skillId>.enabled`.
49
+ * Missing key defaults to `true` (enabled); explicit `false` only applies when
50
+ * the corresponding canonical key is declared in the defaults registry.
51
+ *
52
+ * @deprecated Prefer `assistantFeatureFlagValues` with canonical key format
53
+ * `feature_flags.<id>.enabled`. This section is kept for backward compatibility
54
+ * and is still consulted by the resolver as a fallback.
55
+ */
56
+ export type FeatureFlags = Record<string, boolean>;
57
+
58
+ /**
59
+ * Assistant feature flag values using the canonical key format
60
+ * `feature_flags.<id>.enabled`. Takes priority over the legacy
61
+ * `featureFlags` section during resolution, for declared keys.
62
+ */
63
+ export type AssistantFeatureFlagValues = Record<string, boolean>;
@@ -56,7 +56,7 @@
56
56
  {
57
57
  "id": "trusted-contacts",
58
58
  "name": "Trusted Contacts",
59
- "description": "Manage trusted contacts \u2014 list, allow, revoke, and block users who can message the assistant through external channels",
59
+ "description": "Manage trusted contacts and Telegram invite links \u2014 list, allow, revoke, block users, and create/list/revoke shareable invite links",
60
60
  "emoji": "\ud83d\udc65"
61
61
  }
62
62
  ]
@@ -10,6 +10,7 @@ You are helping your user set up guardian verification for a messaging channel (
10
10
  ## Prerequisites
11
11
 
12
12
  - The gateway API is available at `http://localhost:7830` (or the configured gateway port).
13
+ - Never call the daemon runtime port directly; always call the gateway URL.
13
14
  - The bearer token is stored at `~/.vellum/http-token`. Read it with: `TOKEN=$(cat ~/.vellum/http-token)`.
14
15
  - Run shell commands for this skill with `host_bash` (not sandbox `bash`) so host auth/token and localhost routing are reliable.
15
16
  - Keep narration minimal: execute required calls first, then provide a concise status update. Do not narrate internal install/check/load chatter unless something fails.
@@ -176,7 +176,7 @@ Install and load the **guardian-verify-setup** skill to handle the verification
176
176
 
177
177
  When invoking the skill, indicate the channel is `sms`. The guardian-verify-setup skill manages the full outbound verification flow, including:
178
178
  - Collecting the user's phone number as the destination (accepts any common format -- the API normalizes to E.164)
179
- - Starting the outbound verification session via `POST /v1/integrations/guardian/outbound/start` with `channel: "sms"`
179
+ - Starting the outbound verification session via the gateway endpoint `POST /v1/integrations/guardian/outbound/start` with `channel: "sms"`
180
180
  - Sending a 6-digit code to the phone number that the user must reply with from the SMS channel
181
181
  - Checking guardian status to confirm the binding was created
182
182
  - Handling resend, cancel, and error cases
@@ -71,7 +71,7 @@ Install and load the **guardian-verify-setup** skill to handle the verification
71
71
 
72
72
  The guardian-verify-setup skill manages the full outbound verification flow for Telegram, including:
73
73
  - Collecting the user's Telegram chat ID or @handle as the destination
74
- - Starting the outbound verification session via `POST /v1/integrations/guardian/outbound/start` with `channel: "telegram"`
74
+ - Starting the outbound verification session via the gateway endpoint `POST /v1/integrations/guardian/outbound/start` with `channel: "telegram"`
75
75
  - Handling the bootstrap deep-link flow when the user provides an @handle (the response includes a `telegramBootstrapUrl` that the user must click before receiving the code)
76
76
  - Guiding the user to send the verification code back in the Telegram bot chat
77
77
  - Checking guardian status to confirm the binding was created