@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
@@ -0,0 +1,93 @@
1
+ import { RiskLevel } from "../../permissions/types.js";
2
+ import type { ToolDefinition } from "../../providers/types.js";
3
+ import { registerTool } from "../registry.js";
4
+ import { FileSystemOps } from "../shared/filesystem/file-ops-service.js";
5
+ import { sandboxPolicy } from "../shared/filesystem/path-policy.js";
6
+ import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
7
+
8
+ class FileListTool implements Tool {
9
+ name = "file_list";
10
+ description =
11
+ "List the contents of a directory. Returns file and subdirectory names with type indicators and sizes.";
12
+ category = "filesystem";
13
+ defaultRiskLevel = RiskLevel.Low;
14
+
15
+ getDefinition(): ToolDefinition {
16
+ return {
17
+ name: this.name,
18
+ description: this.description,
19
+ input_schema: {
20
+ type: "object",
21
+ properties: {
22
+ path: {
23
+ type: "string",
24
+ description: "The directory path to list",
25
+ },
26
+ glob: {
27
+ type: "string",
28
+ description: "Filter entries by glob pattern, e.g. '*.md'",
29
+ },
30
+ activity: {
31
+ type: "string",
32
+ description:
33
+ "Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
34
+ },
35
+ },
36
+ required: ["path", "activity"],
37
+ },
38
+ };
39
+ }
40
+
41
+ async execute(
42
+ input: Record<string, unknown>,
43
+ context: ToolContext,
44
+ ): Promise<ToolExecutionResult> {
45
+ const rawPath = input.path as string;
46
+ if (!rawPath || typeof rawPath !== "string") {
47
+ return {
48
+ content: "Error: path is required and must be a string",
49
+ isError: true,
50
+ };
51
+ }
52
+
53
+ const ops = new FileSystemOps((path, opts) =>
54
+ sandboxPolicy(path, context.workingDir, opts),
55
+ );
56
+
57
+ const result = ops.listDirSafe({
58
+ path: rawPath,
59
+ glob: typeof input.glob === "string" ? input.glob : undefined,
60
+ });
61
+
62
+ if (!result.ok) {
63
+ const { error } = result;
64
+ switch (error.code) {
65
+ case "NOT_A_DIRECTORY":
66
+ return {
67
+ content: `Error: ${error.path} is not a directory`,
68
+ isError: true,
69
+ };
70
+ case "NOT_FOUND":
71
+ return {
72
+ content: `Error: directory not found: ${error.path}`,
73
+ isError: true,
74
+ };
75
+ default: {
76
+ const hint =
77
+ error.code === "PATH_OUT_OF_BOUNDS"
78
+ ? ". To list files outside the workspace, use the host_bash tool instead."
79
+ : "";
80
+ return {
81
+ content: `Error: ${error.message}${hint}`,
82
+ isError: true,
83
+ };
84
+ }
85
+ }
86
+ }
87
+
88
+ return { content: result.value.listing, isError: false };
89
+ }
90
+ }
91
+
92
+ export const fileListTool = new FileListTool();
93
+ registerTool(fileListTool);
@@ -1,3 +1,4 @@
1
+ import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
1
2
  import { getConfig } from "../config/loader.js";
2
3
  import { getHookManager } from "../hooks/manager.js";
