@vellumai/assistant 0.6.0 → 0.6.2

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 (358) 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 +42 -1
  6. package/docs/architecture/integrations.md +1 -1
  7. package/docs/architecture/memory.md +21 -24
  8. package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
  9. package/openapi.yaml +539 -4
  10. package/package.json +5 -1
  11. package/src/__tests__/anthropic-provider.test.ts +160 -95
  12. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  13. package/src/__tests__/app-executors.test.ts +47 -1
  14. package/src/__tests__/app-source-watcher.test.ts +159 -0
  15. package/src/__tests__/assistant-event-hub.test.ts +30 -0
  16. package/src/__tests__/checker.test.ts +138 -172
  17. package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
  18. package/src/__tests__/config-schema.test.ts +5 -0
  19. package/src/__tests__/context-overflow-approval.test.ts +5 -5
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +4 -51
  22. package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
  23. package/src/__tests__/conversation-directories-parse.test.ts +105 -0
  24. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
  27. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
  28. package/src/__tests__/conversation-wipe.test.ts +2 -6
  29. package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
  30. package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
  31. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  32. package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
  33. package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
  34. package/src/__tests__/date-context.test.ts +76 -210
  35. package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
  36. package/src/__tests__/file-list-tool.test.ts +219 -0
  37. package/src/__tests__/first-greeting.test.ts +1 -1
  38. package/src/__tests__/heartbeat-service.test.ts +180 -3
  39. package/src/__tests__/identity-routes.test.ts +328 -0
  40. package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
  41. package/src/__tests__/injection-block.test.ts +24 -0
  42. package/src/__tests__/inline-command-runner.test.ts +7 -5
  43. package/src/__tests__/install-skill-routing.test.ts +7 -6
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
  45. package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
  46. package/src/__tests__/llm-context-normalization.test.ts +18 -18
  47. package/src/__tests__/llm-context-route-provider.test.ts +101 -0
  48. package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
  49. package/src/__tests__/log-export-workspace.test.ts +257 -100
  50. package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
  51. package/src/__tests__/mcp-abort-signal.test.ts +5 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +5 -0
  53. package/src/__tests__/memory-recall-log-store.test.ts +132 -0
  54. package/src/__tests__/migration-export-streaming.test.ts +304 -0
  55. package/src/__tests__/migration-import-commit-http.test.ts +11 -10
  56. package/src/__tests__/mock-fetch.ts +87 -0
  57. package/src/__tests__/navigate-settings-tab.test.ts +14 -1
  58. package/src/__tests__/notification-broadcaster.test.ts +65 -0
  59. package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
  60. package/src/__tests__/onboarding-template-contract.test.ts +63 -14
  61. package/src/__tests__/parser.test.ts +32 -0
  62. package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
  63. package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
  64. package/src/__tests__/permission-mode-sse.test.ts +418 -0
  65. package/src/__tests__/permission-mode-store.test.ts +277 -0
  66. package/src/__tests__/permission-mode.test.ts +101 -0
  67. package/src/__tests__/pkb-autoinject.test.ts +96 -0
  68. package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
  69. package/src/__tests__/profiler-routes.test.ts +502 -0
  70. package/src/__tests__/profiler-run-store.test.ts +441 -0
  71. package/src/__tests__/proxy-approval-callback.test.ts +4 -75
  72. package/src/__tests__/registry.test.ts +1 -1
  73. package/src/__tests__/require-fresh-approval.test.ts +0 -2
  74. package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
  75. package/src/__tests__/sandbox-host-parity.test.ts +5 -4
  76. package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
  77. package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
  78. package/src/__tests__/search-skills-unified.test.ts +4 -3
  79. package/src/__tests__/send-endpoint-busy.test.ts +42 -3
  80. package/src/__tests__/set-permission-mode.test.ts +274 -0
  81. package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
  82. package/src/__tests__/skill-memory.test.ts +2 -783
  83. package/src/__tests__/strip-memory-injections.test.ts +187 -0
  84. package/src/__tests__/subagent-detail.test.ts +84 -0
  85. package/src/__tests__/subagent-disposal.test.ts +308 -0
  86. package/src/__tests__/subagent-manager-notify.test.ts +19 -10
  87. package/src/__tests__/subagent-notify-parent.test.ts +390 -0
  88. package/src/__tests__/subagent-role-registry.test.ts +108 -0
  89. package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
  90. package/src/__tests__/subagent-tools.test.ts +464 -4
  91. package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
  92. package/src/__tests__/task-memory-cleanup.test.ts +12 -12
  93. package/src/__tests__/terminal-sandbox.test.ts +1 -1
  94. package/src/__tests__/terminal-tools.test.ts +16 -29
  95. package/src/__tests__/test-preload.ts +18 -0
  96. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
  98. package/src/__tests__/tool-executor.test.ts +4 -27
  99. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  100. package/src/__tests__/top-level-renderer.test.ts +10 -13
  101. package/src/__tests__/transport-hints-queue.test.ts +77 -0
  102. package/src/__tests__/trust-store.test.ts +4 -4
  103. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
  104. package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
  105. package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
  106. package/src/__tests__/workspace-policy.test.ts +2 -7
  107. package/src/agent/loop.ts +6 -29
  108. package/src/approvals/guardian-request-resolvers.ts +24 -0
  109. package/src/avatar/traits-png-sync.ts +3 -3
  110. package/src/channels/types.ts +5 -0
  111. package/src/cli/__tests__/run-assistant-command.ts +56 -0
  112. package/src/cli/__tests__/unknown-command.test.ts +33 -0
  113. package/src/cli/commands/__tests__/email-download.test.ts +245 -0
  114. package/src/cli/commands/__tests__/email-list.test.ts +192 -0
  115. package/src/cli/commands/__tests__/email-register.test.ts +186 -0
  116. package/src/cli/commands/__tests__/email-send.test.ts +291 -0
  117. package/src/cli/commands/__tests__/email-status.test.ts +181 -0
  118. package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
  119. package/src/cli/commands/__tests__/routes.test.ts +562 -0
  120. package/src/cli/commands/conversations.ts +1 -8
  121. package/src/cli/commands/default-action.ts +68 -1
  122. package/src/cli/commands/email.ts +584 -835
  123. package/src/cli/commands/memory.ts +1 -34
  124. package/src/cli/commands/notifications.ts +7 -2
  125. package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
  126. package/src/cli/commands/oauth/connect.ts +25 -5
  127. package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
  128. package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
  129. package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
  130. package/src/cli/commands/routes.ts +396 -0
  131. package/src/cli/commands/skills.ts +130 -20
  132. package/src/cli/program.ts +11 -2
  133. package/src/cli.ts +1 -120
  134. package/src/config/assistant-feature-flags.ts +59 -55
  135. package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
  136. package/src/config/bundled-skills/gmail/SKILL.md +13 -8
  137. package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
  138. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
  139. package/src/config/bundled-skills/messaging/SKILL.md +7 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +22 -2
  141. package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
  142. package/src/config/bundled-skills/settings/TOOLS.json +1 -1
  143. package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
  144. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
  145. package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
  146. package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
  147. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  148. package/src/config/bundled-skills/subagent/SKILL.md +43 -3
  149. package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
  150. package/src/config/env-registry.ts +63 -0
  151. package/src/config/feature-flag-registry.json +17 -1
  152. package/src/config/schema.ts +8 -0
  153. package/src/config/schemas/filing.ts +51 -0
  154. package/src/config/schemas/heartbeat.ts +15 -12
  155. package/src/config/schemas/memory-lifecycle.ts +12 -0
  156. package/src/config/schemas/security.ts +14 -0
  157. package/src/config/schemas/services.ts +8 -0
  158. package/src/credential-execution/approval-bridge.ts +0 -1
  159. package/src/credential-execution/managed-catalog.ts +3 -7
  160. package/src/daemon/app-source-watcher.ts +93 -0
  161. package/src/daemon/config-watcher.ts +85 -3
  162. package/src/daemon/context-overflow-approval.ts +0 -1
  163. package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
  164. package/src/daemon/conversation-agent-loop.ts +179 -65
  165. package/src/daemon/conversation-attachments.ts +0 -1
  166. package/src/daemon/conversation-history.ts +4 -19
  167. package/src/daemon/conversation-lifecycle.ts +8 -14
  168. package/src/daemon/conversation-messaging.ts +3 -0
  169. package/src/daemon/conversation-process.ts +30 -8
  170. package/src/daemon/conversation-queue-manager.ts +8 -0
  171. package/src/daemon/conversation-runtime-assembly.ts +359 -308
  172. package/src/daemon/conversation-surfaces.ts +65 -0
  173. package/src/daemon/conversation-tool-setup.ts +44 -17
  174. package/src/daemon/conversation-workspace.ts +1 -2
  175. package/src/daemon/conversation.ts +19 -3
  176. package/src/daemon/date-context.ts +26 -53
  177. package/src/daemon/first-greeting.ts +1 -1
  178. package/src/daemon/handlers/conversations.ts +5 -7
  179. package/src/daemon/handlers/shared.test.ts +143 -0
  180. package/src/daemon/handlers/shared.ts +70 -5
  181. package/src/daemon/handlers/skills.ts +11 -18
  182. package/src/daemon/lifecycle.ts +220 -158
  183. package/src/daemon/message-types/conversations.ts +29 -6
  184. package/src/daemon/message-types/messages.ts +9 -2
  185. package/src/daemon/message-types/notifications.ts +12 -0
  186. package/src/daemon/message-types/schedules.ts +1 -0
  187. package/src/daemon/message-types/settings.ts +18 -0
  188. package/src/daemon/profiler-run-store.ts +557 -0
  189. package/src/daemon/server.ts +87 -10
  190. package/src/daemon/shutdown-handlers.ts +5 -0
  191. package/src/daemon/tool-side-effects.ts +23 -3
  192. package/src/daemon/transport-hints.ts +33 -0
  193. package/src/export/transcript-formatter.ts +148 -0
  194. package/src/filing/filing-service.ts +228 -0
  195. package/src/heartbeat/heartbeat-service.ts +96 -7
  196. package/src/index.ts +1 -1
  197. package/src/mcp/client.ts +6 -0
  198. package/src/mcp/mcp-oauth-provider.ts +149 -27
  199. package/src/memory/admin.ts +33 -32
  200. package/src/memory/app-store.ts +69 -0
  201. package/src/memory/conversation-bootstrap.ts +1 -1
  202. package/src/memory/conversation-crud.ts +151 -117
  203. package/src/memory/conversation-directories.ts +39 -0
  204. package/src/memory/conversation-group-migration.ts +66 -6
  205. package/src/memory/conversation-queries.ts +58 -12
  206. package/src/memory/conversation-title-service.ts +1 -0
  207. package/src/memory/db-init.ts +182 -376
  208. package/src/memory/embedding-local.ts +1 -1
  209. package/src/memory/graph/bootstrap.ts +75 -66
  210. package/src/memory/graph/capability-seed.ts +167 -17
  211. package/src/memory/graph/consolidation.ts +38 -4
  212. package/src/memory/graph/conversation-graph-memory.ts +133 -104
  213. package/src/memory/graph/extraction-job.ts +9 -4
  214. package/src/memory/graph/extraction.ts +66 -23
  215. package/src/memory/graph/graph-memory-state-store.ts +37 -0
  216. package/src/memory/graph/graph-search.ts +29 -15
  217. package/src/memory/graph/injection.ts +38 -8
  218. package/src/memory/graph/inspect.ts +12 -3
  219. package/src/memory/graph/retriever.ts +365 -262
  220. package/src/memory/graph/store.test.ts +48 -0
  221. package/src/memory/graph/store.ts +150 -11
  222. package/src/memory/graph/tool-handlers.ts +84 -209
  223. package/src/memory/graph/tools.ts +8 -52
  224. package/src/memory/graph/types.ts +24 -0
  225. package/src/memory/group-crud.ts +25 -9
  226. package/src/memory/job-handlers/cleanup.ts +44 -1
  227. package/src/memory/jobs-store.ts +70 -60
  228. package/src/memory/jobs-worker.ts +44 -28
  229. package/src/memory/llm-request-log-store.ts +96 -12
  230. package/src/memory/memory-recall-log-store.ts +49 -5
  231. package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
  232. package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
  233. package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
  234. package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
  235. package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
  236. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
  237. package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
  238. package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
  239. package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
  240. package/src/memory/migrations/index.ts +8 -0
  241. package/src/memory/migrations/registry.ts +8 -0
  242. package/src/memory/schema/conversations.ts +14 -0
  243. package/src/memory/schema/infrastructure.ts +8 -1
  244. package/src/memory/schema/memory-core.ts +0 -51
  245. package/src/memory/schema/memory-graph.ts +15 -0
  246. package/src/memory/task-memory-cleanup.ts +30 -11
  247. package/src/messaging/provider.ts +1 -1
  248. package/src/notifications/broadcaster.ts +6 -0
  249. package/src/notifications/conversation-pairing.ts +12 -4
  250. package/src/notifications/copy-composer.ts +86 -0
  251. package/src/notifications/decision-engine.ts +35 -0
  252. package/src/notifications/emit-signal.ts +14 -0
  253. package/src/notifications/signal.ts +11 -0
  254. package/src/oauth/platform-connection.test.ts +2 -2
  255. package/src/oauth/seed-providers.ts +1 -0
  256. package/src/permissions/checker.ts +15 -4
  257. package/src/permissions/defaults.ts +7 -8
  258. package/src/permissions/permission-mode-store.ts +180 -0
  259. package/src/permissions/permission-mode.ts +31 -0
  260. package/src/permissions/prompter.ts +0 -2
  261. package/src/permissions/workspace-policy.ts +9 -0
  262. package/src/platform/client.ts +1 -1
  263. package/src/prompts/system-prompt.ts +59 -7
  264. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
  265. package/src/prompts/templates/BOOTSTRAP.md +76 -162
  266. package/src/prompts/templates/HEARTBEAT.md +3 -1
  267. package/src/prompts/templates/SOUL.md +30 -9
  268. package/src/prompts/templates/UPDATES.md +8 -0
  269. package/src/providers/anthropic/client.ts +107 -219
  270. package/src/runtime/assistant-event-hub.ts +22 -0
  271. package/src/runtime/auth/route-policy.ts +23 -0
  272. package/src/runtime/auth/token-service.ts +8 -0
  273. package/src/runtime/http-server.ts +32 -2
  274. package/src/runtime/http-types.ts +12 -1
  275. package/src/runtime/migrations/vbundle-builder.ts +389 -3
  276. package/src/runtime/migrations/vbundle-importer.ts +8 -6
  277. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
  278. package/src/runtime/routes/app-management-routes.ts +1 -11
  279. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
  280. package/src/runtime/routes/archive-utils.ts +29 -0
  281. package/src/runtime/routes/avatar-routes.ts +2 -9
  282. package/src/runtime/routes/btw-routes.ts +14 -1
  283. package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
  284. package/src/runtime/routes/conversation-management-routes.ts +1 -14
  285. package/src/runtime/routes/conversation-query-routes.ts +49 -3
  286. package/src/runtime/routes/conversation-routes.ts +270 -44
  287. package/src/runtime/routes/group-routes.ts +22 -8
  288. package/src/runtime/routes/heartbeat-routes.ts +4 -10
  289. package/src/runtime/routes/identity-routes.ts +53 -18
  290. package/src/runtime/routes/llm-context-normalization.ts +14 -10
  291. package/src/runtime/routes/log-export/AGENTS.md +104 -0
  292. package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
  293. package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
  294. package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
  295. package/src/runtime/routes/log-export-routes.ts +41 -278
  296. package/src/runtime/routes/memory-item-routes.test.ts +168 -233
  297. package/src/runtime/routes/migration-routes.ts +18 -7
  298. package/src/runtime/routes/profiler-routes.ts +350 -0
  299. package/src/runtime/routes/schedule-routes.ts +27 -12
  300. package/src/runtime/routes/settings-routes.ts +95 -8
  301. package/src/runtime/routes/subagents-routes.ts +28 -7
  302. package/src/runtime/routes/user-route-dispatcher.ts +223 -0
  303. package/src/runtime/routes/user-routes.ts +41 -0
  304. package/src/runtime/routes/workspace-routes.ts +0 -1
  305. package/src/schedule/schedule-store.ts +30 -0
  306. package/src/schedule/scheduler.ts +45 -18
  307. package/src/skills/catalog-install.ts +10 -2
  308. package/src/skills/inline-command-runner.ts +12 -14
  309. package/src/skills/managed-store.ts +2 -2
  310. package/src/skills/skill-memory.ts +1 -293
  311. package/src/subagent/index.ts +13 -3
  312. package/src/subagent/manager.ts +308 -29
  313. package/src/subagent/types.ts +68 -0
  314. package/src/tasks/task-runner.ts +4 -4
  315. package/src/tools/apps/executors.ts +29 -4
  316. package/src/tools/filesystem/list.ts +93 -0
  317. package/src/tools/permission-checker.ts +78 -18
  318. package/src/tools/registry.ts +4 -0
  319. package/src/tools/schedule/create.ts +3 -0
  320. package/src/tools/schedule/list.ts +1 -0
  321. package/src/tools/schedule/update.ts +6 -0
  322. package/src/tools/secret-detection-handler.ts +0 -1
  323. package/src/tools/shared/filesystem/errors.ts +5 -0
  324. package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
  325. package/src/tools/shared/filesystem/types.ts +17 -0
  326. package/src/tools/shared/shell-output.ts +31 -2
  327. package/src/tools/skills/sandbox-runner.ts +3 -6
  328. package/src/tools/subagent/abort.ts +12 -2
  329. package/src/tools/subagent/message.ts +9 -2
  330. package/src/tools/subagent/notify-parent.ts +79 -0
  331. package/src/tools/subagent/read.ts +29 -8
  332. package/src/tools/subagent/resolve.ts +21 -0
  333. package/src/tools/subagent/spawn.ts +2 -0
  334. package/src/tools/subagent/status.ts +11 -1
  335. package/src/tools/system/avatar-generator.ts +3 -3
  336. package/src/tools/system/register.ts +23 -0
  337. package/src/tools/system/set-permission-mode.ts +103 -0
  338. package/src/tools/terminal/parser.ts +30 -5
  339. package/src/tools/terminal/safe-env.ts +16 -1
  340. package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
  341. package/src/tools/terminal/sandbox.ts +4 -1
  342. package/src/tools/terminal/shell.ts +3 -5
  343. package/src/tools/tool-manifest.ts +6 -0
  344. package/src/tools/types.ts +2 -3
  345. package/src/util/logger.ts +1 -1
  346. package/src/util/platform.ts +50 -17
  347. package/src/watcher/provider-types.ts +1 -1
  348. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
  349. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
  350. package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
  351. package/src/workspace/migrations/029-seed-pkb.ts +85 -0
  352. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
  353. package/src/workspace/migrations/registry.ts +6 -0
  354. package/src/workspace/top-level-renderer.ts +5 -9
  355. package/src/__tests__/cli-memory.test.ts +0 -377
  356. package/src/__tests__/clipboard.test.ts +0 -88
  357. package/src/cli/cli-memory.ts +0 -179
  358. package/src/util/clipboard.ts +0 -34
