@vellumai/assistant 0.5.14 → 0.5.16

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 (175) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/architecture/integrations.md +15 -14
  3. package/knip.json +3 -1
  4. package/openapi.yaml +11 -43
  5. package/package.json +1 -1
  6. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
  7. package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
  8. package/src/__tests__/checker.test.ts +59 -0
  9. package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
  10. package/src/__tests__/cli-memory.test.ts +372 -0
  11. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
  12. package/src/__tests__/config-schema.test.ts +0 -2
  13. package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
  15. package/src/__tests__/conversation-slash-commands.test.ts +2 -6
  16. package/src/__tests__/conversation-usage.test.ts +1 -0
  17. package/src/__tests__/credential-security-e2e.test.ts +4 -1
  18. package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
  20. package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
  21. package/src/__tests__/heartbeat-service.test.ts +1 -3
  22. package/src/__tests__/intent-routing.test.ts +6 -18
  23. package/src/__tests__/log-export-workspace.test.ts +2 -28
  24. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
  25. package/src/__tests__/managed-store.test.ts +2 -10
  26. package/src/__tests__/messaging-send-tool.test.ts +6 -6
  27. package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
  28. package/src/__tests__/migration-export-http.test.ts +3 -34
  29. package/src/__tests__/migration-import-commit-http.test.ts +1 -29
  30. package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
  31. package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
  32. package/src/__tests__/oauth-apps-routes.test.ts +120 -10
  33. package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
  34. package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
  35. package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
  36. package/src/__tests__/oauth-providers-routes.test.ts +5 -2
  37. package/src/__tests__/oauth-store.test.ts +0 -5
  38. package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
  39. package/src/__tests__/path-policy.test.ts +2 -17
  40. package/src/__tests__/permission-types.test.ts +0 -1
  41. package/src/__tests__/platform-callback-registration.test.ts +3 -7
  42. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  43. package/src/__tests__/provider-error-scenarios.test.ts +0 -2
  44. package/src/__tests__/qdrant-manager.test.ts +68 -21
  45. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  46. package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
  47. package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
  48. package/src/__tests__/secret-allowlist.test.ts +20 -35
  49. package/src/__tests__/shell-credential-ref.test.ts +0 -5
  50. package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
  51. package/src/__tests__/skill-load-inline-command.test.ts +3 -65
  52. package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
  53. package/src/__tests__/skill-load-tool.test.ts +3 -67
  54. package/src/__tests__/skill-memory.test.ts +362 -119
  55. package/src/__tests__/skills.test.ts +22 -49
  56. package/src/__tests__/slack-channel-config.test.ts +2 -21
  57. package/src/__tests__/starter-bundle.test.ts +2 -8
  58. package/src/__tests__/stt-hints.test.ts +7 -2
  59. package/src/__tests__/system-prompt.test.ts +25 -45
  60. package/src/__tests__/task-compiler.test.ts +0 -21
  61. package/src/__tests__/task-management-tools.test.ts +0 -21
  62. package/src/__tests__/task-memory-cleanup.test.ts +0 -21
  63. package/src/__tests__/task-runner.test.ts +0 -21
  64. package/src/__tests__/task-scheduler.test.ts +0 -21
  65. package/src/__tests__/terminal-tools.test.ts +1 -17
  66. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
  67. package/src/__tests__/tool-approval-handler.test.ts +1 -20
  68. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
  69. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
  70. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  71. package/src/__tests__/tool-executor.test.ts +0 -1
  72. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
  73. package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
  74. package/src/__tests__/trust-store.test.ts +9 -41
  75. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
  76. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
  77. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
  78. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
  79. package/src/__tests__/trusted-contact-verification.test.ts +0 -22
  80. package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
  81. package/src/__tests__/twilio-provider.test.ts +0 -16
  82. package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
  83. package/src/__tests__/twilio-routes.test.ts +0 -24
  84. package/src/__tests__/update-bulletin.test.ts +17 -89
  85. package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
  86. package/src/__tests__/usage-routes.test.ts +0 -21
  87. package/src/__tests__/user-reference.test.ts +1 -5
  88. package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
  89. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
  90. package/src/__tests__/voice-invite-redemption.test.ts +0 -21
  91. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
  92. package/src/__tests__/voice-session-bridge.test.ts +0 -21
  93. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
  94. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
  95. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
  96. package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
  97. package/src/acp/client-handler.ts +1 -2
  98. package/src/cli/__tests__/notifications.test.ts +0 -22
  99. package/src/cli/cli-memory.ts +176 -0
  100. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  101. package/src/cli/commands/oauth/connect.ts +15 -0
  102. package/src/cli/commands/oauth/providers.ts +49 -42
  103. package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
  104. package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
  105. package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
  106. package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
  107. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  108. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  109. package/src/config/feature-flag-registry.json +16 -0
  110. package/src/config/loader.ts +4 -0
  111. package/src/config/schemas/security.ts +0 -6
  112. package/src/config/schemas/services.ts +8 -0
  113. package/src/context/window-manager.ts +28 -9
  114. package/src/credential-execution/approval-bridge.ts +0 -1
  115. package/src/daemon/config-watcher.ts +51 -0
  116. package/src/daemon/conversation-agent-loop.ts +3 -2
  117. package/src/daemon/conversation-process.ts +1 -0
  118. package/src/daemon/conversation-usage.ts +1 -0
  119. package/src/daemon/handlers/skills.ts +9 -1
  120. package/src/daemon/lifecycle.ts +13 -4
  121. package/src/daemon/message-types/conversations.ts +1 -0
  122. package/src/daemon/providers-setup.ts +2 -0
  123. package/src/daemon/server.ts +26 -22
  124. package/src/events/domain-events.ts +1 -2
  125. package/src/memory/db-init.ts +9 -0
  126. package/src/memory/job-handlers/batch-extraction.ts +16 -4
  127. package/src/memory/job-handlers/embedding.test.ts +3 -27
  128. package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
  129. package/src/memory/llm-usage-store.ts +35 -2
  130. package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
  131. package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
  132. package/src/memory/migrations/index.ts +2 -0
  133. package/src/memory/qdrant-manager.ts +26 -5
  134. package/src/memory/query-expansion.ts +1 -1
  135. package/src/memory/retriever.test.ts +22 -20
  136. package/src/memory/retriever.ts +10 -2
  137. package/src/memory/schema/oauth.ts +1 -1
  138. package/src/memory/search/mmr.ts +8 -5
  139. package/src/memory/slack-thread-store.ts +17 -0
  140. package/src/messaging/providers/outlook/adapter.ts +193 -0
  141. package/src/messaging/providers/outlook/client.ts +311 -0
  142. package/src/messaging/providers/outlook/types.ts +83 -0
  143. package/src/notifications/adapters/slack.ts +1 -1
  144. package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
  145. package/src/oauth/connect-orchestrator.ts +10 -3
  146. package/src/oauth/oauth-store.ts +10 -11
  147. package/src/oauth/provider-serializer.ts +3 -0
  148. package/src/oauth/provider-visibility.ts +16 -0
  149. package/src/oauth/seed-providers.ts +49 -17
  150. package/src/permissions/checker.ts +39 -7
  151. package/src/permissions/types.ts +2 -4
  152. package/src/prompts/journal-context.ts +9 -11
  153. package/src/prompts/system-prompt.ts +3 -64
  154. package/src/prompts/templates/UPDATES.md +6 -0
  155. package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
  156. package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
  157. package/src/runtime/auth/route-policy.ts +0 -4
  158. package/src/runtime/guardian-reply-router.ts +6 -2
  159. package/src/runtime/routes/conversation-query-routes.ts +2 -58
  160. package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
  161. package/src/runtime/routes/memory-item-routes.test.ts +0 -17
  162. package/src/runtime/routes/memory-item-routes.ts +103 -12
  163. package/src/runtime/routes/oauth-apps.ts +18 -1
  164. package/src/runtime/routes/oauth-providers.ts +13 -1
  165. package/src/runtime/routes/settings-routes.ts +1 -0
  166. package/src/runtime/routes/usage-routes.ts +19 -2
  167. package/src/runtime/routes/work-items-routes.test.ts +0 -21
  168. package/src/runtime/routes/workspace-routes.test.ts +3 -27
  169. package/src/security/secret-allowlist.ts +4 -4
  170. package/src/skills/skill-memory.ts +62 -23
  171. package/src/tools/memory/handlers.test.ts +1 -29
  172. package/src/tools/permission-checker.ts +0 -18
  173. package/src/tools/skills/skill-script-runner.ts +1 -1
  174. package/src/util/device-id.ts +3 -65
  175. package/src/workspace/git-service.ts +27 -6