3
4
  import {
@@ -6,9 +7,11 @@ import {
6
7
  generateAllowlistOptions,
7
8
  generateScopeOptions,
8
9
  } from "../permissions/checker.js";
10
+ import { getMode } from "../permissions/permission-mode-store.js";
9
11
  import type { PermissionPrompter } from "../permissions/prompter.js";
10
12
  import { addRule } from "../permissions/trust-store.js";
11
13
  import { RiskLevel } from "../permissions/types.js";
14
+ import { isHostTool } from "../permissions/workspace-policy.js";
12
15
  import {
13
16
  getEffectiveMode,
14
17
  setConversationMode,
@@ -65,6 +68,52 @@ export class PermissionChecker {
65
68
  }
66
69
  | undefined,
67
70
  ): Promise<PermissionDecision> {
71
+ // ── permission-controls-v2 early gate ──────────────────────────────
72
+ // When the v2 flag is enabled, replace the entire risk-classification
73
+ // path with a simple binary check: is it a host tool + is host access
74
+ // enabled? Certain security gates (requireFreshApproval,
75
+ // forcePromptSideEffects, hostAccess=false) fall through to the v1
76
+ // prompt flow so the interactive prompter is engaged.
77
+ const cfg = getConfig();
78
+ let v2ForcePrompt = false;
79
+ if (isAssistantFeatureFlagEnabled("permission-controls-v2", cfg)) {
80
+ // requireFreshApproval demands an interactive prompt every time —
81
+ // fall through to v1 so the prompter is engaged.
82
+ const needsFreshApproval = !!context.requireFreshApproval;
83
+
84
+ // forcePromptSideEffects (private conversations, untrusted actors)
85
+ // requires explicit approval for side-effect tools.
86
+ const needsSideEffectPrompt =
87
+ !!context.forcePromptSideEffects && isSideEffectTool(name, input);
88
+
89
+ if (!needsFreshApproval && !needsSideEffectPrompt) {
90
+ if (isHostTool(name)) {
91
+ const mode = getMode();
92
+ if (mode.hostAccess) {
93
+ return {
94
+ allowed: true,
95
+ decision: "allow",
96
+ riskLevel: RiskLevel.Low,
97
+ };
98
+ }
99
+ // Host tool with hostAccess disabled — fall through to v1 so the
100
+ // interactive prompter is engaged (returning allowed:false here
101
+ // would surface an error string instead of a permission dialog).
102
+ // The v2ForcePrompt flag ensures check()'s allow decision is
103
+ // promoted to prompt so the user sees a permission dialog.
104
+ v2ForcePrompt = true;
105
+ } else {
106
+ // Non-host tools are auto-allowed when v2 is on
107
+ return {
108
+ allowed: true,
109
+ decision: "allow",
110
+ riskLevel: RiskLevel.Low,
111
+ };
112
+ }
113
+ }
114
+ // Falls through to the v1 risk-classification + prompter path
115
+ }
116
+
68
117
  const risk = await classifyRisk(
69
118
  name,
70
119
  input,
@@ -114,6 +163,14 @@ export class PermissionChecker {
114
163
  "Fresh approval required: per-invocation human review enforced";
115
164
  }
116
165
 
166
+ // v2 host-access-disabled: the v2 gate fell through because
167
+ // hostAccess is off. Promote allow → prompt so the user sees an
168
+ // interactive permission dialog instead of an error string.
169
+ if (v2ForcePrompt && result.decision === "allow") {
170
+ result.decision = "prompt";
171
+ result.reason = "Host access disabled: requires explicit approval";
172
+ }
173
+
117
174
  if (result.decision === "deny") {
118
175
  const durationMs = Date.now() - startTime;
119
176
  emitLifecycleEvent({
@@ -137,6 +194,26 @@ export class PermissionChecker {
137
194
  };
138
195
  }
139
196
 
197
+ // Platform-hosted mode: auto-approve sandboxed bash for guardians.
198
+ // The sandbox provides the security boundary — prompting is unnecessary
199
+ // friction. host_bash is excluded because it runs unsandboxed on the
200
+ // user's machine and warrants explicit approval.
201
+ // Deny rules are still respected (checked above). requireFreshApproval
202
+ // is preserved as a belt-and-suspenders guard.
203
+ if (
204
+ result.decision === "prompt" &&
205
+ context.isPlatformHosted &&
206
+ name === "bash" &&
207
+ context.trustClass === "guardian" &&
208
+ !context.requireFreshApproval
209
+ ) {
210
+ log.info(
211
+ { toolName: name, riskLevel },
212
+ "Auto-approving bash tool for platform-hosted guardian session",
213
+ );
214
+ return { allowed: true, decision: "platform_auto_approve", riskLevel };
215
+ }
216
+
140
217
  if (result.decision === "prompt") {
141
218
  // Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
142
219
  // able to use bundled tools without interactive approval. The guardian
@@ -158,6 +235,7 @@ export class PermissionChecker {
158
235
  context.trustClass === "guardian" &&
159
236
  !context.requireFreshApproval &&
160
237
  !isDynamicSkillLoad &&
238
+ !v2ForcePrompt &&
161
239
  riskLevel !== RiskLevel.High
162
240
  ) {
163
241
  log.info(
@@ -7,6 +7,8 @@ import { hostFileEditTool } from "./host-filesystem/edit.js";
7
7
  import { hostFileReadTool } from "./host-filesystem/read.js";
8
8
  import { hostFileWriteTool } from "./host-filesystem/write.js";
9
9
  import { hostShellTool } from "./host-terminal/host-shell.js";
10
+ import { registerSystemTools } from "./system/register.js";
11
+ import { setPermissionModeTool } from "./system/set-permission-mode.js";
10
12
  import type { Tool } from "./types.js";
11
13
  import { allUiSurfaceTools } from "./ui-surface/definitions.js";
12
14
  import { registerUiSurfaceTools } from "./ui-surface/registry.js";
@@ -264,6 +266,7 @@ export async function initializeTools(): Promise<void> {
264
266
 
265
267
  registerUiSurfaceTools();
266
268
  registerAppTools();
269
+ registerSystemTools();
267
270
 
268
271
  // Snapshot core tools for __resetRegistryForTesting(). We include every
269
272
  // non-skill tool that was registered by the manifest, while excluding
@@ -282,6 +285,7 @@ export async function initializeTools(): Promise<void> {
282
285
  ...allComputerUseTools.map((t: Tool) => t.name),
283
286
  ...allUiSurfaceTools.map((t: Tool) => t.name),
284
287
  ...coreAppProxyTools.map((t: Tool) => t.name),
288
+ setPermissionModeTool.name,
285
289
  ]);
286
290
 
287
291
  coreToolsSnapshot = new Map<string, Tool>();
@@ -42,6 +42,7 @@ export async function executeScheduleCreate(
42
42
  | Record<string, unknown>
43
43
  | undefined;
44
44
  const quiet = (input.quiet as boolean) ?? false;
45
+ const reuseConversation = (input.reuse_conversation as boolean) ?? false;
45
46
 
46
47
  if (!name || typeof name !== "string") {
47
48
  return {
@@ -114,6 +115,7 @@ export async function executeScheduleCreate(
114
115
  routingIntent: routingIntent as RoutingIntent | undefined,
115
116
  routingHints,
116
117
  quiet,
118
+ reuseConversation,
117
119
  });
118
120
 
119
121
  const fireDate = formatLocalDate(job.nextRunAt);
@@ -190,6 +192,7 @@ export async function executeScheduleCreate(
190
192
  routingIntent: routingIntent as RoutingIntent | undefined,
191
193
  routingHints,
192
194
  quiet,
195
+ reuseConversation,
193
196
  });
194
197
 
195
198
  const scheduleDescription =
@@ -65,6 +65,7 @@ export async function executeScheduleList(
65
65
  lines.push(
66
66
  ` Enabled: ${job.enabled}`,
67
67
  ` Quiet: ${job.quiet}`,
68
+ ` Reuse conversation: ${job.reuseConversation}`,
68
69
  ` Message: ${job.message}`,
69
70
  );
70
71
 
@@ -102,6 +102,11 @@ export async function executeScheduleUpdate(
102
102
  updates.quiet = input.quiet;
103
103
  }
104
104
 
105
+ // Conversation reuse
106
+ if (input.reuse_conversation !== undefined) {
107
+ updates.reuseConversation = input.reuse_conversation;
108
+ }
109
+
105
110
  // Auto-detect syntax when expression changes without explicit syntax
106
111
  if (input.expression !== undefined || input.syntax !== undefined) {
107
112
  const resolved = normalizeScheduleSyntax({
@@ -165,6 +170,7 @@ export async function executeScheduleUpdate(
165
170
  routingIntent?: RoutingIntent;
166
171
  routingHints?: Record<string, unknown>;
167
172
  quiet?: boolean;
173
+ reuseConversation?: boolean;
168
174
  },
169
175
  );
170
176
 
@@ -8,6 +8,7 @@ export type FsErrorCode =
8
8
  | "PATH_NOT_ABSOLUTE"
9
9
  | "NOT_FOUND"
10
10
  | "NOT_A_FILE"
11
+ | "NOT_A_DIRECTORY"
11
12
  | "SIZE_LIMIT_EXCEEDED"
12
13
  | "MATCH_NOT_FOUND"
13
14
  | "MATCH_AMBIGUOUS"
@@ -56,6 +57,10 @@ export function notAFile(path: string): FsError {
56
57
  return { code: "NOT_A_FILE", message: `Not a regular file: ${path}`, path };
57
58
  }
58
59
 
60
+ export function notADirectory(path: string): FsError {
61
+ return { code: "NOT_A_DIRECTORY", message: `Not a directory: ${path}`, path };
62
+ }
63
+
59
64
  export function sizeLimitExceeded(path: string, detail: string): FsError {
60
65
  return {
61
66
  code: "SIZE_LIMIT_EXCEEDED",
@@ -1,5 +1,13 @@
1
- import { readFileSync, statSync, writeFileSync } from "node:fs";
2
- import { dirname } from "node:path";
1
+ import {
2
+ lstatSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ statSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { dirname, join } from "node:path";
9
+
10
+ import { minimatch } from "minimatch";
3
11
 
4
12
  import { ensureDir, pathExists } from "../../../util/fs.js";
5
13
  import { applyEdit } from "./edit-engine.js";
@@ -9,6 +17,8 @@ import { checkContentSize, checkFileSizeOnDisk } from "./size-guard.js";
9
17
  import type {
10
18
  EditInput,
11
19
  EditResult,
20
+ ListInput,
21
+ ListResult,
12
22
  ReadInput,
13
23
  ReadResult,
14
24
  WriteInput,
@@ -237,4 +247,82 @@ export class FileSystemOps {
237
247
  },
238
248
  };
239
249
  }
250
+
251
+ // -------------------------------------------------------------------------
252
+ // List
253
+ // -------------------------------------------------------------------------
254
+
255
+ listDirSafe(input: ListInput): ListResult {
256
+ const pathCheck = this.policy(input.path, { mustExist: true });
257
+ if (!pathCheck.ok) {
258
+ return {
259
+ ok: false,
260
+ error: pathError(input.path, pathCheck.reason, pathCheck.error),
261
+ };
262
+ }
263
+ const resolved = pathCheck.resolved;
264
+
265
+ if (!pathExists(resolved)) {
266
+ return { ok: false, error: Err.notFound(resolved) };
267
+ }
268
+
269
+ const stat = statSync(resolved);
270
+ if (!stat.isDirectory()) {
271
+ return { ok: false, error: Err.notADirectory(resolved) };
272
+ }
273
+
274
+ try {
275
+ let entries = readdirSync(resolved, { withFileTypes: true });
276
+
277
+ if (input.glob) {
278
+ const pattern = input.glob;
279
+ entries = entries.filter((e) => minimatch(e.name, pattern));
280
+ }
281
+
282
+ // Sort: directories first (alphabetical), then files (alphabetical)
283
+ const dirs = entries
284
+ .filter((e) => e.isDirectory())
285
+ .sort((a, b) => a.name.localeCompare(b.name));
286
+ const files = entries
287
+ .filter((e) => !e.isDirectory())
288
+ .sort((a, b) => a.name.localeCompare(b.name));
289
+ const sorted = [...dirs, ...files];
290
+
291
+ const MAX_ENTRIES = 500;
292
+ const truncated = sorted.length > MAX_ENTRIES;
293
+ const visible = sorted.slice(0, MAX_ENTRIES);
294
+
295
+ const lines = visible.map((entry) => {
296
+ if (entry.isDirectory()) {
297
+ return `${entry.name}/`;
298
+ }
299
+ if (entry.isSymbolicLink()) {
300
+ return `${entry.name}@`;
301
+ }
302
+ const fileStat = lstatSync(join(resolved, entry.name));
303
+ return `${entry.name} ${formatSize(fileStat.size)}`;
304
+ });
305
+
306
+ if (truncated) {
307
+ lines.push(
308
+ `\n... and ${sorted.length - MAX_ENTRIES} more entries (use glob to filter)`,
309
+ );
310
+ }
311
+
312
+ return { ok: true, value: { listing: lines.join("\n") } };
313
+ } catch (err) {
314
+ const msg = err instanceof Error ? err.message : String(err);
315
+ return { ok: false, error: Err.ioError(resolved, msg) };
316
+ }
317
+ }
318
+ }
319
+
320
+ // ---------------------------------------------------------------------------
321
+ // Helpers
322
+ // ---------------------------------------------------------------------------
323
+
324
+ function formatSize(bytes: number): string {
325
+ if (bytes < 1024) return `${bytes} B`;
326
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
327
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
240
328
  }
@@ -78,3 +78,20 @@ export interface EditOutput {
78
78
  export type EditResult =
79
79
  | { ok: true; value: EditOutput }
80
80
  | { ok: false; error: FsError };
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // List
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export interface ListInput {
87
+ path: string;
88
+ glob?: string;
89
+ }
90
+
91
+ export interface ListOutput {
92
+ listing: string;
93
+ }
94
+
95
+ export type ListResult =
96
+ | { ok: true; value: ListOutput }
97
+ | { ok: false; error: FsError };
@@ -1,4 +1,24 @@
1
- export const MAX_OUTPUT_LENGTH = 50_000;
1
+ import { randomUUID } from "node:crypto";
2
+ import { unlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ export const MAX_OUTPUT_LENGTH = 20_000;
7
+
8
+ /** Tracks temp files created for truncated shell output so they can be cleaned up on shutdown. */
9
+ const trackedTempFiles = new Set<string>();
10
+
11
+ /** Remove all tracked truncated-output temp files. Safe to call multiple times. */
12
+ export function cleanupShellOutputTempFiles(): void {
13
+ for (const filePath of trackedTempFiles) {
14
+ try {
15
+ unlinkSync(filePath);
16
+ } catch {
17
+ // File may already be gone — ignore.
18
+ }
19
+ }
20
+ trackedTempFiles.clear();
21
+ }
2
22
 
3
23
  export interface ShellOutputResult {
4
24
  content: string;
@@ -33,7 +53,16 @@ export function formatShellOutput(
33
53
  }
34
54
 
35
55
  if (output.length > MAX_OUTPUT_LENGTH) {
36
- const msg = '<output_truncated limit="50K" />';
56
+ let fullOutputPath: string | undefined;
57
+ try {
58
+ fullOutputPath = join(tmpdir(), `vellum-shell-output-${randomUUID()}.txt`);
59
+ writeFileSync(fullOutputPath, output, { encoding: "utf-8", mode: 0o600 });
60
+ trackedTempFiles.add(fullOutputPath);
61
+ } catch {
62
+ fullOutputPath = undefined;
63
+ }
64
+ const fileAttr = fullOutputPath ? ` file="${fullOutputPath}"` : "";
65
+ const msg = `<output_truncated limit="20K"${fileAttr} />`;
37
66
  output = output.slice(0, MAX_OUTPUT_LENGTH) + `\n${msg}`;
38
67
  statusParts.push(msg);
39
68
  }
@@ -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 executeSubagentAbort(
5
6
  input: Record<string, unknown>,
6
7
  context: ToolContext,
7
8
  ): Promise<ToolExecutionResult> {
8
- const subagentId = input.subagent_id as string;
9
+ const subagentId = resolveSubagentId(input, context);
10
+ if (!subagentId && input.label) {
11
+ return {
12
+ content: `No subagent found with label "${input.label as string}".`,
13
+ isError: true,
14
+ };
15
+ }
9
16
  if (!subagentId) {
10
- return { content: '"subagent_id" is required.', isError: true };
17
+ return {
18
+ content: '"subagent_id" or "label" is required.',
19
+ isError: true,
20
+ };
11
21
  }
12
22
 
13
23
  const manager = getSubagentManager();
@@ -1,16 +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 executeSubagentMessage(
5
6
  input: Record<string, unknown>,
6
7
  context: ToolContext,
7
8
  ): Promise<ToolExecutionResult> {
8
- const subagentId = input.subagent_id as string;
9
+ const subagentId = resolveSubagentId(input, context);
9
10
  const content = input.content as string;
10
11
 
12
+ if (!subagentId && input.label) {
13
+ return {
14
+ content: `No subagent found with label "${input.label as string}".`,
15
+ isError: true,
16
+ };
17
+ }
11
18
  if (!subagentId || !content) {
12
19
  return {
13
- content: 'Both "subagent_id" and "content" are required.',
20
+ content: '"subagent_id" or "label", and "content" are required.',
14
21
  isError: true,
15
22
  };
16
23
  }
@@ -0,0 +1,79 @@
1
+ import { RiskLevel } from "../../permissions/types.js";
2
+ import type { ToolDefinition } from "../../providers/types.js";
3
+ import { getSubagentManager } from "../../subagent/index.js";
4
+ import { registerTool } from "../registry.js";
5
+ import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
6
+
7
+ export async function executeSubagentNotifyParent(
8
+ input: Record<string, unknown>,
9
+ context: ToolContext,
10
+ ): Promise<ToolExecutionResult> {
11
+ const message = input.message as string;
12
+ const urgency = (input.urgency as string) || "info";
13
+
14
+ if (!message) {
15
+ return { content: '"message" is required.', isError: true };
16
+ }
17
+
18
+ const manager = getSubagentManager();
19
+ const sent = manager.notifyParent(context.conversationId, message, urgency);
20
+
21
+ if (!sent) {
22
+ return {
23
+ content:
24
+ "Could not notify parent. This tool is only available to subagents.",
25
+ isError: true,
26
+ };
27
+ }
28
+
29
+ return {
30
+ content: JSON.stringify({ sent: true, urgency }),
31
+ isError: false,
32
+ };
33
+ }
34
+
35
+ class NotifyParentTool implements Tool {
36
+ name = "notify_parent";
37
+ description =
38
+ "Send a notification to the parent conversation. Use this for important findings, when you're blocked, or when you have preliminary results the parent should know about. Do not overuse — notify for significant findings, not after every tool call.";
39
+ category = "orchestration";
40
+ defaultRiskLevel = RiskLevel.Low;
41
+
42
+ getDefinition(): ToolDefinition {
43
+ return {
44
+ name: this.name,
45
+ description: this.description,
46
+ input_schema: {
47
+ type: "object",
48
+ properties: {
49
+ message: {
50
+ type: "string",
51
+ description: "The notification content for the parent.",
52
+ },
53
+ urgency: {
54
+ type: "string",
55
+ enum: ["info", "important", "blocked"],
56
+ description:
57
+ "'info' for progress updates, 'important' for key findings, 'blocked' when you need guidance.",
58
+ },
59
+ activity: {
60
+ type: "string",
61
+ description:
62
+ "Brief non-technical explanation of what you are doing and why, shown to the user as a status update.",
63
+ },
64
+ },
65
+ required: ["message", "activity"],
66
+ },
67
+ };
68
+ }
69
+
70
+ async execute(
71
+ input: Record<string, unknown>,
72
+ context: ToolContext,
73
+ ): Promise<ToolExecutionResult> {
74
+ return executeSubagentNotifyParent(input, context);
75
+ }
76
+ }
77
+
78
+ export const notifyParentTool = new NotifyParentTool();
79
+ registerTool(notifyParentTool);