@vellumai/assistant 0.6.0 → 0.6.1

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 (285) hide show
  1. package/AGENTS.md +4 -0
  2. package/ARCHITECTURE.md +68 -15
  3. package/Dockerfile +2 -2
  4. package/bun.lock +6 -2
  5. package/docker-entrypoint.sh +32 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/openapi.yaml +538 -3
  9. package/package.json +5 -1
  10. package/src/__tests__/anthropic-provider.test.ts +160 -95
  11. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  12. package/src/__tests__/app-executors.test.ts +47 -1
  13. package/src/__tests__/app-source-watcher.test.ts +159 -0
  14. package/src/__tests__/checker.test.ts +38 -6
  15. package/src/__tests__/config-schema.test.ts +5 -0
  16. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  17. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  18. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  19. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  20. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  21. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  22. package/src/__tests__/conversation-wipe.test.ts +2 -6
  23. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  24. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  25. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  26. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  27. package/src/__tests__/date-context.test.ts +76 -210
  28. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  29. package/src/__tests__/file-list-tool.test.ts +219 -0
  30. package/src/__tests__/first-greeting.test.ts +1 -1
  31. package/src/__tests__/heartbeat-service.test.ts +180 -3
  32. package/src/__tests__/identity-routes.test.ts +328 -0
  33. package/src/__tests__/injection-block.test.ts +24 -0
  34. package/src/__tests__/install-skill-routing.test.ts +7 -6
  35. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  36. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  37. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  38. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  39. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  40. package/src/__tests__/log-export-workspace.test.ts +72 -105
  41. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  42. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  43. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  44. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  45. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  46. package/src/__tests__/mock-fetch.ts +87 -0
  47. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  48. package/src/__tests__/onboarding-template-contract.test.ts +62 -14
  49. package/src/__tests__/parser.test.ts +32 -0
  50. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  51. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  52. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  53. package/src/__tests__/permission-mode-store.test.ts +277 -0
  54. package/src/__tests__/permission-mode.test.ts +101 -0
  55. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  56. package/src/__tests__/profiler-routes.test.ts +502 -0
  57. package/src/__tests__/profiler-run-store.test.ts +441 -0
  58. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  59. package/src/__tests__/registry.test.ts +1 -1
  60. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  61. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  62. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  63. package/src/__tests__/search-skills-unified.test.ts +4 -3
  64. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  65. package/src/__tests__/set-permission-mode.test.ts +274 -0
  66. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  67. package/src/__tests__/skill-memory.test.ts +2 -783
  68. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  69. package/src/__tests__/subagent-detail.test.ts +84 -0
  70. package/src/__tests__/subagent-disposal.test.ts +308 -0
  71. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  72. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  73. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  74. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  75. package/src/__tests__/subagent-tools.test.ts +464 -4
  76. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  77. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  78. package/src/__tests__/terminal-tools.test.ts +17 -27
  79. package/src/__tests__/test-preload.ts +4 -0
  80. package/src/__tests__/tool-executor.test.ts +4 -26
  81. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  82. package/src/__tests__/top-level-renderer.test.ts +10 -13
  83. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  84. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  85. package/src/agent/loop.ts +6 -0
  86. package/src/approvals/guardian-request-resolvers.ts +24 -0
  87. package/src/avatar/traits-png-sync.ts +3 -3
  88. package/src/cli/__tests__/run-assistant-command.ts +29 -0
  89. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  90. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  91. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  92. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  93. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  94. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  95. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  96. package/src/cli/commands/conversations.ts +1 -8
  97. package/src/cli/commands/email.ts +584 -835
  98. package/src/cli/commands/memory.ts +1 -34
  99. package/src/cli/commands/notifications.ts +7 -2
  100. package/src/cli/commands/oauth/connect.ts +14 -5
  101. package/src/cli/commands/routes.ts +396 -0
  102. package/src/cli/commands/skills.ts +130 -20
  103. package/src/cli/program.ts +2 -0
  104. package/src/cli.ts +1 -120
  105. package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
  106. package/src/config/bundled-skills/gmail/SKILL.md +2 -2
  107. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  108. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  109. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  111. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  112. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  113. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  114. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  115. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  116. package/src/config/env-registry.ts +63 -0
  117. package/src/config/feature-flag-registry.json +17 -1
  118. package/src/config/schema.ts +8 -0
  119. package/src/config/schemas/filing.ts +51 -0
  120. package/src/config/schemas/heartbeat.ts +15 -12
  121. package/src/config/schemas/memory-lifecycle.ts +12 -0
  122. package/src/config/schemas/security.ts +14 -0
  123. package/src/daemon/app-source-watcher.ts +93 -0
  124. package/src/daemon/config-watcher.ts +79 -1
  125. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  126. package/src/daemon/conversation-agent-loop.ts +158 -65
  127. package/src/daemon/conversation-history.ts +4 -19
  128. package/src/daemon/conversation-lifecycle.ts +8 -14
  129. package/src/daemon/conversation-process.ts +13 -7
  130. package/src/daemon/conversation-runtime-assembly.ts +300 -306
  131. package/src/daemon/conversation-tool-setup.ts +44 -14
  132. package/src/daemon/conversation-workspace.ts +1 -2
  133. package/src/daemon/conversation.ts +18 -0
  134. package/src/daemon/date-context.ts +26 -53
  135. package/src/daemon/first-greeting.ts +1 -1
  136. package/src/daemon/handlers/conversations.ts +4 -7
  137. package/src/daemon/handlers/shared.test.ts +143 -0
  138. package/src/daemon/handlers/shared.ts +63 -5
  139. package/src/daemon/handlers/skills.ts +11 -18
  140. package/src/daemon/lifecycle.ts +199 -157
  141. package/src/daemon/message-types/conversations.ts +25 -6
  142. package/src/daemon/message-types/messages.ts +9 -1
  143. package/src/daemon/message-types/schedules.ts +1 -0
  144. package/src/daemon/message-types/settings.ts +6 -0
  145. package/src/daemon/profiler-run-store.ts +557 -0
  146. package/src/daemon/server.ts +89 -9
  147. package/src/daemon/shutdown-handlers.ts +5 -0
  148. package/src/daemon/tool-side-effects.ts +23 -3
  149. package/src/export/transcript-formatter.ts +148 -0
  150. package/src/filing/filing-service.ts +228 -0
  151. package/src/heartbeat/heartbeat-service.ts +96 -7
  152. package/src/mcp/client.ts +6 -0
  153. package/src/mcp/mcp-oauth-provider.ts +149 -27
  154. package/src/memory/admin.ts +33 -32
  155. package/src/memory/app-store.ts +69 -0
  156. package/src/memory/conversation-bootstrap.ts +1 -1
  157. package/src/memory/conversation-crud.ts +136 -107
  158. package/src/memory/conversation-group-migration.ts +1 -1
  159. package/src/memory/conversation-queries.ts +58 -12
  160. package/src/memory/conversation-title-service.ts +1 -0
  161. package/src/memory/db-init.ts +182 -376
  162. package/src/memory/graph/bootstrap.ts +75 -66
  163. package/src/memory/graph/capability-seed.ts +167 -15
  164. package/src/memory/graph/consolidation.ts +38 -4
  165. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  166. package/src/memory/graph/extraction-job.ts +9 -4
  167. package/src/memory/graph/extraction.ts +66 -23
  168. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  169. package/src/memory/graph/graph-search.ts +29 -15
  170. package/src/memory/graph/injection.ts +38 -8
  171. package/src/memory/graph/inspect.ts +12 -3
  172. package/src/memory/graph/retriever.ts +365 -262
  173. package/src/memory/graph/store.test.ts +48 -0
  174. package/src/memory/graph/store.ts +150 -11
  175. package/src/memory/graph/tool-handlers.ts +84 -209
  176. package/src/memory/graph/tools.ts +8 -52
  177. package/src/memory/graph/types.ts +24 -0
  178. package/src/memory/job-handlers/cleanup.ts +44 -1
  179. package/src/memory/jobs-store.ts +70 -60
  180. package/src/memory/jobs-worker.ts +44 -28
  181. package/src/memory/llm-request-log-store.ts +96 -12
  182. package/src/memory/memory-recall-log-store.ts +49 -5
  183. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  184. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  185. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  186. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  187. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  188. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  189. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  190. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  191. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  192. package/src/memory/migrations/index.ts +8 -0
  193. package/src/memory/migrations/registry.ts +8 -0
  194. package/src/memory/schema/conversations.ts +14 -0
  195. package/src/memory/schema/infrastructure.ts +8 -1
  196. package/src/memory/schema/memory-core.ts +0 -51
  197. package/src/memory/schema/memory-graph.ts +15 -0
  198. package/src/memory/task-memory-cleanup.ts +30 -11
  199. package/src/notifications/copy-composer.ts +86 -0
  200. package/src/notifications/decision-engine.ts +35 -0
  201. package/src/permissions/checker.ts +12 -1
  202. package/src/permissions/permission-mode-store.ts +180 -0
  203. package/src/permissions/permission-mode.ts +31 -0
  204. package/src/permissions/workspace-policy.ts +9 -0
  205. package/src/prompts/system-prompt.ts +59 -7
  206. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  207. package/src/prompts/templates/BOOTSTRAP.md +70 -165
  208. package/src/prompts/templates/HEARTBEAT.md +3 -1
  209. package/src/prompts/templates/SOUL.md +25 -4
  210. package/src/prompts/templates/UPDATES.md +8 -0
  211. package/src/providers/anthropic/client.ts +107 -219
  212. package/src/runtime/auth/route-policy.ts +23 -0
  213. package/src/runtime/http-server.ts +32 -2
  214. package/src/runtime/http-types.ts +12 -1
  215. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  216. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  217. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  218. package/src/runtime/routes/app-management-routes.ts +1 -11
  219. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  220. package/src/runtime/routes/archive-utils.ts +29 -0
  221. package/src/runtime/routes/avatar-routes.ts +2 -9
  222. package/src/runtime/routes/btw-routes.ts +14 -1
  223. package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
  224. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  225. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  226. package/src/runtime/routes/conversation-routes.ts +264 -44
  227. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  228. package/src/runtime/routes/identity-routes.ts +53 -18
  229. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  230. package/src/runtime/routes/log-export-routes.ts +23 -275
  231. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  232. package/src/runtime/routes/migration-routes.ts +18 -7
  233. package/src/runtime/routes/profiler-routes.ts +350 -0
  234. package/src/runtime/routes/schedule-routes.ts +27 -12
  235. package/src/runtime/routes/settings-routes.ts +95 -8
  236. package/src/runtime/routes/subagents-routes.ts +28 -7
  237. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  238. package/src/runtime/routes/user-routes.ts +41 -0
  239. package/src/runtime/routes/workspace-routes.ts +0 -1
  240. package/src/schedule/schedule-store.ts +30 -0
  241. package/src/schedule/scheduler.ts +45 -18
  242. package/src/skills/catalog-install.ts +10 -2
  243. package/src/skills/managed-store.ts +2 -2
  244. package/src/skills/skill-memory.ts +1 -293
  245. package/src/subagent/index.ts +13 -3
  246. package/src/subagent/manager.ts +308 -29
  247. package/src/subagent/types.ts +68 -0
  248. package/src/tasks/task-runner.ts +4 -4
  249. package/src/tools/apps/executors.ts +29 -4
  250. package/src/tools/filesystem/list.ts +93 -0
  251. package/src/tools/permission-checker.ts +78 -0
  252. package/src/tools/registry.ts +4 -0
  253. package/src/tools/schedule/create.ts +3 -0
  254. package/src/tools/schedule/list.ts +1 -0
  255. package/src/tools/schedule/update.ts +6 -0
  256. package/src/tools/shared/filesystem/errors.ts +5 -0
  257. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  258. package/src/tools/shared/filesystem/types.ts +17 -0
  259. package/src/tools/shared/shell-output.ts +31 -2
  260. package/src/tools/subagent/abort.ts +12 -2
  261. package/src/tools/subagent/message.ts +9 -2
  262. package/src/tools/subagent/notify-parent.ts +79 -0
  263. package/src/tools/subagent/read.ts +29 -8
  264. package/src/tools/subagent/resolve.ts +21 -0
  265. package/src/tools/subagent/spawn.ts +2 -0
  266. package/src/tools/subagent/status.ts +11 -1
  267. package/src/tools/system/avatar-generator.ts +3 -3
  268. package/src/tools/system/register.ts +23 -0
  269. package/src/tools/system/set-permission-mode.ts +103 -0
  270. package/src/tools/terminal/parser.ts +30 -5
  271. package/src/tools/terminal/safe-env.ts +16 -1
  272. package/src/tools/tool-manifest.ts +6 -0
  273. package/src/tools/types.ts +2 -0
  274. package/src/util/logger.ts +1 -1
  275. package/src/util/platform.ts +50 -17
  276. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  277. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  278. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  279. package/src/workspace/migrations/029-seed-pkb.ts +84 -0
  280. package/src/workspace/migrations/registry.ts +4 -0
  281. package/src/workspace/top-level-renderer.ts +5 -9
  282. package/src/__tests__/cli-memory.test.ts +0 -377
  283. package/src/__tests__/clipboard.test.ts +0 -88
  284. package/src/cli/cli-memory.ts +0 -179
  285. package/src/util/clipboard.ts +0 -34