@@ -30,9 +30,11 @@ export const conversations = sqliteTable(
30
30
  forkParentMessageId: text("fork_parent_message_id"),
31
31
  isAutoTitle: integer("is_auto_title").notNull().default(1),
32
32
  scheduleJobId: text("schedule_job_id"),
33
+ lastMessageAt: integer("last_message_at"),
33
34
  },
34
35
  (table) => [
35
36
  index("idx_conversations_updated_at").on(table.updatedAt),
37
+ index("idx_conversations_last_message_at").on(table.lastMessageAt),
36
38
  index("idx_conversations_conversation_type").on(table.conversationType),
37
39
  index("idx_conversations_fork_parent_conversation_id").on(
38
40
  table.forkParentConversationId,
@@ -109,6 +111,18 @@ export const messageAttachments = sqliteTable("message_attachments", {
109
111
  createdAt: integer("created_at").notNull(),
110
112
  });
111
113
 
114
+ export const conversationGraphMemoryState = sqliteTable(
115
+ "conversation_graph_memory_state",
116
+ {
117
+ conversationId: text("conversation_id")
118
+ .primaryKey()
119
+ .references(() => conversations.id, { onDelete: "cascade" }),
120
+ stateJson: text("state_json").notNull(),
121
+ createdAt: integer("created_at").notNull(),
122
+ updatedAt: integer("updated_at").notNull(),
123
+ },
124
+ );
125
+
112
126
  export const channelInboundEvents = sqliteTable("channel_inbound_events", {
113
127
  id: text("id").primaryKey(),
114
128
  sourceChannel: text("source_channel").notNull(),
@@ -25,6 +25,9 @@ export const cronJobs = sqliteTable("cron_jobs", {
25
25
  routingHintsJson: text("routing_hints_json").notNull().default("{}"),
26
26
  status: text("status").notNull().default("active"), // 'active' | 'firing' | 'fired' | 'cancelled'
27
27
  quiet: integer("quiet", { mode: "boolean" }).notNull().default(false), // suppress completion notifications
28
+ reuseConversation: integer("reuse_conversation", { mode: "boolean" })
29
+ .notNull()
30
+ .default(false), // reuse the same conversation across runs
28
31
  createdAt: integer("created_at").notNull(),
29
32
  updatedAt: integer("updated_at").notNull(),
30
33
  });
@@ -118,7 +121,10 @@ export const llmRequestLogs = sqliteTable(
118
121
  responsePayload: text("response_payload").notNull(),
119
122
  createdAt: integer("created_at").notNull(),
120
123
  },
121
- (table) => [index("idx_llm_request_logs_message_id").on(table.messageId)],
124
+ (table) => [
125
+ index("idx_llm_request_logs_message_id").on(table.messageId),
126
+ index("idx_llm_request_logs_created_at").on(table.createdAt),
127
+ ],
122
128
  );
123
129
 
124
130
  export const memoryRecallLogs = sqliteTable(
@@ -144,6 +150,7 @@ export const memoryRecallLogs = sqliteTable(
144
150
  topCandidatesJson: text("top_candidates_json").notNull(),
145
151
  injectedText: text("injected_text"),
146
152
  reason: text("reason"),
153
+ queryContext: text("query_context"),
147
154
  createdAt: integer("created_at").notNull(),
148
155
  },
149
156
  (table) => [
@@ -2,7 +2,6 @@ import {
2
2
  blob,
3
3
  index,
4
4
  integer,
5
- real,
6
5
  sqliteTable,
7
6
  text,
8
7
  uniqueIndex,
@@ -32,56 +31,6 @@ export const memorySegments = sqliteTable(
32
31
  (table) => [index("idx_memory_segments_scope_id").on(table.scopeId)],
33
32
  );
34
33
 
35
- export const memoryItems = sqliteTable(
36
- "memory_items",
37
- {
38
- id: text("id").primaryKey(),
39
- kind: text("kind").notNull(),
40
- subject: text("subject").notNull(),
41
- statement: text("statement").notNull(),
42
- status: text("status").notNull(),
43
- confidence: real("confidence").notNull(),
44
- importance: real("importance"),
45
- accessCount: integer("access_count").notNull().default(0),
46
- fingerprint: text("fingerprint").notNull(),
47
- verificationState: text("verification_state")
48
- .notNull()
49
- .default("assistant_inferred"),
50
- scopeId: text("scope_id").notNull().default("default"),
51
- firstSeenAt: integer("first_seen_at").notNull(),
52
- lastSeenAt: integer("last_seen_at").notNull(),
53
- lastUsedAt: integer("last_used_at"),
54
- validFrom: integer("valid_from"),
55
- invalidAt: integer("invalid_at"),
56
- supersedes: text("supersedes"),
57
- supersededBy: text("superseded_by"),
58
- overrideConfidence: text("override_confidence").default("inferred"),
59
- sourceType: text("source_type").notNull().default("extraction"),
60
- sourceMessageRole: text("source_message_role"),
61
- },
62
- (table) => [
63
- index("idx_memory_items_scope_id").on(table.scopeId),
64
- index("idx_memory_items_fingerprint").on(table.fingerprint),
65
- ],
66
- );
67
-
68
- export const memoryItemSources = sqliteTable(
69
- "memory_item_sources",
70
- {
71
- memoryItemId: text("memory_item_id")
72
- .notNull()
73
- .references(() => memoryItems.id, { onDelete: "cascade" }),
74
- messageId: text("message_id")
75
- .notNull()
76
- .references(() => messages.id, { onDelete: "cascade" }),
77
- evidence: text("evidence"),
78
- createdAt: integer("created_at").notNull(),
79
- },
80
- (table) => [
81
- index("idx_memory_item_sources_memory_item_id").on(table.memoryItemId),
82
- ],
83
- );
84
-
85
34
  export const memorySummaries = sqliteTable(
86
35
  "memory_summaries",
87
36
  {
@@ -137,3 +137,18 @@ export const memoryGraphTriggers = sqliteTable(
137
137
  index("idx_graph_triggers_type").on(table.type),
138
138
  ],
139
139
  );
140
+
141
+ export const memoryGraphNodeEdits = sqliteTable(
142
+ "memory_graph_node_edits",
143
+ {
144
+ id: text("id").primaryKey(),
145
+ nodeId: text("node_id")
146
+ .notNull()
147
+ .references(() => memoryGraphNodes.id, { onDelete: "cascade" }),
148
+ previousContent: text("previous_content").notNull(),
149
+ newContent: text("new_content").notNull(),
150
+ source: text("source").notNull(),
151
+ conversationId: text("conversation_id"),
152
+ created: integer("created").notNull(),
153
+ },
154
+ );
@@ -9,16 +9,29 @@ const log = getLogger("task-memory-cleanup");
9
9
  * so the check survives daemon restarts.
10
10
  */
11
11
  export function isConversationFailed(conversationId: string): boolean {
12
+ // For reused schedule conversations the same conversation_id appears in
13
+ // multiple cron_runs. A single failed run should NOT mark the conversation
14
+ // as permanently failed — only the *most recent* run for that conversation
15
+ // matters. We therefore check whether the latest cron_run (by created_at,
16
+ // which is a monotonically increasing epoch timestamp) has an error status.
17
+ // Note: cron_runs.id is a UUID v4 (random), so we cannot use MAX(id).
12
18
  const row = rawGet<{ found: number }>(
13
19
  `SELECT 1 AS found
14
20
  FROM (
15
21
  SELECT 1 FROM task_runs WHERE conversation_id = ? AND status = 'failed'
16
22
  UNION ALL
17
- SELECT 1 FROM cron_runs WHERE conversation_id = ? AND status = 'error'
23
+ SELECT 1 FROM cron_runs
24
+ WHERE conversation_id = ?
25
+ AND status = 'error'
26
+ AND id = (
27
+ SELECT id FROM cron_runs WHERE conversation_id = ?
28
+ ORDER BY created_at DESC LIMIT 1
29
+ )
18
30
  )
19
31
  LIMIT 1`,
20
32
  conversationId,
21
33
  conversationId,
34
+ conversationId,
22
35
  );
23
36
  return row != null;
24
37
  }
@@ -57,9 +70,17 @@ export function invalidateAssistantInferredItemsForConversation(
57
70
  AND tr.status = 'failed'
58
71
  )
59
72
  AND NOT EXISTS (
73
+ -- Check only the most recent cron_run for each conversation
74
+ -- so reused conversations with historical errors but recent
75
+ -- successes are still treated as valid corroborators.
60
76
  SELECT 1 FROM cron_runs cr
61
77
  WHERE cr.conversation_id = jc2.value
62
78
  AND cr.status = 'error'
79
+ AND cr.id = (
80
+ SELECT cr2.id FROM cron_runs cr2
81
+ WHERE cr2.conversation_id = jc2.value
82
+ ORDER BY cr2.created_at DESC LIMIT 1
83
+ )
63
84
  )
64
85
  )`,
65
86
  Date.now(),
@@ -79,9 +100,9 @@ export function invalidateAssistantInferredItemsForConversation(
79
100
 
80
101
  /**
81
102
  * Cancel all pending/running memory jobs referencing the given conversation.
82
- * Covers every job type: `extract_items`, `embed_attachment` (keyed by messageId),
103
+ * Covers every job type: `embed_attachment` (keyed by messageId),
83
104
  * `embed_segment` (keyed by segmentId via memory_segments),
84
- * `build_conversation_summary` (keyed by conversationId),
105
+ * `graph_extract`, `build_conversation_summary` (keyed by conversationId),
85
106
  * and `embed_graph_node` (keyed by nodeId sourced from the conversation).
86
107
  */
87
108
  export function cancelPendingJobsForConversation(
@@ -91,7 +112,7 @@ export function cancelPendingJobsForConversation(
91
112
  const now = Date.now();
92
113
  let total = 0;
93
114
 
94
- // Jobs keyed by messageId: extract_items, embed_attachment
115
+ // Jobs keyed by messageId: embed_attachment
95
116
  total += rawRun(
96
117
  `UPDATE memory_jobs
97
118
  SET status = 'failed',
@@ -106,7 +127,7 @@ export function cancelPendingJobsForConversation(
106
127
  conversationId,
107
128
  );
108
129
 
109
- // Jobs keyed by conversationId: build_conversation_summary
130
+ // Jobs keyed by conversationId: graph_extract, build_conversation_summary
110
131
  total += rawRun(
111
132
  `UPDATE memory_jobs
112
133
  SET status = 'failed',
@@ -162,8 +183,8 @@ export function cancelPendingJobsForConversation(
162
183
  }
163
184
 
164
185
  /**
165
- * Cancel only pending/running `extract_items` jobs for messages in the
166
- * given conversation. Used by the task-failure path where we want to
186
+ * Cancel only pending/running `graph_extract` jobs for the given
187
+ * conversation. Used by the task-failure path where we want to
167
188
  * stop new extractions but must NOT cancel `embed_graph_node` jobs —
168
189
  * those nodes may be multi-sourced and still valid.
169
190
  */
@@ -177,10 +198,8 @@ function cancelPendingExtractionJobsForConversation(
177
198
  last_error = 'conversation_failed',
178
199
  updated_at = ?
179
200
  WHERE status IN ('pending', 'running')
180
- AND type = 'extract_items'
181
- AND json_extract(payload, '$.messageId') IN (
182
- SELECT id FROM messages WHERE conversation_id = ?
183
- )`,
201
+ AND type = 'graph_extract'
202
+ AND json_extract(payload, '$.conversationId') = ?`,
184
203
  now,
185
204
  conversationId,
186
205
  );
@@ -25,7 +25,7 @@ export interface MessagingProvider {
25
25
  id: string;
26
26
  /** Human-readable name (e.g. 'Slack', 'Gmail'). */
27
27
  displayName: string;
28
- /** Credential service name for token-manager (e.g. 'integration:slack'). */
28
+ /** Credential service name for token-manager (e.g. 'slack'). */
29
29
  credentialService: string;
30
30
 
31
31
  // ── Universal operations (every platform must implement) ──────────
@@ -41,6 +41,10 @@ export interface ConversationCreatedInfo {
41
41
  sourceEventName: string;
42
42
  /** Present when the conversation is for a guardian-sensitive notification. */
43
43
  targetGuardianPrincipalId?: string;
44
+ /** Conversation group identifier from the signal producer (e.g. "system:scheduled"). */
45
+ groupId?: string;
46
+ /** Semantic source from the signal producer (e.g. "schedule", "reminder"). */
47
+ source?: string;
44
48
  }
45
49
  export type OnConversationCreatedFn = (info: ConversationCreatedInfo) => void;
46
50
  export interface BroadcastDecisionOptions {
@@ -238,6 +242,8 @@ export class NotificationBroadcaster {
238
242
  title: conversationTitle,
239
243
  sourceEventName: signal.sourceEventName,
240
244
  targetGuardianPrincipalId,
245
+ groupId: signal.conversationMetadata?.groupId,
246
+ source: signal.conversationMetadata?.source,
241
247
  };
242
248
 
243
249
  // The per-dispatch onConversationCreated callback fires whenever a vellum
@@ -130,7 +130,9 @@ export async function pairDeliveryWithConversation(
130
130
  const targetId = conversationAction.conversationId;
131
131
  const existing = getConversation(targetId);
132
132
 
133
- if (existing && existing.source === "notification") {
133
+ const effectiveSource =
134
+ signal.conversationMetadata?.source ?? "notification";
135
+ if (existing && existing.source === effectiveSource) {
134
136
  // Append the seed message to the existing conversation
135
137
  const message = await addMessage(
136
138
  existing.id,
@@ -186,7 +188,9 @@ export async function pairDeliveryWithConversation(
186
188
  const conversation = createConversation({
187
189
  title,
188
190
  conversationType,
189
- source: "notification",
191
+ source: signal.conversationMetadata?.source ?? "notification",
192
+ groupId: signal.conversationMetadata?.groupId,
193
+ scheduleJobId: signal.conversationMetadata?.scheduleJobId,
190
194
  });
191
195
 
192
196
  const message = await addMessage(
@@ -241,7 +245,9 @@ export async function pairDeliveryWithConversation(
241
245
  existingBinding.conversationId,
242
246
  );
243
247
 
244
- if (boundConversation && boundConversation.source === "notification") {
248
+ const effectiveSource =
249
+ signal.conversationMetadata?.source ?? "notification";
250
+ if (boundConversation && boundConversation.source === effectiveSource) {
245
251
  const message = await addMessage(
246
252
  boundConversation.id,
247
253
  "assistant",
@@ -299,7 +305,9 @@ export async function pairDeliveryWithConversation(
299
305
  const conversation = createConversation({
300
306
  title,
301
307
  conversationType,
302
- source: "notification",
308
+ source: signal.conversationMetadata?.source ?? "notification",
309
+ groupId: signal.conversationMetadata?.groupId,
310
+ scheduleJobId: signal.conversationMetadata?.scheduleJobId,
303
311
  });
304
312
 
305
313
  // Skip memory indexing — notification audit messages are not conversational
@@ -408,6 +408,92 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
408
408
  };
409
409
  },
410
410
 
411
+ "ingress.trusted_contact.guardian_decision": (payload) => {
412
+ const decision = str(payload.decision, "decided on");
413
+ const sourceChannel =
414
+ typeof payload.sourceChannel === "string"
415
+ ? payload.sourceChannel
416
+ : undefined;
417
+
418
+ const requesterDisplayName =
419
+ typeof payload.requesterDisplayName === "string" &&
420
+ payload.requesterDisplayName.length > 0
421
+ ? payload.requesterDisplayName
422
+ : undefined;
423
+ const requesterExternalUserId =
424
+ typeof payload.requesterExternalUserId === "string" &&
425
+ payload.requesterExternalUserId.length > 0
426
+ ? payload.requesterExternalUserId
427
+ : undefined;
428
+ const requesterLabel = sanitizeIdentityField(
429
+ requesterDisplayName ??
430
+ (sourceChannel === "slack" &&
431
+ requesterExternalUserId &&
432
+ /^U[A-Z0-9]+$/i.test(requesterExternalUserId)
433
+ ? `<@${requesterExternalUserId}>`
434
+ : requesterExternalUserId) ??
435
+ "Someone",
436
+ );
437
+
438
+ const decidedByDisplayName =
439
+ typeof payload.decidedByDisplayName === "string" &&
440
+ payload.decidedByDisplayName.length > 0
441
+ ? payload.decidedByDisplayName
442
+ : undefined;
443
+ const decidedByExternalUserId =
444
+ typeof payload.decidedByExternalUserId === "string" &&
445
+ payload.decidedByExternalUserId.length > 0
446
+ ? payload.decidedByExternalUserId
447
+ : undefined;
448
+ const decidedByLabel = sanitizeIdentityField(
449
+ decidedByDisplayName ??
450
+ (sourceChannel === "slack" &&
451
+ decidedByExternalUserId &&
452
+ /^U[A-Z0-9]+$/i.test(decidedByExternalUserId)
453
+ ? `<@${decidedByExternalUserId}>`
454
+ : decidedByExternalUserId) ??
455
+ "a guardian",
456
+ );
457
+
458
+ const verb = decision === "approved" ? "approved" : "denied";
459
+ return {
460
+ title: "Trusted Contact Decision",
461
+ body: `${requesterLabel}'s access request has been ${verb} by ${decidedByLabel}.`,
462
+ };
463
+ },
464
+
465
+ "ingress.trusted_contact.denied": (payload) => {
466
+ const sourceChannel =
467
+ typeof payload.sourceChannel === "string"
468
+ ? payload.sourceChannel
469
+ : undefined;
470
+
471
+ const requesterDisplayName =
472
+ typeof payload.requesterDisplayName === "string" &&
473
+ payload.requesterDisplayName.length > 0
474
+ ? payload.requesterDisplayName
475
+ : undefined;
476
+ const requesterExternalUserId =
477
+ typeof payload.requesterExternalUserId === "string" &&
478
+ payload.requesterExternalUserId.length > 0
479
+ ? payload.requesterExternalUserId
480
+ : undefined;
481
+ const requesterLabel = sanitizeIdentityField(
482
+ requesterDisplayName ??
483
+ (sourceChannel === "slack" &&
484
+ requesterExternalUserId &&
485
+ /^U[A-Z0-9]+$/i.test(requesterExternalUserId)
486
+ ? `<@${requesterExternalUserId}>`
487
+ : requesterExternalUserId) ??
488
+ "Someone",
489
+ );
490
+
491
+ return {
492
+ title: "Trusted Contact Denied",
493
+ body: `A trusted contact request from ${requesterLabel} has been denied.`,
494
+ };
495
+ },
496
+
411
497
  "ingress.escalation": (payload) => ({
412
498
  title: "Escalation",
413
499
  body:
@@ -13,6 +13,7 @@ import { v4 as uuid } from "uuid";
13
13
 
14
14
  import { getDeliverableChannels } from "../channels/config.js";
15
15
  import { getConfig } from "../config/loader.js";
16
+ import { listGuardianChannels } from "../contacts/contact-store.js";
16
17
  import { resolveGuardianPersona } from "../prompts/persona-resolver.js";
17
18
  import { buildCoreIdentityContext } from "../prompts/system-prompt.js";
18
19
  import {
@@ -73,6 +74,7 @@ function buildSystemPrompt(
73
74
  preferenceContext?: string,
74
75
  candidateContext?: string,
75
76
  identityContext?: string,
77
+ recipientNotes?: string,
76
78
  ): string {
77
79
  const sections: string[] = [
78
80
  `You are a notification routing engine. Given a signal describing an event, decide whether the user should be notified, on which channel(s), and compose the notification copy.`,
@@ -89,6 +91,16 @@ function buildSystemPrompt(
89
91
  );
90
92
  }
91
93
 
94
+ if (recipientNotes) {
95
+ sections.push(
96
+ ``,
97
+ `<recipient-context>`,
98
+ `The following are notes about the notification recipient. Use this context to tailor notification tone, formality, and content to the recipient's preferences.`,
99
+ recipientNotes,
100
+ `</recipient-context>`,
101
+ );
102
+ }
103
+
92
104
  if (identityContext) {
93
105
  sections.push(
94
106
  ``,
@@ -807,11 +819,34 @@ async function classifyWithLLM(
807
819
  const identityContext = rawIdentityContext
808
820
  ? truncate(rawIdentityContext, MAX_IDENTITY_CONTEXT_CHARS, "\n…[truncated]")
809
821
  : undefined;
822
+
823
+ // Resolve guardian contact notes for recipient context. Use the channel-
824
+ // agnostic guardian lookup so notes are available even when the only
825
+ // deliverable channel is "vellum" (which has no contact channel type).
826
+ let recipientNotes: string | undefined;
827
+ try {
828
+ const guardianResult = listGuardianChannels();
829
+ if (guardianResult?.contact.notes) {
830
+ recipientNotes = truncate(
831
+ guardianResult.contact.notes,
832
+ MAX_IDENTITY_CONTEXT_CHARS,
833
+ "\n…[truncated]",
834
+ );
835
+ }
836
+ } catch (err) {
837
+ const errMsg = err instanceof Error ? err.message : String(err);
838
+ log.warn(
839
+ { err: errMsg },
840
+ "Failed to resolve guardian contact notes, proceeding without recipient context",
841
+ );
842
+ }
843
+
810
844
  const systemPrompt = buildSystemPrompt(
811
845
  availableChannels,
812
846
  preferenceContext,
813
847
  candidateContext,
814
848
  identityContext,
849
+ recipientNotes,
815
850
  );
816
851
  const prompt = buildUserPrompt(signal);
817
852
  const tool = buildDecisionTool(availableChannels);
@@ -79,6 +79,8 @@ function getBroadcaster(): NotificationBroadcaster {
79
79
  title: info.title,
80
80
  sourceEventName: info.sourceEventName,
81
81
  targetGuardianPrincipalId: info.targetGuardianPrincipalId,
82
+ groupId: info.groupId,
83
+ source: info.source,
82
84
  });
83
85
  log.info(
84
86
  {
@@ -176,6 +178,17 @@ export interface EmitSignalParams<TEventName extends string = string> {
176
178
  * Useful for direct user-invoked actions that must fail closed.
177
179
  */
178
180
  throwOnError?: boolean;
181
+ /**
182
+ * Optional metadata propagated to the conversation created by the notification
183
+ * pipeline. Allows signal producers (e.g. the scheduler) to set groupId,
184
+ * scheduleJobId, or override the default "notification" source on the
185
+ * resulting conversation so it appears in the correct folder on clients.
186
+ */
187
+ conversationMetadata?: {
188
+ groupId?: string;
189
+ scheduleJobId?: string;
190
+ source?: string;
191
+ };
179
192
  }
180
193
 
181
194
  export interface EmitSignalResult {
@@ -210,6 +223,7 @@ export async function emitNotificationSignal<TEventName extends string>(
210
223
  routingIntent: params.routingIntent,
211
224
  routingHints: params.routingHints,
212
225
  conversationAffinityHint: params.conversationAffinityHint,
226
+ conversationMetadata: params.conversationMetadata,
213
227
  };
214
228
 
215
229
  try {
@@ -200,4 +200,15 @@ export interface NotificationSignal<TEventName extends string = string> {
200
200
  * affinity within a call session.
201
201
  */
202
202
  conversationAffinityHint?: Partial<Record<string, string>>;
203
+ /**
204
+ * Optional metadata propagated to the conversation created by the notification
205
+ * pipeline. Allows signal producers (e.g. the scheduler) to set groupId,
206
+ * scheduleJobId, or override the default "notification" source on the
207
+ * resulting conversation so it appears in the correct folder on clients.
208
+ */
209
+ conversationMetadata?: {
210
+ groupId?: string;
211
+ scheduleJobId?: string;
212
+ source?: string;
213
+ };
203
214
  }
@@ -27,7 +27,7 @@ function makeMockClient(
27
27
  fetch: mock(async (path: string, init?: RequestInit) => {
28
28
  const url = `https://platform.example.com${path}`;
29
29
  const headers = new Headers(init?.headers);
30
- headers.set("Authorization", "Api-Key test-api-key");
30
+ headers.set("Authorization", "Bearer test-api-key");
31
31
  return mockFetchFn(url, { ...init, headers });
32
32
  }),
33
33
  } as unknown as VellumPlatformClient;
@@ -53,7 +53,7 @@ describe("PlatformOAuthConnection", () => {
53
53
  );
54
54
  expect(init?.method).toBe("POST");
55
55
  const headers = new Headers(init?.headers);
56
- expect(headers.get("Authorization")).toBe("Api-Key test-api-key");
56
+ expect(headers.get("Authorization")).toBe("Bearer test-api-key");
57
57
  expect(headers.get("Content-Type")).toBe("application/json");
58
58
 
59
59
  const parsed = JSON.parse(init?.body as string);
@@ -295,6 +295,7 @@ const PROVIDER_SEED_DATA: Record<
295
295
  },
296
296
  extraParams: { prompt: "consent" },
297
297
  loopbackPort: 17324,
298
+ managedServiceConfigKey: "linear-oauth",
298
299
  injectionTemplates: [
299
300
  {
300
301
  hostPattern: "api.linear.app",
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
 
5
5
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
6
+ import { getIsContainerized } from "../config/env-registry.js";
6
7
  import { getConfig } from "../config/loader.js";
7
8
  import { loadSkillCatalog, resolveSkillSelector } from "../config/skills.js";
8
9
  import { indexCatalogById } from "../skills/include-graph.js";
@@ -677,7 +678,7 @@ export async function classifyRisk(
677
678
  }
678
679
  }
679
680
 
680
- const result = await classifyRiskUncached(
681
+ let result = await classifyRiskUncached(
681
682
  toolName,
682
683
  input,
683
684
  workingDir,
@@ -685,6 +686,17 @@ export async function classifyRisk(
685
686
  manifestOverride,
686
687
  );
687
688
 
689
+ // Proxied bash commands route through the credential proxy which handles
690
+ // per-request approval separately. Cap the bash tool's own risk at Medium
691
+ // so trust rules can auto-allow the command execution.
692
+ if (
693
+ toolName === "bash" &&
694
+ input.network_mode === "proxied" &&
695
+ result === RiskLevel.High
696
+ ) {
697
+ result = RiskLevel.Medium;
698
+ }
699
+
688
700
  if (cacheKey) {
689
701
  if (riskCache.size >= RISK_CACHE_MAX) {
690
702
  const oldest = riskCache.keys().next().value;
@@ -1086,9 +1098,8 @@ export async function check(
1086
1098
  !matchedRule &&
1087
1099
  risk === RiskLevel.Low
1088
1100
  ) {
1089
- // When sandbox is disabled, bash runs on the host — don't auto-allow
1090
- const sandboxEnabled = getConfig().sandbox.enabled;
1091
- if (toolName === "bash" && !sandboxEnabled) {
1101
+ // Outside a container, bash runs on the host — don't auto-allow
1102
+ if (toolName === "bash" && !getIsContainerized()) {
1092
1103
  // Fall through to risk-based policy below
1093
1104
  } else if (isWorkspaceScopedInvocation(toolName, input, workingDir)) {
1094
1105
  return {
@@ -1,5 +1,6 @@
1
1
  import { join } from "node:path";
2
2
 
3
+ import { getIsContainerized } from "../config/env-registry.js";
3
4
  import { getConfig } from "../config/loader.js";
4
5
  import { getBundledSkillsDir } from "../config/skills.js";
5
6
  import { getWorkspaceDir } from "../util/platform.js";
@@ -42,7 +43,6 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
42
43
  // Some test suites mock getConfig() with partial objects; treat missing
43
44
  // branches as defaults so rule generation remains deterministic.
44
45
  const config = getConfig() as {
45
- sandbox?: { enabled?: boolean };
46
46
  skills?: { load?: { extraDirs?: unknown } };
47
47
  };
48
48
 
@@ -67,12 +67,11 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
67
67
  priority: 50,
68
68
  };
69
69
 
70
- // Sandboxed bash commands run in an isolated container auto-allow all of
71
- // them (including high-risk) so the user is never prompted for sandbox work.
72
- // Only emit this rule when the sandbox is actually enabled; otherwise bash
73
- // commands execute on the host and must go through normal permission checks.
74
- const sandboxEnabled = config.sandbox?.enabled !== false;
75
- const sandboxShellRule: DefaultRuleTemplate | null = sandboxEnabled
70
+ // When running inside a container (IS_CONTAINERIZED=true), bash commands
71
+ // execute in an isolated environment auto-allow all of them (including
72
+ // high-risk) so the user is never prompted. Outside a container, bash
73
+ // commands run on the host and go through normal permission checks.
74
+ const bashShellRule: DefaultRuleTemplate | null = getIsContainerized()
76
75
  ? {
77
76
  id: "default:allow-bash-global",
78
77
  tool: "bash",
@@ -300,7 +299,7 @@ export function getDefaultRuleTemplates(): DefaultRuleTemplate[] {
300
299
  return [
301
300
  ...hostFileRules,
302
301
  hostShellRule,
303
- ...(sandboxShellRule ? [sandboxShellRule] : []),
302
+ ...(bashShellRule ? [bashShellRule] : []),
304
303
  ...computerUseRules,
305
304
  ...managedSkillRules,
306
305
  ...workspacePromptRules,