@@ -1,10 +1,5 @@
1
- import { mkdtempSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
5
2
 
6
- const testDir = mkdtempSync(join(tmpdir(), "platform-status-test-"));
7
-
8
3
  // ---------------------------------------------------------------------------
9
4
  // Mock state
10
5
  // ---------------------------------------------------------------------------
@@ -68,51 +63,6 @@ mock.module("../../../../util/logger.js", () => ({
68
63
  pruneOldLogFiles: () => 0,
69
64
  }));
70
65
 
71
- mock.module("../../../../util/platform.js", () => ({
72
- getRootDir: () => testDir,
73
- getDataDir: () => join(testDir, "data"),
74
- getWorkspaceSkillsDir: () => join(testDir, "skills"),
75
- getWorkspaceDir: () => join(testDir, "workspace"),
76
- getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
77
- getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
78
- getHooksDir: () => join(testDir, "hooks"),
79
- getSignalsDir: () => join(testDir, "signals"),
80
- getConversationsDir: () => join(testDir, "conversations"),
81
- getEmbeddingModelsDir: () => join(testDir, "models"),
82
- getSandboxRootDir: () => join(testDir, "sandbox"),
83
- getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
84
- getInterfacesDir: () => join(testDir, "interfaces"),
85
- getSoundsDir: () => join(testDir, "sounds"),
86
- getHistoryPath: () => join(testDir, "history"),
87
- isMacOS: () => process.platform === "darwin",
88
- isLinux: () => process.platform === "linux",
89
- isWindows: () => process.platform === "win32",
90
- getPlatformName: () => "linux",
91
- getClipboardCommand: () => null,
92
- resolveInstanceDataDir: () => undefined,
93
- normalizeAssistantId: (id: string) => id,
94
- getTCPPort: () => 0,
95
- isTCPEnabled: () => false,
96
- getTCPHost: () => "127.0.0.1",
97
- isIOSPairingEnabled: () => false,
98
- getPlatformTokenPath: () => join(testDir, "token"),
99
- readPlatformToken: () => null,
100
- getPidPath: () => join(testDir, "test.pid"),
101
- getDbPath: () => join(testDir, "test.db"),
102
- getLogPath: () => join(testDir, "test.log"),
103
- getWorkspaceDirDisplay: () => testDir,
104
- getWorkspacePromptPath: (file: string) => join(testDir, file),
105
- getProtectedDir: () => join(testDir, "protected"),
106
- getDeprecatedDir: () => join(testDir, "workspace", "deprecated"),
107
- getExternalDir: () => join(testDir, "external"),
108
- getBinDir: () => join(testDir, "bin"),
109
- getDotEnvPath: () => join(testDir, ".env"),
110
- getDaemonStderrLogPath: () => join(testDir, "daemon-stderr.log"),
111
- getDaemonStartupLockPath: () => join(testDir, "daemon-startup.lock"),
112
- getEmbedWorkerPidPath: () => join(testDir, "embed-worker.pid"),
113
- ensureDataDir: () => {},
114
- }));
115
-
116
66
  mock.module("../../../../config/loader.js", () => ({
117
67
  API_KEY_PROVIDERS: [] as const,
118
68
  getConfig: () => ({
@@ -23,7 +23,7 @@
23
23
  "name": "computer_use_click",
24
24
  "description": "Click an element on screen. Prefer element_id (from the accessibility tree) over x/y coordinates.",
25
25
  "category": "computer-use",
26
- "risk": "low",
26
+ "risk": "medium",
27
27
  "input_schema": {
28
28
  "type": "object",
29
29
  "properties": {
@@ -62,7 +62,7 @@
62
62
  "name": "computer_use_type_text",
63
63
  "description": "Type text at the current cursor position. First click a text field (by element_id) to focus it, then call this tool. If a field shows 'FOCUSED', skip the click.",
64
64
  "category": "computer-use",
65
- "risk": "low",
65
+ "risk": "medium",
66
66
  "input_schema": {
67
67
  "type": "object",
68
68
  "properties": {
@@ -88,7 +88,7 @@
88
88
  "name": "computer_use_key",
89
89
  "description": "Press a key or keyboard shortcut. Supported: enter, tab, escape, backspace, delete, up, down, left, right, space, cmd+a, cmd+c, cmd+v, cmd+z, cmd+tab, cmd+w, shift+tab, option+tab",
90
90
  "category": "computer-use",
91
- "risk": "low",
91
+ "risk": "medium",
92
92
  "input_schema": {
93
93
  "type": "object",
94
94
  "properties": {
@@ -114,7 +114,7 @@
114
114
  "name": "computer_use_scroll",
115
115
  "description": "Scroll within an element by its [ID], or at raw screen coordinates as fallback.",
116
116
  "category": "computer-use",
117
- "risk": "low",
117
+ "risk": "medium",
118
118
  "input_schema": {
119
119
  "type": "object",
120
120
  "properties": {
@@ -157,7 +157,7 @@
157
157
  "name": "computer_use_drag",
158
158
  "description": "Drag from one element or position to another. Use for moving files, resizing windows, rearranging items, or adjusting sliders.",
159
159
  "category": "computer-use",
160
- "risk": "low",
160
+ "risk": "medium",
161
161
  "input_schema": {
162
162
  "type": "object",
163
163
  "properties": {
@@ -229,7 +229,7 @@
229
229
  "name": "computer_use_open_app",
230
230
  "description": "Open or switch to a macOS application by name. Preferred over cmd+tab for switching apps - more reliable and explicit.",
231
231
  "category": "computer-use",
232
- "risk": "low",
232
+ "risk": "medium",
233
233
  "input_schema": {
234
234
  "type": "object",
235
235
  "properties": {
@@ -255,7 +255,7 @@
255
255
  "name": "computer_use_run_applescript",
256
256
  "description": "Run an AppleScript command. Prefer this over click/type when possible - it doesn't move the cursor or interrupt the user. Never use 'do shell script' inside AppleScript (blocked for security).",
257
257
  "category": "computer-use",
258
- "risk": "low",
258
+ "risk": "medium",
259
259
  "input_schema": {
260
260
  "type": "object",
261
261
  "properties": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: messaging
3
- description: Read, search, send, and manage messages across Gmail, Telegram, and other platforms
3
+ description: Read, search, send, and manage messages across Gmail, Outlook, Telegram, and other platforms
4
4
  compatibility: "Designed for Vellum personal assistants"
5
5
  metadata:
6
6
  emoji: "\U0001F4AC"
@@ -11,7 +11,7 @@ metadata:
11
11
  - "Handles credential flows -- do not improvise setup instructions"
12
12
  ---
13
13
 
14
- You are a unified messaging assistant with access to multiple platforms (Gmail, Telegram, and more). Use the messaging tools to help users read, search, organize, draft, and send messages across all connected platforms.
14
+ You are a unified messaging assistant with access to multiple platforms (Gmail, Outlook, Telegram, and more). Use the messaging tools to help users read, search, organize, draft, and send messages across all connected platforms.
15
15
 
16
16
  **Slack is not handled by this skill.** Slack messaging (send, read, search) is handled by the **slack** skill, which uses the Slack Web API directly via CLI. Do not use messaging tools with `platform: "slack"`.
17
17
 
@@ -67,6 +67,11 @@ When the user asks to "connect my email", "set up email", "manage my email", or
67
67
  - **cancelLabel:** "Not Now"
68
68
  - If the user confirms, briefly acknowledge (e.g., "Setting up Gmail now...") and proceed with the setup guide. If they decline, acknowledge and let them know they can set it up later.
69
69
 
70
+ ### Outlook
71
+
72
+ 1. **Try connecting directly first.** Run `assistant oauth status outlook`. This will show whether the user has previously connected their Outlook account.
73
+ 2. **If no connections are found:** Guide the user to connect their Microsoft account through the OAuth flow.
74
+
70
75
  ### Slack
71
76
 
72
77
  Slack is **not** handled by this skill. For Slack setup, load the **slack-app-setup** skill directly. For Slack messaging, use the **slack** skill which accesses the Slack Web API via CLI.
@@ -118,6 +123,16 @@ When a messaging tool fails with a token or authorization error:
118
123
  - **Send / Reply**: Send a message or reply in a thread (via `thread_id`). High risk - requires user approval.
119
124
  - **Mark Read**: Mark conversation as read
120
125
 
126
+ ### Outlook
127
+
128
+ - **Auth Test**: Verify connection and show account info
129
+ - **List Conversations**: Show mail folders (Inbox, Sent, Drafts, etc.) with unread counts
130
+ - **Read Messages**: Read message history from a folder
131
+ - **Search**: Search messages using Microsoft Graph KQL syntax
132
+ - **Send / Reply**: Send a message or reply to a thread (high risk - requires user approval)
133
+ - **Mark Read**: Mark a message as read
134
+ - **Thread Replies**: View all messages in a conversation thread
135
+
121
136
  ### Telegram
122
137
 
123
138
  Telegram is supported as a messaging provider with limited capabilities compared to Gmail due to Bot API constraints:
@@ -108,7 +108,7 @@
108
108
  "required": ["image_path"]
109
109
  },
110
110
  "executor": "tools/avatar-update.ts",
111
- "execution_target": "sandbox"
111
+ "execution_target": "host"
112
112
  },
113
113
  {
114
114
  "name": "remove_avatar",
@@ -125,7 +125,7 @@
125
125
  }
126
126
  },
127
127
  "executor": "tools/avatar-remove.ts",
128
- "execution_target": "sandbox"
128
+ "execution_target": "host"
129
129
  },
130
130
  {
131
131
  "name": "get_avatar",
@@ -142,7 +142,7 @@
142
142
  }
143
143
  },
144
144
  "executor": "tools/avatar-get.ts",
145
- "execution_target": "sandbox"
145
+ "execution_target": "host"
146
146
  }
147
147
  ]
148
148
  }
@@ -25,6 +25,14 @@
25
25
  "description": "Enable the Vellum Cloud hosting option on the Hosting screen",
26
26
  "defaultEnabled": false
27
27
  },
28
+ {
29
+ "id": "local-docker-enabled",
30
+ "scope": "macos",
31
+ "key": "local-docker-enabled",
32
+ "label": "Local Docker Mode",
33
+ "description": "When enabled, the Local hosting option uses Docker under the hood for sandboxed execution, hiding the separate Docker card",
34
+ "defaultEnabled": false
35
+ },
28
36
  {
29
37
  "id": "contacts",
30
38
  "scope": "assistant",
@@ -360,6 +368,14 @@
360
368
  "label": "Fast Mode",
361
369
  "description": "Enable Anthropic fast mode for Opus 4.6, delivering up to 2.5x higher output tokens per second at premium pricing",
362
370
  "defaultEnabled": false
371
+ },
372
+ {
373
+ "id": "outlook-oauth-integration",
374
+ "scope": "assistant",
375
+ "key": "outlook-oauth-integration",
376
+ "label": "Outlook / Microsoft Integration",
377
+ "description": "Enable the Outlook / Microsoft OAuth provider for connecting to Microsoft Graph APIs (email, calendar)",
378
+ "defaultEnabled": false
363
379
  }
364
380
  ]
365
381
  }
@@ -124,6 +124,10 @@ const DEPRECATED_FIELDS: Record<string, string> = {
124
124
  "providerOrder has been removed from the config schema. " +
125
125
  "Provider selection is now handled automatically. " +
126
126
  "The field will be removed from your config file.",
127
+ "permissions.dangerouslySkipPermissions":
128
+ "permissions.dangerouslySkipPermissions has been removed. " +
129
+ "Permission prompts are now always shown when required. " +
130
+ "The field will be removed from your config file.",
127
131
  };
128
132
 
129
133
  /**
@@ -79,12 +79,6 @@ export const PermissionsConfigSchema = z
79
79
  .describe(
80
80
  "Permission mode — 'strict' requires explicit approval for all operations, 'workspace' allows operations within the workspace",
81
81
  ),
82
- dangerouslySkipPermissions: z
83
- .boolean({
84
- error: "permissions.dangerouslySkipPermissions must be a boolean",
85
- })
86
- .default(false)
87
- .describe("Auto-accept all permission prompts without asking"),
88
82
  })
89
83
  .describe("Permission enforcement mode for tool operations");
90
84
 
@@ -51,6 +51,11 @@ export const GoogleOAuthServiceSchema = BaseServiceSchema.extend({
51
51
  });
52
52
  export type GoogleOAuthService = z.infer<typeof GoogleOAuthServiceSchema>;
53
53
 
54
+ export const OutlookOAuthServiceSchema = BaseServiceSchema.extend({
55
+ mode: ServiceModeSchema.default("your-own"),
56
+ });
57
+ export type OutlookOAuthService = z.infer<typeof OutlookOAuthServiceSchema>;
58
+
54
59
  export const ServicesSchema = z.object({
55
60
  inference: InferenceServiceSchema.default(InferenceServiceSchema.parse({})),
56
61
  "image-generation": ImageGenerationServiceSchema.default(
@@ -62,5 +67,8 @@ export const ServicesSchema = z.object({
62
67
  "google-oauth": GoogleOAuthServiceSchema.default(
63
68
  GoogleOAuthServiceSchema.parse({}),
64
69
  ),
70
+ "outlook-oauth": OutlookOAuthServiceSchema.default(
71
+ OutlookOAuthServiceSchema.parse({}),
72
+ ),
65
73
  });
66
74
  export type Services = z.infer<typeof ServicesSchema>;
@@ -581,7 +581,7 @@ export class ContextWindowManager {
581
581
  const overheadTokens =
582
582
  estimateTextTokens(SUMMARY_SYSTEM_PROMPT) +
583
583
  estimateTextTokens(currentSummary) +
584
- // Scaffolding text in buildSummaryPrompt ("Update the summary...",
584
+ // Scaffolding text in buildSummaryContentBlocks ("Update the summary...",
585
585
  // section headers, etc.) — generous fixed estimate.
586
586
  200 +
587
587
  this.summaryMaxTokens;
@@ -598,6 +598,7 @@ export class ContextWindowManager {
598
598
  for (const block of blocks) {
599
599
  totalTokens += estimateBlockTokens(block);
600
600
  }
601
+ const originalTotalTokens = totalTokens;
601
602
  if (totalTokens <= maxTranscriptTokens) return blocks;
602
603
 
603
604
  // First pass: drop images from the beginning until we fit or run out of
@@ -618,17 +619,39 @@ export class ContextWindowManager {
618
619
  if (totalTokens <= maxTranscriptTokens) return result;
619
620
 
620
621
  // Second pass: drop text blocks from the beginning (oldest) until we fit.
622
+ // If a single text block exceeds the remaining budget, truncate it rather
623
+ // than dropping it entirely so the summarizer always has content to work with.
621
624
  let dropUntil = 0;
622
625
  let droppedTokens = 0;
623
626
  for (let i = 0; i < result.length && totalTokens > maxTranscriptTokens; i++) {
624
- droppedTokens += estimateBlockTokens(result[i]);
625
- totalTokens -= estimateBlockTokens(result[i]);
627
+ const blockTokens = estimateBlockTokens(result[i]);
628
+ const excess = totalTokens - maxTranscriptTokens;
629
+ if (blockTokens > excess && result[i].type === "text") {
630
+ // Truncate this block to shed exactly the excess tokens.
631
+ const keepTokens = blockTokens - excess;
632
+ const text = (result[i] as { type: "text"; text: string }).text;
633
+ // Approximate: 1 token ≈ 4 characters for truncation purposes.
634
+ const keepChars = Math.max(1, Math.floor(keepTokens * 4));
635
+ const truncatedText = text.slice(-keepChars);
636
+ const truncatedBlock: ContentBlock = {
637
+ type: "text",
638
+ text: `[...truncated] ${truncatedText}`,
639
+ };
640
+ const newBlockTokens = estimateBlockTokens(truncatedBlock);
641
+ droppedTokens += blockTokens - newBlockTokens;
642
+ totalTokens -= blockTokens - newBlockTokens;
643
+ result[i] = truncatedBlock;
644
+ dropUntil = i;
645
+ break;
646
+ }
647
+ droppedTokens += blockTokens;
648
+ totalTokens -= blockTokens;
626
649
  dropUntil = i + 1;
627
650
  }
628
651
 
629
652
  log.info(
630
653
  {
631
- originalTokens: totalTokens + droppedTokens,
654
+ originalTokens: originalTotalTokens,
632
655
  cappedTokens: maxTranscriptTokens,
633
656
  droppedTokens,
634
657
  },
@@ -892,11 +915,7 @@ function serializeMessagesToContentBlocks(messages: Message[]): ContentBlock[] {
892
915
  textLines.length = 0;
893
916
  }
894
917
  blocks.push(block);
895
- } else if (block.type === "web_search_tool_result") {
896
- textLines.push(
897
- `web_search_tool_result ${block.tool_use_id}: [opaque]`,
898
- );
899
- } else if (block.type === "tool_result") {
918
+ } else if (block.type === "tool_result") { // guard:allow-tool-result-only — web_search_tool_result handled by serializeBlock via else branch
900
919
  // Extract images from tool_result contentBlocks before serializing.
901
920
  const collectedImages: ImageContent[] = [];
902
921
  textLines.push(serializeToolResultBlock(block, collectedImages));
@@ -103,7 +103,6 @@ function mapUserDecisionToCesDecision(
103
103
  userDecision: decision,
104
104
  };
105
105
  case "temporary_override":
106
- case "dangerously_skip_permissions":
107
106
  return {
108
107
  grantDecision: "approved",
109
108
  ttl: undefined,
@@ -10,8 +10,10 @@ import {
10
10
  readdirSync,
11
11
  watch,
12
12
  } from "node:fs";
13
+ import { homedir } from "node:os";
13
14
  import { join } from "node:path";
14
15
 
16
+ import { clearFeatureFlagOverridesCache } from "../config/assistant-feature-flags.js";
15
17
  import { getConfig, invalidateConfigCache } from "../config/loader.js";
16
18
  import { clearEmbeddingBackendCache } from "../memory/embedding-backend.js";
17
19
  import { clearCache as clearTrustCache } from "../permissions/trust-store.js";
@@ -173,6 +175,7 @@ export class ConfigWatcher {
173
175
  "workspace directory for config/prompt changes",
174
176
  );
175
177
 
178
+ this.startFeatureFlagsWatcher();
176
179
  this.startSignalsWatcher();
177
180
  this.startSkillsWatchers(onConversationEvict);
178
181
  }
@@ -185,6 +188,54 @@ export class ConfigWatcher {
185
188
  this.watchers = [];
186
189
  }
187
190
 
191
+ private startFeatureFlagsWatcher(): void {
192
+ const protectedDir = process.env.GATEWAY_SECURITY_DIR
193
+ ? process.env.GATEWAY_SECURITY_DIR
194
+ : join(homedir(), ".vellum", "protected");
195
+
196
+ try {
197
+ if (!existsSync(protectedDir)) {
198
+ mkdirSync(protectedDir, { recursive: true });
199
+ }
200
+ } catch {
201
+ // If we can't create it, watching will also fail — handled below.
202
+ }
203
+
204
+ const FLAG_FILES = new Set([
205
+ "feature-flags.json",
206
+ "feature-flags-remote.json",
207
+ ]);
208
+
209
+ try {
210
+ const watcher = watch(protectedDir, (_eventType, filename) => {
211
+ if (!filename) return;
212
+ const file = String(filename);
213
+ if (!FLAG_FILES.has(file)) return;
214
+ this.debounceTimers.schedule(
215
+ "file:feature-flags",
216
+ () => {
217
+ log.info(
218
+ { file },
219
+ "Feature flags file changed, invalidating cache",
220
+ );
221
+ clearFeatureFlagOverridesCache();
222
+ },
223
+ 500,
224
+ );
225
+ });
226
+ this.watchers.push(watcher);
227
+ log.info(
228
+ { dir: protectedDir },
229
+ "Watching protected directory for feature flag changes",
230
+ );
231
+ } catch (err) {
232
+ log.warn(
233
+ { err, dir: protectedDir },
234
+ "Failed to watch protected directory for feature flags. Flag changes will require a restart.",
235
+ );
236
+ }
237
+ }
238
+
188
239
  private startSignalsWatcher(): void {
189
240
  const signalsDir = getSignalsDir();
190
241
  try {
@@ -1587,14 +1587,15 @@ export async function runAgentLoopImpl(
1587
1587
  }
1588
1588
 
1589
1589
  const restoredHistory = [...preRepairMessages, ...newMessages];
1590
- ctx.messages = stripInjectedContext(restoredHistory);
1591
1590
 
1592
1591
  const postLoopContextEstimate = estimatePromptTokens(
1593
- ctx.messages,
1592
+ restoredHistory,
1594
1593
  ctx.systemPrompt,
1595
1594
  { providerName: ctx.provider.name, toolTokenBudget },
1596
1595
  );
1597
1596
 
1597
+ ctx.messages = stripInjectedContext(restoredHistory);
1598
+
1598
1599
  emitUsage(
1599
1600
  ctx,
1600
1601
  state.exchangeInputTokens,
@@ -936,6 +936,7 @@ export async function processMessage(
936
936
  return persisted.id;
937
937
  } finally {
938
938
  conversation.processing = false;
939
+ await drainQueue(conversation);
939
940
  }
940
941
  }
941
942
 
@@ -175,6 +175,7 @@ export function recordUsage(
175
175
  );
176
176
  onEvent({
177
177
  type: "usage_update",
178
+ conversationId: ctx.conversationId,
178
179
  inputTokens,
179
180
  outputTokens,
180
181
  totalInputTokens: ctx.usageStats.inputTokens,
@@ -42,7 +42,10 @@ import {
42
42
  removeSkillsIndexEntry,
43
43
  validateManagedSkillId,
44
44
  } from "../../skills/managed-store.js";
45
- import { deleteSkillCapabilityMemory } from "../../skills/skill-memory.js";
45
+ import {
46
+ deleteSkillCapabilityMemory,
47
+ seedCatalogSkillMemories,
48
+ } from "../../skills/skill-memory.js";
46
49
  import { getWorkspaceSkillsDir } from "../../util/platform.js";
47
50
  import {
48
51
  CONFIG_RELOAD_DEBOUNCE_MS,
@@ -478,6 +481,7 @@ export function enableSkill(
478
481
  name: skillId,
479
482
  state: "enabled",
480
483
  });
484
+ seedCatalogSkillMemories();
481
485
  return { success: true };
482
486
  } catch (err) {
483
487
  const message = err instanceof Error ? err.message : String(err);
@@ -572,6 +576,7 @@ export async function installSkill(
572
576
  "Failed to auto-enable bundled skill",
573
577
  );
574
578
  }
579
+ seedCatalogSkillMemories();
575
580
  return { success: true };
576
581
  }
577
582
 
@@ -602,6 +607,7 @@ export async function installSkill(
602
607
  );
603
608
  }
604
609
 
610
+ seedCatalogSkillMemories();
605
611
  return { success: true };
606
612
  }
607
613
  } catch (err) {
@@ -637,6 +643,7 @@ export async function installSkill(
637
643
  log.warn({ err, skillId }, "Failed to auto-enable installed skill");
638
644
  }
639
645
 
646
+ seedCatalogSkillMemories();
640
647
  return { success: true };
641
648
  } catch (err) {
642
649
  const message = err instanceof Error ? err.message : String(err);
@@ -1035,6 +1042,7 @@ export async function createSkill(
1035
1042
  );
1036
1043
  }
1037
1044
 
1045
+ seedCatalogSkillMemories();
1038
1046
  return { success: true };
1039
1047
  } catch (err) {
1040
1048
  const message = err instanceof Error ? err.message : String(err);
@@ -5,6 +5,7 @@ import { reconcileCallsOnStartup } from "../calls/call-recovery.js";
5
5
  import { setRelayBroadcast } from "../calls/relay-server.js";
6
6
  import { TwilioConversationRelayProvider } from "../calls/twilio-provider.js";
7
7
  import { setVoiceBridgeDeps } from "../calls/voice-session-bridge.js";
8
+ import { seedCliCommandMemories } from "../cli/cli-memory.js";
8
9
  import {
9
10
  getPlatformAssistantId,
10
11
  getQdrantHttpPortEnv,
@@ -641,11 +642,19 @@ export async function runDaemon(): Promise<void> {
641
642
  log.info("Daemon startup: starting memory worker");
642
643
  bgRefs.memoryWorker = startMemoryJobsWorker();
643
644
 
644
- // Seed capability memories for first-party catalog skills so the memory
645
+ // Seed capability memories for all enabled skills so the memory
645
646
  // pipeline can surface relevant skills via semantic search.
646
- void seedCatalogSkillMemories().catch((err) =>
647
- log.warn({ err }, "Catalog skill memory seeding failed — continuing"),
648
- );
647
+ try {
648
+ seedCatalogSkillMemories();
649
+ } catch (err) {
650
+ log.warn({ err }, "Catalog skill memory seeding failed — continuing");
651
+ }
652
+
653
+ try {
654
+ seedCliCommandMemories();
655
+ } catch (err) {
656
+ log.warn({ err }, "CLI command memory seeding failed — continuing");
657
+ }
649
658
  }
650
659
 
651
660
  // Fire-and-forget: Qdrant init runs concurrently with the rest of startup
@@ -335,6 +335,7 @@ export interface UndoComplete {
335
335
 
336
336
  export interface UsageUpdate {
337
337
  type: "usage_update";
338
+ conversationId: string;
338
339
  inputTokens: number;
339
340
  outputTokens: number;
340
341
  totalInputTokens: number;
@@ -8,6 +8,7 @@ import type { AssistantConfig } from "../config/types.js";
8
8
  import { setSentryOrganizationId, setSentryUserId } from "../instrument.js";
9
9
  import { getMcpServerManager } from "../mcp/manager.js";
10
10
  import { gmailMessagingProvider } from "../messaging/providers/gmail/adapter.js";
11
+ import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
11
12
  import { slackProvider as slackMessagingProvider } from "../messaging/providers/slack/adapter.js";
12
13
  import { telegramBotMessagingProvider } from "../messaging/providers/telegram-bot/adapter.js";
13
14
  import { whatsappMessagingProvider } from "../messaging/providers/whatsapp/adapter.js";
@@ -140,6 +141,7 @@ export function registerWatcherProviders(): void {
140
141
  export function registerMessagingProviders(): void {
141
142
  registerMessagingProvider(slackMessagingProvider);
142
143
  registerMessagingProvider(gmailMessagingProvider);
144
+ registerMessagingProvider(outlookMessagingProvider);
143
145
  registerMessagingProvider(telegramBotMessagingProvider);
144
146
  registerMessagingProvider(whatsappMessagingProvider);
145
147
  }
@@ -576,6 +576,17 @@ export class DaemonServer {
576
576
  if (params.attachments && params.attachments.length > 0) {
577
577
  for (const a of params.attachments) {
578
578
  try {
579
+ const validation = attachmentsStore.validateAttachmentUpload(
580
+ a.filename,
581
+ a.mimeType,
582
+ );
583
+ if (!validation.ok) {
584
+ log.warn(
585
+ { error: validation.error, path: a.path },
586
+ "Signal attachment rejected by validation",
587
+ );
588
+ continue;
589
+ }
579
590
  const size = statSync(a.path).size;
580
591
  const stored = attachmentsStore.uploadFileBackedAttachment(
581
592
  a.filename,
@@ -584,12 +595,11 @@ export class DaemonServer {
584
595
  size,
585
596
  );
586
597
  attachmentIds.push(stored.id);
587
- const fileData = readFileSync(a.path).toString("base64");
588
598
  resolvedAttachments.push({
589
599
  id: stored.id,
590
600
  filename: a.filename,
591
601
  mimeType: a.mimeType,
592
- data: fileData,
602
+ data: "",
593
603
  filePath: a.path,
594
604
  });
595
605
  } catch (err) {
@@ -609,20 +619,17 @@ export class DaemonServer {
609
619
  "string"
610
620
  ? (msg as { conversationId: string }).conversationId
611
621
  : undefined;
612
- const event = buildAssistantEvent(
613
- DAEMON_INTERNAL_ASSISTANT_ID,
614
- msg,
615
- msgConversationId ?? conversationId,
616
- );
617
- assistantEventHub.publish(event).catch((err) => {
618
- log.warn(
619
- { err },
620
- "assistant-events hub subscriber threw during signal user-message",
621
- );
622
- });
622
+ this.publishAssistantEvent(msg, msgConversationId ?? conversationId);
623
623
  };
624
624
 
625
625
  if (conversation.isProcessing()) {
626
+ // Hydrate file data now — the queue path won't re-read from
627
+ // the attachment store, so base64 content must be inline.
628
+ for (const att of resolvedAttachments) {
629
+ if (att.filePath && !att.data) {
630
+ att.data = readFileSync(att.filePath).toString("base64");
631
+ }
632
+ }
626
633
  const requestId = crypto.randomUUID();
627
634
  const resolvedChannel = resolveTurnChannel(params.sourceChannel);
628
635
  const resolvedInterface = resolveTurnInterface(params.sourceInterface);
@@ -977,15 +984,12 @@ export class DaemonServer {
977
984
  // Persist the conversation's current trust/auth context so it survives
978
985
  // eviction and recreation. The restore path in getOrCreateConversation
979
986
  // reads from storedOptions.trustContext / storedOptions.authContext.
980
- const currentTrust = conversation.trustContext;
981
- const currentAuth = conversation.authContext;
982
- if (currentTrust || currentAuth) {
983
- this.conversationOptions.set(conversationId, {
984
- ...this.conversationOptions.get(conversationId),
985
- ...(currentTrust ? { trustContext: currentTrust } : {}),
986
- ...(currentAuth ? { authContext: currentAuth } : {}),
987
- });
988
- }
987
+ // Always write — including null — so explicit clearing isn't lost.
988
+ this.conversationOptions.set(conversationId, {
989
+ ...this.conversationOptions.get(conversationId),
990
+ trustContext: conversation.trustContext,
991
+ authContext: conversation.authContext,
992
+ });
989
993
  conversation.setChannelCapabilities(
990
994
  resolveChannelCapabilities(
991
995
  sourceChannel,