@@ -1,14 +1,24 @@
1
1
  import { getMessages } from "../../memory/conversation-crud.js";
2
2
  import { getSubagentManager, TERMINAL_STATUSES } from "../../subagent/index.js";
3
3
  import type { ToolContext, ToolExecutionResult } from "../types.js";
4
+ import { resolveSubagentId } from "./resolve.js";
4
5
 
5
6
  export async function executeSubagentRead(
6
7
  input: Record<string, unknown>,
7
8
  context: ToolContext,
8
9
  ): Promise<ToolExecutionResult> {
9
- const subagentId = input.subagent_id as string;
10
+ const subagentId = resolveSubagentId(input, context);
11
+ if (!subagentId && input.label) {
12
+ return {
13
+ content: `No subagent found with label "${input.label as string}".`,
14
+ isError: true,
15
+ };
16
+ }
10
17
  if (!subagentId) {
11
- return { content: '"subagent_id" is required.', isError: true };
18
+ return {
19
+ content: '"subagent_id" or "label" is required.',
20
+ isError: true,
21
+ };
12
22
  }
13
23
 
14
24
  const manager = getSubagentManager();
@@ -45,32 +55,43 @@ export async function executeSubagentRead(
45
55
  }
46
56
 
47
57
  // Extract assistant messages only - that's the subagent's output.
48
- const output: string[] = [];
58
+ // Group text blocks by message so last_n slices messages, not blocks.
59
+ const messageTexts: string[] = [];
49
60
  for (const msg of dbMessages) {
50
61
  if (msg.role !== "assistant") continue;
62
+ const blocks: string[] = [];
51
63
  try {
52
64
  const content = JSON.parse(msg.content);
53
65
  if (Array.isArray(content)) {
54
66
  for (const block of content) {
55
67
  if (block.type === "text" && typeof block.text === "string") {
56
- output.push(block.text);
68
+ blocks.push(block.text);
57
69
  }
58
70
  }
59
71
  } else if (typeof content === "string") {
60
- output.push(content);
72
+ blocks.push(content);
61
73
  }
62
74
  } catch {
63
75
  // Content might be plain text.
64
- output.push(msg.content);
76
+ blocks.push(msg.content);
77
+ }
78
+ if (blocks.length > 0) {
79
+ messageTexts.push(blocks.join("\n\n"));
65
80
  }
66
81
  }
67
82
 
68
- if (output.length === 0) {
83
+ if (messageTexts.length === 0) {
69
84
  return { content: "Subagent produced no text output.", isError: false };
70
85
  }
71
86
 
87
+ const lastN =
88
+ typeof input.last_n === "number" && input.last_n > 0
89
+ ? input.last_n
90
+ : undefined;
91
+ const sliced = lastN ? messageTexts.slice(-lastN) : messageTexts;
92
+
72
93
  return {
73
- content: output.join("\n\n"),
94
+ content: sliced.join("\n\n"),
74
95
  isError: false,
75
96
  };
76
97
  }
@@ -0,0 +1,21 @@
1
+ import { getSubagentManager } from "../../subagent/index.js";
2
+ import type { ToolContext } from "../types.js";
3
+
4
+ /**
5
+ * Resolve a subagent ID from tool input.
6
+ * Accepts either `subagent_id` (direct UUID) or `label` (case-insensitive lookup).
7
+ */
8
+ export function resolveSubagentId(
9
+ input: Record<string, unknown>,
10
+ context: ToolContext,
11
+ ): string | undefined {
12
+ if (input.subagent_id) return input.subagent_id as string;
13
+ if (input.label) {
14
+ const state = getSubagentManager().getByLabel(
15
+ input.label as string,
16
+ context.conversationId,
17
+ );
18
+ return state?.config.id;
19
+ }
20
+ return undefined;
21
+ }
@@ -9,6 +9,7 @@ export async function executeSubagentSpawn(
9
9
  const objective = input.objective as string;
10
10
  const extraContext = input.context as string | undefined;
11
11
  const sendResultToUser = input.send_result_to_user !== false;
12
+ const role = (input.role as string | undefined) ?? undefined;
12
13
 
13
14
  if (!label || !objective) {
14
15
  return {
@@ -36,6 +37,7 @@ export async function executeSubagentSpawn(
36
37
  objective,
37
38
  context: extraContext,
38
39
  sendResultToUser,
40
+ ...(role ? { role: role as import("../../subagent/types.js").SubagentRole } : {}),
39
41
  },
40
42
  sendToClient as (msg: unknown) => void,
41
43
  );
@@ -1,13 +1,23 @@
1
1
  import { getSubagentManager } from "../../subagent/index.js";
2
2
  import type { ToolContext, ToolExecutionResult } from "../types.js";
3
+ import { resolveSubagentId } from "./resolve.js";
3
4
 
4
5
  export async function executeSubagentStatus(
5
6
  input: Record<string, unknown>,
6
7
  context: ToolContext,
7
8
  ): Promise<ToolExecutionResult> {
8
- const subagentId = input.subagent_id as string | undefined;
9
+ const subagentId = resolveSubagentId(input, context);
9
10
  const manager = getSubagentManager();
10
11
 
12
+ // If a label was provided but didn't resolve, that's an error — don't fall
13
+ // through to the "list all" path.
14
+ if (!subagentId && input.label) {
15
+ return {
16
+ content: `No subagent found with label "${input.label as string}".`,
17
+ isError: true,
18
+ };
19
+ }
20
+
11
21
  if (subagentId) {
12
22
  const state = manager.getState(subagentId);
13
23
  if (
@@ -1,17 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync, renameSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
3
+ import { dirname } from "node:path";
4
4
 
5
5
  import { generateAvatar } from "../../media/avatar-router.js";
6
6
  import { mapGeminiError } from "../../media/gemini-image-service.js";
7
7
  import { getLogger } from "../../util/logger.js";
8
- import { getWorkspaceDir } from "../../util/platform.js";
8
+ import { getAvatarImagePath } from "../../util/platform.js";
9
9
 
10
10
  const log = getLogger("avatar-generator");
11
11
 
12
12
  /** Canonical path where the custom avatar PNG is stored. */
13
13
  function getAvatarPath(): string {
14
- return join(getWorkspaceDir(), "data", "avatar", "avatar-image.png");
14
+ return getAvatarImagePath();
15
15
  }
16
16
 
17
17
  export interface AvatarGenerationResult {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Registers feature-flag-gated system tools with the daemon's tool registry.
3
+ *
4
+ * Called once at daemon startup via initializeTools(). Tools that are always
5
+ * registered (e.g. request_system_permission) are handled via the tool
6
+ * manifest's explicit tools list; this module handles conditional registration.
7
+ */
8
+
9
+ import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
10
+ import { getConfig } from "../../config/loader.js";
11
+ import { registerTool } from "../registry.js";
12
+ import { setPermissionModeTool } from "./set-permission-mode.js";
13
+
14
+ export function registerSystemTools(): void {
15
+ try {
16
+ const config = getConfig();
17
+ if (isAssistantFeatureFlagEnabled("permission-controls-v2", config)) {
18
+ registerTool(setPermissionModeTool);
19
+ }
20
+ } catch {
21
+ // Config not yet loaded (e.g. during test setup) — permission mode tool stays off.
22
+ }
23
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * System tool: set_permission_mode
3
+ *
4
+ * Allows the LLM to deterministically switch permission mode axes via the
5
+ * PermissionModeStore rather than relying on model-generated text.
6
+ *
7
+ * This tool is always available (no permission check required) when the
8
+ * `permission-controls-v2` feature flag is enabled — it IS the permission
9
+ * mechanism.
10
+ */
11
+
12
+ import {
13
+ getMode,
14
+ setAskBeforeActing,
15
+ setHostAccess,
16
+ } from "../../permissions/permission-mode-store.js";
17
+ import { RiskLevel } from "../../permissions/types.js";
18
+ import type { ToolDefinition } from "../../providers/types.js";
19
+ import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
20
+
21
+ class SetPermissionModeTool implements Tool {
22
+ name = "set_permission_mode";
23
+ description =
24
+ "Change the assistant's permission mode. Supports partial updates — " +
25
+ "only the provided fields are changed. Use this to toggle whether the " +
26
+ "assistant asks before acting or whether host access is enabled.";
27
+ category = "system";
28
+ defaultRiskLevel = RiskLevel.Low;
29
+
30
+ getDefinition(): ToolDefinition {
31
+ return {
32
+ name: this.name,
33
+ description: this.description,
34
+ input_schema: {
35
+ type: "object",
36
+ properties: {
37
+ askBeforeActing: {
38
+ type: "boolean",
39
+ description:
40
+ "When true, the assistant checks in with the user before taking actions.",
41
+ },
42
+ hostAccess: {
43
+ type: "boolean",
44
+ description:
45
+ "When true, the assistant can execute commands on the host machine without prompting.",
46
+ },
47
+ },
48
+ },
49
+ };
50
+ }
51
+
52
+ async execute(
53
+ input: Record<string, unknown>,
54
+ _context: ToolContext,
55
+ ): Promise<ToolExecutionResult> {
56
+ const { askBeforeActing, hostAccess } = input;
57
+
58
+ // Validate that at least one field is provided
59
+ if (askBeforeActing === undefined && hostAccess === undefined) {
60
+ return {
61
+ content:
62
+ "Error: at least one of askBeforeActing or hostAccess must be provided.",
63
+ isError: true,
64
+ };
65
+ }
66
+
67
+ // Validate types of provided fields
68
+ if (askBeforeActing !== undefined && typeof askBeforeActing !== "boolean") {
69
+ return {
70
+ content: `Error: askBeforeActing must be a boolean, got ${typeof askBeforeActing}.`,
71
+ isError: true,
72
+ };
73
+ }
74
+ if (hostAccess !== undefined && typeof hostAccess !== "boolean") {
75
+ return {
76
+ content: `Error: hostAccess must be a boolean, got ${typeof hostAccess}.`,
77
+ isError: true,
78
+ };
79
+ }
80
+
81
+ // Apply changes for provided fields
82
+ if (typeof askBeforeActing === "boolean") {
83
+ setAskBeforeActing(askBeforeActing);
84
+ }
85
+
86
+ if (typeof hostAccess === "boolean") {
87
+ setHostAccess(hostAccess);
88
+ }
89
+
90
+ // Return confirmation with the new state
91
+ const mode = getMode();
92
+ return {
93
+ content: [
94
+ "Permission mode updated.",
95
+ ` askBeforeActing: ${mode.askBeforeActing}`,
96
+ ` hostAccess: ${mode.hostAccess}`,
97
+ ].join("\n"),
98
+ isError: false,
99
+ };
100
+ }
101
+ }
102
+
103
+ export const setPermissionModeTool = new SetPermissionModeTool();
@@ -49,9 +49,29 @@ const SCRIPT_INTERPRETERS = new Set([
49
49
  "deno",
50
50
  "bun",
51
51
  ]);
52
- // Flags that make an interpreter execute code from an inline argument or stdin
53
- // rather than from a file (e.g. `python -c 'code'`, `node -e 'code'`).
54
- const STDIN_EXEC_FLAGS = new Set(["-c", "-e", "-"]);
52
+ // Flags that make an interpreter read code from stdin rather than from a file.
53
+ const STDIN_EXEC_FLAGS = new Set(["-"]);
54
+ // Per-interpreter flags that provide code inline as an argument (e.g.
55
+ // `python -c 'code'`, `node -e 'code'`). When these are present the
56
+ // interpreter is NOT reading code from stdin — stdin is just data, so piping
57
+ // into the interpreter is no more dangerous than piping into grep or jq.
58
+ //
59
+ // This must be interpreter-specific because the same flag can mean different
60
+ // things across interpreters. For example, `perl -c` is syntax-check mode
61
+ // (still reads code from stdin and executes BEGIN blocks), while
62
+ // `python -c` provides inline code. Similarly, `ruby -c` is syntax-check.
63
+ const INTERPRETER_INLINE_CODE_FLAGS: ReadonlyMap<
64
+ string,
65
+ ReadonlySet<string>
66
+ > = new Map([
67
+ ["python", new Set(["-c"])],
68
+ ["python3", new Set(["-c"])],
69
+ ["ruby", new Set(["-e"])],
70
+ ["perl", new Set(["-e"])],
71
+ ["node", new Set(["-e"])],
72
+ ["deno", new Set(["-e"])],
73
+ ["bun", new Set(["-e"])],
74
+ ]);
55
75
  // Per-interpreter flags that consume the next argument as a value (not a filename).
56
76
  // Mapped by interpreter name since flags differ across interpreters
57
77
  // (e.g. -I is standalone in Python but takes a value in Ruby).
@@ -353,18 +373,23 @@ function extractSegments(node: TSNode): CommandSegment[] {
353
373
 
354
374
  /**
355
375
  * Returns true when the interpreter args indicate stdin-exec mode - i.e. the
356
- * interpreter will read code from stdin (or from an inline -c/-e argument)
357
- * rather than from a file. Concretely:
376
+ * interpreter will read code from stdin rather than from a file. Concretely:
358
377
  * - Any STDIN_EXEC_FLAGS present → stdin-exec
378
+ * - Interpreter-specific inline code flag (-c/-e) → NOT stdin-exec
359
379
  * - No positional (non-flag) arguments at all → stdin-exec (bare `python`)
360
380
  * - Otherwise the first positional arg is a filename → NOT stdin-exec
361
381
  */
362
382
  function isStdinExecMode(interpreter: string, args: string[]): boolean {
363
383
  const valueFlags =
364
384
  INTERPRETER_VALUE_FLAGS.get(interpreter) ?? new Set<string>();
385
+ const inlineCodeFlags =
386
+ INTERPRETER_INLINE_CODE_FLAGS.get(interpreter) ?? new Set<string>();
365
387
  for (let i = 0; i < args.length; i++) {
366
388
  const arg = args[i];
367
389
  if (STDIN_EXEC_FLAGS.has(arg)) return true;
390
+ // Interpreter-specific inline code flags (e.g. python -c, node -e) mean
391
+ // the code is provided as an argument, not read from stdin.
392
+ if (inlineCodeFlags.has(arg)) return false;
368
393
  // First non-flag argument is a filename/module → file mode
369
394
  if (!arg.startsWith("-")) return false;
370
395
  // Flags like -W, -X consume the next token as their value - skip it
@@ -8,7 +8,7 @@
8
8
  import { getGatewayInternalBaseUrl } from "../../config/env.js";
9
9
  import { getDataDir, getWorkspaceDir } from "../../util/platform.js";
10
10
 
11
- const SAFE_ENV_VARS = [
11
+ export const SAFE_ENV_VARS = [
12
12
  "PATH",
13
13
  "HOME",
14
14
  "TERM",
@@ -37,6 +37,21 @@ const SAFE_ENV_VARS = [
37
37
  "IS_CONTAINERIZED",
38
38
  "IS_PLATFORM",
39
39
  "CES_SERVICE_TOKEN",
40
+ "VELLUM_PROFILER_RUN_ID",
41
+ "VELLUM_PROFILER_MODE",
42
+ "VELLUM_PROFILER_MAX_BYTES",
43
+ "VELLUM_PROFILER_MAX_RUNS",
44
+ "VELLUM_PROFILER_MIN_FREE_MB",
45
+ ] as const;
46
+
47
+ /**
48
+ * Keys that buildSanitizedEnv always injects into the returned env,
49
+ * independent of what is present in process.env.
50
+ */
51
+ export const ALWAYS_INJECTED_ENV_VARS = [
52
+ "INTERNAL_GATEWAY_BASE_URL",
53
+ "VELLUM_DATA_DIR",
54
+ "VELLUM_WORKSPACE_DIR",
40
55
  ] as const;
41
56
 
42
57
  export function buildSanitizedEnv(): Record<string, string> {
@@ -16,6 +16,7 @@ import { manageSecureCommandTool } from "./credential-execution/manage-secure-co
16
16
  import { runAuthenticatedCommandTool } from "./credential-execution/run-authenticated-command.js";
17
17
  import { credentialStoreTool } from "./credentials/vault.js";
18
18
  import { fileEditTool } from "./filesystem/edit.js";
19
+ import { fileListTool } from "./filesystem/list.js";
19
20
  import { fileReadTool } from "./filesystem/read.js";
20
21
  import { fileWriteTool } from "./filesystem/write.js";
21
22
  import { recallTool, rememberTool } from "./memory/register.js";
@@ -23,6 +24,7 @@ import { webFetchTool } from "./network/web-fetch.js";
23
24
  import { webSearchTool } from "./network/web-search.js";
24
25
  import { skillExecuteTool } from "./skills/execute.js";
25
26
  import { skillLoadTool } from "./skills/load.js";
27
+ import { notifyParentTool } from "./subagent/notify-parent.js";
26
28
  import { requestSystemPermissionTool } from "./system/request-permission.js";
27
29
  import { shellTool } from "./terminal/shell.js";
28
30
  import type { Tool } from "./types.js";
@@ -56,11 +58,13 @@ export const eagerModuleToolNames: string[] = [
56
58
  "file_read",
57
59
  "file_write",
58
60
  "file_edit",
61
+ "file_list",
59
62
  "web_search",
60
63
  "web_fetch",
61
64
  "skill_execute",
62
65
  "skill_load",
63
66
  "request_system_permission",
67
+ "notify_parent",
64
68
  ];
65
69
 
66
70
  // ── Explicit tool instances ─────────────────────────────────────────
@@ -76,6 +80,7 @@ export const explicitTools: Tool[] = [
76
80
  fileReadTool,
77
81
  fileWriteTool,
78
82
  fileEditTool,
83
+ fileListTool,
79
84
  webFetchTool,
80
85
  webSearchTool,
81
86
  skillExecuteTool,
@@ -85,6 +90,7 @@ export const explicitTools: Tool[] = [
85
90
  rememberTool,
86
91
  recallTool,
87
92
  credentialStoreTool,
93
+ notifyParentTool,
88
94
  ];
89
95
 
90
96
  // ── CES tools (feature-flag gated) ──────────────────────────────────
@@ -181,6 +181,8 @@ export interface ToolContext {
181
181
  hostBashProxy?: import("../daemon/host-bash-proxy.js").HostBashProxy;
182
182
  /** Optional proxy for delegating host_file_read/write/edit execution to a connected client (managed/cloud-hosted mode). */
183
183
  hostFileProxy?: import("../daemon/host-file-proxy.js").HostFileProxy;
184
+ /** True when the assistant is running as a platform-managed remote instance. Used to auto-approve sandboxed bash tools. */
185
+ isPlatformHosted?: boolean;
184
186
  /** CES RPC client for credential execution operations. When present, the executor can bridge CES approval flows. */
185
187
  cesClient?: CesClient;
186
188
  }
@@ -36,7 +36,7 @@ export type LogFileConfig = {
36
36
 
37
37
  const LOG_FILE_PREFIX = "assistant-";
38
38
  const LOG_FILE_SUFFIX = ".log";
39
- const LOG_FILE_PATTERN = /^assistant-(\d{4}-\d{2}-\d{2})\.log$/;
39
+ export const LOG_FILE_PATTERN = /^assistant-(\d{4}-\d{2}-\d{2})\.log$/;
40
40
 
41
41
  function formatDate(date: Date): string {
42
42
  const y = date.getUTCFullYear();
@@ -25,16 +25,6 @@ export function getPlatformName(): string {
25
25
  return process.platform;
26
26
  }
27
27
 
28
- /**
29
- * Returns the platform-specific clipboard copy command, or null if
30
- * clipboard access is not supported on the current platform.
31
- */
32
- export function getClipboardCommand(): string | null {
33
- if (isMacOS()) return "pbcopy";
34
- if (isLinux()) return "xclip -selection clipboard";
35
- return null;
36
- }
37
-
38
28
  /**
39
29
  * Normalize an assistant ID to its canonical form for DB operations.
40
30
  *
@@ -105,13 +95,25 @@ export function getInterfacesDir(): string {
105
95
 
106
96
  /**
107
97
  * Returns the sounds directory (~/.vellum/workspace/data/sounds).
108
- * Custom sound files and sound configuration live here. Sound files
109
- * can be large, so this directory is excluded from diagnostic exports.
98
+ * Custom sound files and sound configuration live here.
110
99
  */
111
100
  export function getSoundsDir(): string {
112
101
  return join(getWorkspaceDir(), "data", "sounds");
113
102
  }
114
103
 
104
+ /** Returns the avatar directory ($VELLUM_WORKSPACE_DIR/data/avatar). */
105
+ export function getAvatarDir(): string {
106
+ return join(getWorkspaceDir(), "data", "avatar");
107
+ }
108
+
109
+ /** Canonical filename for the custom avatar PNG. */
110
+ export const AVATAR_IMAGE_FILENAME = "avatar-image.png";
111
+
112
+ /** Returns the canonical avatar image path (~/.vellum/workspace/data/avatar/avatar-image.png). */
113
+ export function getAvatarImagePath(): string {
114
+ return join(getAvatarDir(), AVATAR_IMAGE_FILENAME);
115
+ }
116
+
115
117
  /**
116
118
  * Returns the TCP port the daemon should listen on for iOS clients.
117
119
  * Hardcoded default: 8765.
@@ -218,7 +220,7 @@ export function getHistoryPath(): string {
218
220
  * overrides, device approval lists — live here.
219
221
  *
220
222
  * This directory is:
221
- * - Outside the workspace (not included in diagnostic exports)
223
+ * - Outside the workspace
222
224
  * - Outside the sandbox write boundary (tools cannot modify it)
223
225
  * - Skipped in containerized mode (credentials via CES, trust via gateway)
224
226
  */
@@ -271,10 +273,6 @@ export function getEmbedWorkerPidPath(): string {
271
273
  * When the VELLUM_WORKSPACE_DIR env var is set, returns that value (used in
272
274
  * containerized deployments where the workspace is a separate volume).
273
275
  * Otherwise falls back to ~/.vellum/workspace.
274
- *
275
- * WARNING: The entire workspace directory is included in diagnostic log exports
276
- * ("Send logs to Vellum"). Do not store secrets, API keys, or sensitive
277
- * credentials here — use the credential store or ~/.vellum/protected/ instead.
278
276
  */
279
277
  export function getWorkspaceDir(): string {
280
278
  const override = getWorkspaceDirOverride();
@@ -315,6 +313,11 @@ export function getWorkspaceHooksDir(): string {
315
313
  return join(getWorkspaceDir(), "hooks");
316
314
  }
317
315
 
316
+ /** Returns $VELLUM_WORKSPACE_DIR/routes — user-defined HTTP route handlers. */
317
+ export function getWorkspaceRoutesDir(): string {
318
+ return join(getWorkspaceDir(), "routes");
319
+ }
320
+
318
321
  /** Returns ~/.vellum/workspace/deprecated — transitional files slated for removal. */
319
322
  export function getDeprecatedDir(): string {
320
323
  return join(getWorkspaceDir(), "deprecated");
@@ -330,6 +333,35 @@ export function getWorkspacePromptPath(file: string): string {
330
333
  return join(getWorkspaceDir(), file);
331
334
  }
332
335
 
336
+ // ── Profiler filesystem layout ──────────────────────────────────────────
337
+ // Managed profiler runs live under <workspace>/data/profiler/. These
338
+ // helpers enforce a single canonical layout so every runtime caller
339
+ // resolves the same paths.
340
+
341
+ /**
342
+ * Returns the profiler root directory (<workspace>/data/profiler).
343
+ * All profiler state (runs directory, global metadata) lives here.
344
+ */
345
+ export function getProfilerRootDir(): string {
346
+ return join(getDataDir(), "profiler");
347
+ }
348
+
349
+ /**
350
+ * Returns the profiler runs directory (<workspace>/data/profiler/runs).
351
+ * Each completed or active profiler run gets its own sub-directory here.
352
+ */
353
+ export function getProfilerRunsDir(): string {
354
+ return join(getProfilerRootDir(), "runs");
355
+ }
356
+
357
+ /**
358
+ * Returns the directory for a specific profiler run by ID
359
+ * (<workspace>/data/profiler/runs/<runId>).
360
+ */
361
+ export function getProfilerRunDir(runId: string): string {
362
+ return join(getProfilerRunsDir(), runId);
363
+ }
364
+
333
365
  export function ensureDataDir(): void {
334
366
  const root = vellumRoot();
335
367
  const workspace = getWorkspaceDir();
@@ -342,6 +374,7 @@ export function ensureDataDir(): void {
342
374
  join(workspace, "signals"),
343
375
  join(workspace, "hooks"),
344
376
  join(workspace, "skills"),
377
+ join(workspace, "routes"),
345
378
  join(workspace, "embedding-models"),
346
379
  join(workspace, "conversations"),
347
380
  join(workspace, "logs"),
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Previously, dictation-profiles.json, email-guardrails.json, and
5
5
  * active-call-leases.json lived directly under getRootDir() (~/.vellum/).
6
- * This migration moves them into the workspace directory so they are
7
- * included in diagnostic exports and follow the workspace convention.
6
+ * This migration moves them into the workspace directory so they follow
7
+ * the workspace convention for organizational consistency.
8
8
  */
9
9
 
10
10
  import { existsSync, renameSync, unlinkSync } from "node:fs";
@@ -40,8 +40,8 @@ function getRootDir(): string {
40
40
  const FILE_MOVES: Array<{ name: string; subdir?: string }> = [
41
41
  { name: "daemon-stderr.log", subdir: "logs" },
42
42
  { name: "daemon-startup.lock" },
43
- // .env stays at root — it contains secrets (API keys) and the entire
44
- // workspace directory is included in diagnostic log exports.
43
+ // .env stays at root — it contains secrets (API keys) and should not
44
+ // be in the sandbox working directory.
45
45
  { name: "embed-worker.pid" },
46
46
  ];
47
47