@vellumai/assistant 0.5.1 → 0.5.3

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 (405) hide show
  1. package/ARCHITECTURE.md +163 -54
  2. package/docs/architecture/integrations.md +62 -67
  3. package/docs/credential-execution-service.md +3 -3
  4. package/docs/skills.md +100 -0
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop.test.ts +111 -0
  7. package/src/__tests__/always-loaded-tools-guard.test.ts +3 -4
  8. package/src/__tests__/app-builder-tool-scripts.test.ts +13 -151
  9. package/src/__tests__/app-dir-path-guard.test.ts +78 -0
  10. package/src/__tests__/app-executors.test.ts +1 -291
  11. package/src/__tests__/app-git-history.test.ts +4 -4
  12. package/src/__tests__/app-routes-csp.test.ts +1 -0
  13. package/src/__tests__/app-store-dir-names.test.ts +426 -0
  14. package/src/__tests__/attachments-store.test.ts +169 -21
  15. package/src/__tests__/attachments.test.ts +115 -1
  16. package/src/__tests__/btw-routes.test.ts +1 -0
  17. package/src/__tests__/canonical-guardian-store.test.ts +38 -0
  18. package/src/__tests__/channel-reply-delivery.test.ts +55 -0
  19. package/src/__tests__/checker.test.ts +54 -0
  20. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  21. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  22. package/src/__tests__/compaction.benchmark.test.ts +2 -1
  23. package/src/__tests__/config-schema-cmd.test.ts +68 -21
  24. package/src/__tests__/config-schema.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-overflow.test.ts +156 -5
  26. package/src/__tests__/conversation-agent-loop.test.ts +297 -2
  27. package/src/__tests__/conversation-attachments.test.ts +17 -19
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +277 -0
  29. package/src/__tests__/conversation-disk-view.test.ts +810 -0
  30. package/src/__tests__/conversation-error.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +551 -0
  32. package/src/__tests__/conversation-fork-route.test.ts +386 -0
  33. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  34. package/src/__tests__/conversation-key-store-disk-view.test.ts +130 -0
  35. package/src/__tests__/conversation-media-retry.test.ts +8 -2
  36. package/src/__tests__/conversation-memory-dirty-tail.test.ts +150 -0
  37. package/src/__tests__/conversation-provider-retry-repair.test.ts +7 -0
  38. package/src/__tests__/conversation-queue.test.ts +36 -1
  39. package/src/__tests__/conversation-routes-disk-view.test.ts +439 -0
  40. package/src/__tests__/conversation-routes-guardian-reply.test.ts +2 -2
  41. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -7
  42. package/src/__tests__/conversation-runtime-assembly.test.ts +17 -2
  43. package/src/__tests__/conversation-skill-tools.test.ts +4 -9
  44. package/src/__tests__/conversation-slash-commands.test.ts +149 -0
  45. package/src/__tests__/conversation-store.test.ts +24 -21
  46. package/src/__tests__/conversation-surfaces-state-update.test.ts +246 -0
  47. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -0
  48. package/src/__tests__/conversation-title-service.test.ts +137 -0
  49. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +25 -315
  50. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +1 -0
  51. package/src/__tests__/conversation-tool-setup-side-effect-flag.test.ts +1 -0
  52. package/src/__tests__/conversation-wipe.test.ts +226 -0
  53. package/src/__tests__/conversation-workspace-cache-state.test.ts +44 -2
  54. package/src/__tests__/conversation-workspace-injection.test.ts +11 -0
  55. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  56. package/src/__tests__/credential-vault-unit.test.ts +5 -10
  57. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  58. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +241 -0
  59. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +214 -0
  60. package/src/__tests__/db-memory-archive-migration.test.ts +372 -0
  61. package/src/__tests__/db-memory-brief-state-migration.test.ts +213 -0
  62. package/src/__tests__/db-memory-reducer-checkpoints.test.ts +273 -0
  63. package/src/__tests__/diagnostics-export.test.ts +70 -1
  64. package/src/__tests__/first-greeting.test.ts +80 -0
  65. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  66. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +3 -7
  67. package/src/__tests__/history-repair.test.ts +32 -10
  68. package/src/__tests__/http-conversation-lineage.test.ts +251 -0
  69. package/src/__tests__/image-source-path-reinject.test.ts +136 -0
  70. package/src/__tests__/inline-command-runner.test.ts +311 -0
  71. package/src/__tests__/inline-skill-authoring-guard.test.ts +220 -0
  72. package/src/__tests__/inline-skill-load-permissions.test.ts +435 -0
  73. package/src/__tests__/list-messages-attachments.test.ts +96 -0
  74. package/src/__tests__/llm-context-normalization.test.ts +1116 -0
  75. package/src/__tests__/llm-context-route-provider.test.ts +217 -0
  76. package/src/__tests__/llm-request-log-turn-query.test.ts +270 -0
  77. package/src/__tests__/media-generate-image.test.ts +47 -94
  78. package/src/__tests__/memory-brief-open-loops.test.ts +530 -0
  79. package/src/__tests__/memory-brief-time.test.ts +285 -0
  80. package/src/__tests__/memory-brief-wrapper.test.ts +311 -0
  81. package/src/__tests__/memory-chunk-archive.test.ts +400 -0
  82. package/src/__tests__/memory-chunk-dual-write.test.ts +453 -0
  83. package/src/__tests__/memory-episode-archive.test.ts +370 -0
  84. package/src/__tests__/memory-episode-dual-write.test.ts +626 -0
  85. package/src/__tests__/memory-lifecycle-e2e.test.ts +3 -1
  86. package/src/__tests__/memory-observation-archive.test.ts +375 -0
  87. package/src/__tests__/memory-observation-dual-write.test.ts +318 -0
  88. package/src/__tests__/memory-recall-quality.test.ts +7 -7
  89. package/src/__tests__/memory-reducer-store.test.ts +728 -0
  90. package/src/__tests__/memory-reducer-types.test.ts +699 -0
  91. package/src/__tests__/memory-reducer.test.ts +698 -0
  92. package/src/__tests__/memory-regressions.test.ts +6 -4
  93. package/src/__tests__/memory-simplified-config.test.ts +281 -0
  94. package/src/__tests__/migration-cross-version-compatibility.test.ts +4 -1
  95. package/src/__tests__/migration-export-http.test.ts +3 -1
  96. package/src/__tests__/migration-import-commit-http.test.ts +18 -4
  97. package/src/__tests__/migration-import-preflight-http.test.ts +1 -3
  98. package/src/__tests__/mime-builder.test.ts +3 -2
  99. package/src/__tests__/non-member-access-request.test.ts +12 -1
  100. package/src/__tests__/notification-decision-identity.test.ts +52 -0
  101. package/src/__tests__/oauth-apps-routes.test.ts +103 -0
  102. package/src/__tests__/oauth-store.test.ts +115 -0
  103. package/src/__tests__/parse-identity-fields.test.ts +129 -0
  104. package/src/__tests__/provider-error-scenarios.test.ts +1 -3
  105. package/src/__tests__/provider-failover-actual-provider.test.ts +66 -0
  106. package/src/__tests__/recording-handler.test.ts +17 -0
  107. package/src/__tests__/registry.test.ts +3 -8
  108. package/src/__tests__/relay-server.test.ts +1 -1
  109. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -3
  110. package/src/__tests__/schema-transforms.test.ts +165 -5
  111. package/src/__tests__/server-history-render.test.ts +2 -2
  112. package/src/__tests__/skill-load-inline-command.test.ts +598 -0
  113. package/src/__tests__/skill-load-inline-includes.test.ts +644 -0
  114. package/src/__tests__/skills-inline-command-expansions.test.ts +301 -0
  115. package/src/__tests__/skills-transitive-hash.test.ts +333 -0
  116. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  117. package/src/__tests__/slack-inbound-verification.test.ts +2 -2
  118. package/src/__tests__/starter-task-flow.test.ts +1 -0
  119. package/src/__tests__/suggestion-routes.test.ts +443 -0
  120. package/src/__tests__/swarm-conversation-integration.test.ts +1 -0
  121. package/src/__tests__/swarm-recursion.test.ts +1 -0
  122. package/src/__tests__/swarm-tool.test.ts +1 -0
  123. package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -0
  124. package/src/__tests__/tool-preview-lifecycle.test.ts +32 -5
  125. package/src/__tests__/top-level-renderer.test.ts +22 -0
  126. package/src/__tests__/turn-boundary-resolution.test.ts +243 -0
  127. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +320 -0
  128. package/src/__tests__/web-fetch.test.ts +6 -2
  129. package/src/__tests__/workspace-migration-006-services-config.test.ts +335 -0
  130. package/src/__tests__/workspace-migration-007-web-search-provider-rename.test.ts +312 -0
  131. package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +278 -0
  132. package/src/__tests__/workspace-migration-010-app-dir-rename.test.ts +275 -0
  133. package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +77 -0
  134. package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +401 -0
  135. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +328 -0
  136. package/src/__tests__/workspace-migration-seed-device-id.test.ts +6 -10
  137. package/src/agent/attachments.ts +27 -1
  138. package/src/agent/loop.ts +29 -1
  139. package/src/avatar/traits-png-sync.ts +80 -25
  140. package/src/bundler/app-bundler.ts +4 -4
  141. package/src/calls/call-domain.ts +1 -0
  142. package/src/calls/voice-session-bridge.ts +1 -0
  143. package/src/cli/commands/auth.ts +92 -0
  144. package/src/cli/commands/avatar.ts +7 -6
  145. package/src/cli/commands/config.ts +2 -0
  146. package/src/cli/commands/oauth/providers.ts +29 -0
  147. package/src/cli/program.ts +12 -0
  148. package/src/cli.ts +15 -48
  149. package/src/config/bundled-skills/app-builder/SKILL.md +103 -28
  150. package/src/config/bundled-skills/app-builder/TOOLS.json +5 -199
  151. package/src/config/bundled-skills/app-builder/tools/{app-query.ts → app-refresh.ts} +2 -2
  152. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +2 -3
  153. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +6 -9
  154. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +4 -6
  155. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +2 -3
  156. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +2 -3
  157. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +2 -3
  158. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +2 -3
  159. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +4 -6
  160. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +2 -3
  161. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +2 -3
  162. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -3
  163. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +2 -3
  164. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -3
  165. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +2 -3
  166. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -2
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +45 -72
  170. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +2 -2
  171. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
  172. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -3
  173. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  174. package/src/config/bundled-skills/skill-management/TOOLS.json +2 -2
  175. package/src/config/bundled-skills/slack/tools/shared.ts +19 -4
  176. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +2 -3
  177. package/src/config/bundled-skills/transcribe/SKILL.md +1 -1
  178. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -6
  179. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +19 -83
  180. package/src/config/bundled-tool-registry.ts +2 -14
  181. package/src/config/feature-flag-registry.json +24 -0
  182. package/src/config/loader.ts +65 -0
  183. package/src/config/raw-config-utils.ts +58 -0
  184. package/src/config/schema-utils.ts +28 -7
  185. package/src/config/schema.ts +20 -0
  186. package/src/config/schemas/elevenlabs.ts +18 -0
  187. package/src/config/schemas/memory-lifecycle.ts +4 -2
  188. package/src/config/schemas/memory-simplified.ts +101 -0
  189. package/src/config/schemas/memory-storage.ts +1 -1
  190. package/src/config/schemas/memory.ts +4 -0
  191. package/src/config/schemas/services.ts +8 -6
  192. package/src/config/skills.ts +50 -4
  193. package/src/contacts/contact-store.ts +13 -6
  194. package/src/contacts/contacts-write.ts +0 -1
  195. package/src/context/window-manager.ts +13 -2
  196. package/src/daemon/conversation-agent-loop-handlers.ts +54 -8
  197. package/src/daemon/conversation-agent-loop.ts +127 -20
  198. package/src/daemon/conversation-attachments.ts +18 -36
  199. package/src/daemon/conversation-error.ts +2 -1
  200. package/src/daemon/conversation-history.ts +18 -4
  201. package/src/daemon/conversation-lifecycle.ts +50 -16
  202. package/src/daemon/conversation-messaging.ts +70 -26
  203. package/src/daemon/conversation-process.ts +58 -34
  204. package/src/daemon/conversation-runtime-assembly.ts +22 -38
  205. package/src/daemon/conversation-slash.ts +121 -256
  206. package/src/daemon/conversation-surfaces.ts +170 -24
  207. package/src/daemon/conversation-tool-setup.ts +0 -6
  208. package/src/daemon/conversation-workspace.ts +21 -1
  209. package/src/daemon/conversation.ts +69 -30
  210. package/src/daemon/first-greeting.ts +35 -0
  211. package/src/daemon/handlers/config-embeddings.ts +156 -0
  212. package/src/daemon/handlers/config-model.ts +62 -26
  213. package/src/daemon/handlers/conversations.ts +0 -23
  214. package/src/daemon/handlers/identity.ts +12 -1
  215. package/src/daemon/handlers/recording.ts +26 -21
  216. package/src/daemon/host-cu-proxy.ts +2 -2
  217. package/src/daemon/lifecycle.ts +115 -65
  218. package/src/daemon/message-protocol.ts +3 -0
  219. package/src/daemon/message-types/conversations.ts +18 -0
  220. package/src/daemon/message-types/messages.ts +1 -0
  221. package/src/daemon/message-types/shared.ts +2 -0
  222. package/src/daemon/message-types/surfaces.ts +2 -0
  223. package/src/daemon/message-types/upgrades.ts +23 -0
  224. package/src/daemon/server.ts +83 -12
  225. package/src/daemon/shutdown-handlers.ts +8 -5
  226. package/src/daemon/startup-error.ts +9 -0
  227. package/src/daemon/tool-side-effects.ts +11 -28
  228. package/src/events/tool-permission-telemetry-listener.ts +1 -3
  229. package/src/followups/followup-store.ts +47 -1
  230. package/src/instrument.ts +0 -4
  231. package/src/media/app-icon-generator.ts +2 -2
  232. package/src/memory/app-git-service.ts +28 -16
  233. package/src/memory/app-store.ts +230 -41
  234. package/src/memory/archive-store.ts +400 -0
  235. package/src/memory/attachments-store.ts +558 -130
  236. package/src/memory/brief-formatting.ts +33 -0
  237. package/src/memory/brief-open-loops.ts +266 -0
  238. package/src/memory/brief-time.ts +161 -0
  239. package/src/memory/brief.ts +75 -0
  240. package/src/memory/conversation-attention-store.ts +70 -0
  241. package/src/memory/conversation-crud.ts +591 -8
  242. package/src/memory/conversation-directories.ts +125 -0
  243. package/src/memory/conversation-disk-view.ts +390 -0
  244. package/src/memory/conversation-key-store.ts +17 -5
  245. package/src/memory/conversation-queries.ts +5 -1
  246. package/src/memory/conversation-title-service.ts +21 -49
  247. package/src/memory/db-init.ts +40 -0
  248. package/src/memory/embedding-backend.ts +42 -53
  249. package/src/memory/embedding-gemini.test.ts +4 -4
  250. package/src/memory/embedding-local.ts +1 -3
  251. package/src/memory/embedding-ollama.ts +1 -3
  252. package/src/memory/embedding-openai.ts +1 -3
  253. package/src/memory/indexer.ts +114 -21
  254. package/src/memory/items-extractor.ts +42 -13
  255. package/src/memory/job-handlers/conversation-starters.ts +6 -1
  256. package/src/memory/job-handlers/embedding.test.ts +2 -4
  257. package/src/memory/job-handlers/embedding.ts +83 -0
  258. package/src/memory/job-utils.ts +1 -1
  259. package/src/memory/jobs-store.ts +6 -0
  260. package/src/memory/jobs-worker.ts +12 -0
  261. package/src/memory/llm-request-log-store.ts +100 -1
  262. package/src/memory/migrations/102-alter-table-columns.ts +5 -0
  263. package/src/memory/migrations/146-schedule-oneshot-routing.ts +3 -3
  264. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +66 -70
  265. package/src/memory/migrations/148-drop-reminders-table.ts +5 -9
  266. package/src/memory/migrations/160-drop-loopback-port-column.ts +1 -3
  267. package/src/memory/migrations/174-rename-thread-starters-table.ts +0 -7
  268. package/src/memory/migrations/178-oauth-providers-managed-service-config-key.ts +15 -0
  269. package/src/memory/migrations/179-llm-request-log-message-id.ts +16 -0
  270. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +66 -0
  271. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +46 -0
  272. package/src/memory/migrations/182-oauth-providers-display-metadata.ts +20 -0
  273. package/src/memory/migrations/183-add-conversation-fork-lineage.ts +22 -0
  274. package/src/memory/migrations/184-llm-request-log-provider.ts +12 -0
  275. package/src/memory/migrations/185-memory-brief-state.ts +52 -0
  276. package/src/memory/migrations/186-memory-archive.ts +109 -0
  277. package/src/memory/migrations/187-memory-reducer-checkpoints.ts +19 -0
  278. package/src/memory/migrations/index.ts +10 -0
  279. package/src/memory/migrations/registry.ts +13 -0
  280. package/src/memory/qdrant-client.ts +23 -4
  281. package/src/memory/reducer-store.ts +271 -0
  282. package/src/memory/reducer-types.ts +99 -0
  283. package/src/memory/reducer.ts +453 -0
  284. package/src/memory/retriever.test.ts +601 -2
  285. package/src/memory/retriever.ts +85 -9
  286. package/src/memory/schema/conversations.ts +9 -0
  287. package/src/memory/schema/index.ts +2 -0
  288. package/src/memory/schema/infrastructure.ts +13 -7
  289. package/src/memory/schema/memory-archive.ts +121 -0
  290. package/src/memory/schema/memory-brief.ts +55 -0
  291. package/src/memory/schema/oauth.ts +6 -0
  292. package/src/memory/search/semantic.ts +17 -4
  293. package/src/messaging/providers/gmail/mime-builder.ts +3 -1
  294. package/src/notifications/copy-composer.ts +26 -0
  295. package/src/notifications/decision-engine.ts +14 -1
  296. package/src/notifications/emit-signal.ts +1 -1
  297. package/src/notifications/signal.ts +36 -0
  298. package/src/oauth/byo-connection.test.ts +1 -45
  299. package/src/oauth/byo-connection.ts +2 -8
  300. package/src/oauth/connect-orchestrator.ts +15 -11
  301. package/src/oauth/connection-resolver.test.ts +191 -0
  302. package/src/oauth/connection-resolver.ts +66 -38
  303. package/src/oauth/connection.ts +0 -1
  304. package/src/oauth/oauth-store.ts +99 -47
  305. package/src/oauth/platform-connection.test.ts +0 -1
  306. package/src/oauth/platform-connection.ts +11 -3
  307. package/src/oauth/seed-providers.ts +78 -3
  308. package/src/oauth/token-persistence.ts +16 -10
  309. package/src/permissions/checker.ts +160 -14
  310. package/src/permissions/defaults.ts +14 -0
  311. package/src/prompts/templates/BOOTSTRAP.md +2 -0
  312. package/src/providers/anthropic/client.ts +8 -1
  313. package/src/providers/failover.ts +4 -1
  314. package/src/providers/gemini/client.ts +50 -0
  315. package/src/providers/model-catalog.ts +92 -0
  316. package/src/providers/model-intents.ts +29 -20
  317. package/src/providers/openai/client.ts +49 -0
  318. package/src/providers/types.ts +2 -0
  319. package/src/runtime/access-request-helper.ts +16 -7
  320. package/src/runtime/auth/credential-service.ts +3 -1
  321. package/src/runtime/auth/route-policy.ts +14 -1
  322. package/src/runtime/btw-sidechain.ts +101 -0
  323. package/src/runtime/channel-reply-delivery.ts +17 -1
  324. package/src/runtime/http-router.ts +3 -1
  325. package/src/runtime/http-server.ts +196 -141
  326. package/src/runtime/http-types.ts +1 -0
  327. package/src/runtime/migrations/vbundle-builder.ts +5 -1
  328. package/src/runtime/routes/access-request-decision.ts +41 -0
  329. package/src/runtime/routes/app-management-routes.ts +6 -3
  330. package/src/runtime/routes/app-routes.ts +7 -3
  331. package/src/runtime/routes/approval-routes.ts +1 -0
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +34 -2
  333. package/src/runtime/routes/attachment-routes.ts +45 -15
  334. package/src/runtime/routes/btw-routes.ts +21 -61
  335. package/src/runtime/routes/conversation-management-routes.ts +74 -0
  336. package/src/runtime/routes/conversation-query-routes.ts +187 -10
  337. package/src/runtime/routes/conversation-routes.ts +269 -28
  338. package/src/runtime/routes/conversation-starter-routes.ts +9 -11
  339. package/src/runtime/routes/diagnostics-routes.ts +1 -0
  340. package/src/runtime/routes/identity-routes.ts +2 -35
  341. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -2
  342. package/src/runtime/routes/llm-context-normalization.ts +1212 -0
  343. package/src/runtime/routes/log-export-routes.ts +3 -0
  344. package/src/runtime/routes/memory-item-routes.test.ts +34 -0
  345. package/src/runtime/routes/memory-item-routes.ts +94 -5
  346. package/src/runtime/routes/migration-routes.ts +4 -1
  347. package/src/runtime/routes/oauth-apps.ts +291 -0
  348. package/src/runtime/routes/secret-routes.ts +30 -1
  349. package/src/runtime/routes/settings-routes.ts +14 -0
  350. package/src/runtime/routes/surface-action-routes.ts +68 -1
  351. package/src/runtime/routes/trace-event-routes.ts +4 -1
  352. package/src/schedule/schedule-store.ts +30 -21
  353. package/src/security/secure-keys.ts +21 -0
  354. package/src/signals/bash.ts +1 -1
  355. package/src/skills/inline-command-expansions.ts +204 -0
  356. package/src/skills/inline-command-render.ts +127 -0
  357. package/src/skills/inline-command-runner.ts +242 -0
  358. package/src/skills/transitive-version-hash.ts +88 -0
  359. package/src/swarm/backend-claude-code.ts +3 -6
  360. package/src/tasks/task-store.ts +43 -1
  361. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  362. package/src/telemetry/usage-telemetry-reporter.ts +3 -1
  363. package/src/tools/AGENTS.md +6 -10
  364. package/src/tools/apps/executors.ts +17 -232
  365. package/src/tools/claude-code/claude-code.ts +2 -3
  366. package/src/tools/credentials/vault.ts +7 -12
  367. package/src/tools/host-filesystem/read.ts +13 -10
  368. package/src/tools/network/__tests__/web-search.test.ts +4 -2
  369. package/src/tools/permission-checker.ts +8 -1
  370. package/src/tools/schedule/list.ts +2 -7
  371. package/src/tools/schema-transforms.ts +5 -0
  372. package/src/tools/shared/filesystem/format-diff.ts +2 -7
  373. package/src/tools/skills/execute.ts +1 -1
  374. package/src/tools/skills/load.ts +140 -6
  375. package/src/tools/tool-manifest.ts +0 -6
  376. package/src/tools/ui-surface/definitions.ts +2 -2
  377. package/src/util/device-id.ts +28 -5
  378. package/src/util/platform.ts +24 -0
  379. package/src/util/pricing.ts +1 -0
  380. package/src/util/retry.ts +1 -3
  381. package/src/workspace/migrations/003-seed-device-id.ts +3 -4
  382. package/src/workspace/migrations/006-services-config.ts +5 -0
  383. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +12 -0
  384. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +10 -0
  385. package/src/workspace/migrations/010-app-dir-rename.ts +223 -0
  386. package/src/workspace/migrations/{002-backfill-installation-id.ts → 011-backfill-installation-id.ts} +24 -13
  387. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +64 -0
  388. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +11 -0
  389. package/src/workspace/migrations/rebuild-conversation-disk-view.ts +186 -0
  390. package/src/workspace/migrations/registry.ts +11 -1
  391. package/src/workspace/top-level-renderer.ts +12 -0
  392. package/src/__tests__/asset-materialize-tool.test.ts +0 -523
  393. package/src/__tests__/asset-search-tool.test.ts +0 -536
  394. package/src/__tests__/fixtures/media-reuse-fixtures.ts +0 -56
  395. package/src/__tests__/media-reuse-story.e2e.test.ts +0 -762
  396. package/src/__tests__/media-visibility-policy.test.ts +0 -190
  397. package/src/config/bundled-skills/app-builder/tools/app-file-edit.ts +0 -14
  398. package/src/config/bundled-skills/app-builder/tools/app-file-list.ts +0 -13
  399. package/src/config/bundled-skills/app-builder/tools/app-file-read.ts +0 -21
  400. package/src/config/bundled-skills/app-builder/tools/app-file-write.ts +0 -14
  401. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -13
  402. package/src/config/bundled-skills/app-builder/tools/app-update.ts +0 -23
  403. package/src/daemon/media-visibility-policy.ts +0 -59
  404. package/src/tools/assets/materialize.ts +0 -248
  405. package/src/tools/assets/search.ts +0 -400
package/docs/skills.md CHANGED
@@ -156,3 +156,103 @@ Trust rules are stored in `~/.vellum/protected/trust.json`. You can inspect this
156
156
  ### "A skill tool keeps prompting even though I approved it."
157
157
 
158
158
  Check whether the rule has the correct `executionTarget` — a rule scoped to `sandbox` will not match a tool running on `host`.
159
+
160
+ ## Inline Command Expansions
161
+
162
+ Skills can embed dynamic content by using the **inline command expansion** syntax. When a skill containing these tokens is loaded, each token is executed and replaced with its output before the skill body is delivered to the model. The syntax is shown in the fenced block below.
163
+
164
+ This syntax is intentionally compatible with the convention established by [inline skill commands](https://x.com) for portable cross-agent skill authoring. Vellum adopts the exact same token format so that externally authored skills load without rewriting — but applies stricter execution constraints.
165
+
166
+ ### Syntax
167
+
168
+ The canonical syntax is:
169
+
170
+ ```
171
+ !`command`
172
+ ```
173
+
174
+ Where `command` is any shell command string. The exclamation mark immediately precedes the opening backtick with no whitespace in between. Examples:
175
+
176
+ ```markdown
177
+ Current branch: !`git branch --show-current`
178
+ Recent changes: !`git log --oneline -5`
179
+ Project info: !`cat package.json | jq '.name, .version'`
180
+ ```
181
+
182
+ Tokens inside fenced code blocks (` ``` ` or `~~~`) are **not** expanded — they are treated as documentation examples. This allows skills to safely include syntax examples without triggering execution.
183
+
184
+ ### Parsing rules
185
+
186
+ The parser (`parseInlineCommandExpansions`) enforces fail-closed semantics:
187
+
188
+ | Condition | Behavior |
189
+ | ------------------------------------------------- | ---------------------- |
190
+ | Well-formed token outside fenced code | Parsed as an expansion |
191
+ | Token inside a fenced code block | Skipped (not expanded) |
192
+ | Empty command text (no content between backticks) | Rejected as malformed |
193
+ | Whitespace-only command text | Rejected as malformed |
194
+ | Unmatched opening (no closing backtick found) | Rejected as malformed |
195
+ | Nested backticks inside command text | Rejected as malformed |
196
+
197
+ Malformed tokens do not silently pass through — they are collected as errors and logged. If a skill body contains any malformed tokens, the valid tokens are still expanded, but the errors are reported for diagnostics.
198
+
199
+ ### Feature flag
200
+
201
+ Inline command expansion is gated by the `inline-skill-commands` feature flag (key: `feature_flags.inline-skill-commands.enabled`). The flag defaults to **enabled**.
202
+
203
+ When the flag is disabled and a skill contains inline command expansion tokens, `skill_load` returns an error rather than delivering unexpanded tokens to the model. This fail-closed behavior prevents the LLM from seeing raw expansion tokens and attempting to interpret them.
204
+
205
+ ### Approval model
206
+
207
+ Skills with inline command expansions use a separate permission namespace: `skill_load_dynamic:*`. This ensures they do not silently inherit the permissive default `skill_load:*` allow rule.
208
+
209
+ When a user is prompted to approve a dynamic skill load, the allowlist options are:
210
+
211
+ | Option | Pattern | Behavior |
212
+ | -------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
213
+ | Version-pinned | `skill_load_dynamic:<id>@<transitive-hash>` | Approved for this exact version only. Any change to the skill or its includes invalidates the rule. |
214
+ | Any-version | `skill_load_dynamic:<id>` | Approved for all versions of this skill. |
215
+
216
+ The transitive hash covers the skill's own content plus all included skills, so a change anywhere in the dependency graph triggers re-approval for version-pinned rules.
217
+
218
+ ### v1 execution limits
219
+
220
+ In the initial implementation, inline command execution enforces these constraints:
221
+
222
+ | Constraint | Value |
223
+ | ---------------- | ------------------------------------------------------- |
224
+ | Execution target | Sandbox only (no host fallback) |
225
+ | Network access | Off (no outbound connections) |
226
+ | Environment | Sanitized (no API keys, tokens, or credentials) |
227
+ | Timeout | 10 seconds per command |
228
+ | Output cap | 20,000 characters (truncated with `[output truncated]`) |
229
+ | Binary output | Rejected if >10% non-printable characters |
230
+ | ANSI sequences | Stripped before output processing |
231
+ | stderr | Discarded (only stdout is captured) |
232
+
233
+ Commands that fail (timeout, non-zero exit, spawn failure, binary output) produce a deterministic stub in the rendered body rather than leaking raw error output:
234
+
235
+ ```
236
+ <inline_skill_command index="0">[inline command unavailable: command timed out]</inline_skill_command>
237
+ ```
238
+
239
+ ### Eligible skill sources
240
+
241
+ Only **bundled**, **managed**, and **workspace** skills may use inline command expansions. Third-party **extra** skill sources are explicitly rejected — `skill_load` returns an error if an extra-source skill contains inline expansion tokens.
242
+
243
+ | Source | Eligible | Reason |
244
+ | ----------- | -------- | -------------------------------------- |
245
+ | `bundled` | Yes | Shipped with the application, trusted |
246
+ | `managed` | Yes | User-installed, subject to approval |
247
+ | `workspace` | Yes | Project-local, subject to approval |
248
+ | `extra` | No | Third-party roots, out of scope for v1 |
249
+
250
+ ### Fail-closed summary
251
+
252
+ The system fails closed at every layer:
253
+
254
+ 1. **Flag off** — skill_load returns an error, tokens never reach the model.
255
+ 2. **Malformed syntax** — rejected by the parser, logged as errors.
256
+ 3. **Unsupported source** — skill_load returns an error for extra-source skills.
257
+ 4. **Command failure** — deterministic stub replaces the token, no raw stderr.
258
+ 5. **No permission** — `skill_load_dynamic:*` namespace requires explicit approval.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -1629,4 +1629,115 @@ describe("AgentLoop", () => {
1629
1629
  );
1630
1630
  expect(textBlock!.text).toBe("Normal response with no placeholders.");
1631
1631
  });
1632
+
1633
+ // Tool error retry nudge — when a tool returns isError: true, the loop
1634
+ // should inject a system_notice nudging the LLM to retry instead of ending.
1635
+ test("injects retry nudge system_notice when tool returns an error", async () => {
1636
+ const { provider, calls } = createMockProvider([
1637
+ // First turn: LLM calls a tool that errors
1638
+ toolUseResponse("t1", "read_file", { path: "/missing.txt" }),
1639
+ // Second turn: LLM retries after seeing the error + nudge
1640
+ toolUseResponse("t2", "read_file", { path: "/existing.txt" }),
1641
+ // Third turn: LLM responds with success
1642
+ textResponse("Got the file."),
1643
+ ]);
1644
+
1645
+ let callCount = 0;
1646
+ const toolExecutor = async (
1647
+ _name: string,
1648
+ _input: Record<string, unknown>,
1649
+ ) => {
1650
+ callCount++;
1651
+ if (callCount === 1) {
1652
+ return {
1653
+ content:
1654
+ '{"error":"name is required and must be a non-empty string"}',
1655
+ isError: true,
1656
+ };
1657
+ }
1658
+ return { content: "file contents", isError: false };
1659
+ };
1660
+
1661
+ const loop = new AgentLoop(
1662
+ provider,
1663
+ "system",
1664
+ {},
1665
+ dummyTools,
1666
+ toolExecutor,
1667
+ );
1668
+ await loop.run([userMessage], () => {});
1669
+
1670
+ // Provider should have been called 3 times (error -> retry -> final text)
1671
+ expect(calls).toHaveLength(3);
1672
+
1673
+ // The second call's messages should contain the retry nudge system_notice
1674
+ const secondCallMessages = calls[1].messages;
1675
+ const toolResultMessage = secondCallMessages[secondCallMessages.length - 1];
1676
+ expect(toolResultMessage.role).toBe("user");
1677
+
1678
+ const retryNudge = toolResultMessage.content.find(
1679
+ (b): b is Extract<ContentBlock, { type: "text" }> =>
1680
+ b.type === "text" && b.text.includes("looks recoverable"),
1681
+ );
1682
+ expect(retryNudge).toBeDefined();
1683
+
1684
+ // The third call should NOT have the retry nudge (successful tool result)
1685
+ const thirdCallMessages = calls[2].messages;
1686
+ const thirdToolResultMessage =
1687
+ thirdCallMessages[thirdCallMessages.length - 1];
1688
+ const noRetryNudge = thirdToolResultMessage.content.find(
1689
+ (b): b is Extract<ContentBlock, { type: "text" }> =>
1690
+ b.type === "text" && b.text.includes("looks recoverable"),
1691
+ );
1692
+ expect(noRetryNudge).toBeUndefined();
1693
+ });
1694
+
1695
+ // Retry nudge stops after MAX_CONSECUTIVE_ERROR_NUDGES (3) consecutive errors
1696
+ test("stops injecting retry nudge after 3 consecutive error turns", async () => {
1697
+ const { provider, calls } = createMockProvider([
1698
+ // 4 consecutive error turns, then final text
1699
+ toolUseResponse("t1", "read_file", { path: "/a" }),
1700
+ toolUseResponse("t2", "read_file", { path: "/b" }),
1701
+ toolUseResponse("t3", "read_file", { path: "/c" }),
1702
+ toolUseResponse("t4", "read_file", { path: "/d" }),
1703
+ textResponse("Giving up."),
1704
+ ]);
1705
+
1706
+ const toolExecutor = async (
1707
+ _name: string,
1708
+ _input: Record<string, unknown>,
1709
+ ) => {
1710
+ return { content: "service unavailable", isError: true };
1711
+ };
1712
+
1713
+ const loop = new AgentLoop(
1714
+ provider,
1715
+ "system",
1716
+ {},
1717
+ dummyTools,
1718
+ toolExecutor,
1719
+ );
1720
+ await loop.run([userMessage], () => {});
1721
+
1722
+ expect(calls).toHaveLength(5);
1723
+
1724
+ // Helper to check if a call's last user message has the retry nudge
1725
+ const hasRetryNudge = (callIndex: number): boolean => {
1726
+ const msgs = calls[callIndex].messages;
1727
+ const lastMsg = msgs[msgs.length - 1];
1728
+ return lastMsg.content.some(
1729
+ (b) =>
1730
+ b.type === "text" &&
1731
+ "text" in b &&
1732
+ (b as { text: string }).text.includes("looks recoverable"),
1733
+ );
1734
+ };
1735
+
1736
+ // Turns 1-3 should have the nudge
1737
+ expect(hasRetryNudge(1)).toBe(true);
1738
+ expect(hasRetryNudge(2)).toBe(true);
1739
+ expect(hasRetryNudge(3)).toBe(true);
1740
+ // Turn 4 should NOT have the nudge (exceeded limit)
1741
+ expect(hasRetryNudge(4)).toBe(false);
1742
+ });
1632
1743
  });
@@ -2,8 +2,8 @@
2
2
  * Guard test: always-loaded tool count
3
3
  *
4
4
  * This test asserts the exact set of tools that are active when no client is
5
- * connected, no host proxy is available, no attachments are present, and no
6
- * special channel capabilities exist. This represents the minimal "always-loaded"
5
+ * connected, no host proxy is available, and no special channel capabilities
6
+ * exist. This represents the minimal "always-loaded"
7
7
  * baseline that is sent to the LLM on every turn.
8
8
  *
9
9
  * Adding a tool to this set increases token cost for every request. If this test
@@ -32,14 +32,13 @@ describe("always-loaded tool count", () => {
32
32
  await initializeTools();
33
33
  const allDefs = buildToolDefinitions();
34
34
 
35
- // Minimal context: no client, no attachments, no capabilities
35
+ // Minimal context: no client, no capabilities
36
36
  const minimalContext: SkillProjectionContext = {
37
37
  skillProjectionState: new Map(),
38
38
  skillProjectionCache: {},
39
39
  coreToolNames: new Set(),
40
40
  toolsDisabledDepth: 0,
41
41
  hasNoClient: true,
42
- hasAttachments: false,
43
42
  channelCapabilities: undefined,
44
43
  };
45
44
 
@@ -2,7 +2,6 @@ import { describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { AppDefinition } from "../memory/app-store.js";
4
4
  import type { AppStore } from "../tools/apps/executors.js";
5
- import type { EditEngineResult } from "../tools/shared/filesystem/edit-engine.js";
6
5
  import type { ToolContext } from "../tools/types.js";
7
6
 
8
7
  // ---------------------------------------------------------------------------
@@ -26,24 +25,11 @@ function makeMockStore(overrides: Partial<AppStore> = {}): AppStore {
26
25
  return {
27
26
  getApp: () => makeApp(),
28
27
  listApps: () => [makeApp()],
29
- queryAppRecords: () => [],
30
- listAppFiles: () => ["index.html"],
31
- readAppFile: () => "<h1>Hi</h1>",
32
28
  createApp: (params) =>
33
29
  makeApp({ name: params.name, description: params.description }),
34
30
  updateApp: (id, updates) => makeApp({ id, ...updates }),
35
31
  deleteApp: () => {},
36
32
  writeAppFile: () => {},
37
- editAppFile: () =>
38
- ({
39
- ok: true,
40
- updatedContent: "new",
41
- matchCount: 1,
42
- matchMethod: "exact",
43
- similarity: 1,
44
- actualOld: "old",
45
- actualNew: "new",
46
- }) as EditEngineResult,
47
33
  ...overrides,
48
34
  };
49
35
  }
@@ -66,6 +52,11 @@ const mockStore = makeMockStore();
66
52
  mock.module("../memory/app-store.js", () => ({
67
53
  ...mockStore,
68
54
  getAppsDir: () => "/tmp/test-apps",
55
+ getAppDirPath: (appId: string) => `/tmp/test-apps/${appId}`,
56
+ resolveAppDir: (id: string) => ({
57
+ dirName: id,
58
+ appDir: `/tmp/test-apps/${id}`,
59
+ }),
69
60
  isMultifileApp: (app: AppDefinition) => app.formatVersion === 2,
70
61
  }));
71
62
 
@@ -85,13 +76,7 @@ mock.module("../bundler/app-compiler.js", () => ({
85
76
 
86
77
  import * as appCreateScript from "../config/bundled-skills/app-builder/tools/app-create.js";
87
78
  import * as appDeleteScript from "../config/bundled-skills/app-builder/tools/app-delete.js";
88
- import * as appFileEditScript from "../config/bundled-skills/app-builder/tools/app-file-edit.js";
89
- import * as appFileListScript from "../config/bundled-skills/app-builder/tools/app-file-list.js";
90
- import * as appFileReadScript from "../config/bundled-skills/app-builder/tools/app-file-read.js";
91
- import * as appFileWriteScript from "../config/bundled-skills/app-builder/tools/app-file-write.js";
92
- import * as appListScript from "../config/bundled-skills/app-builder/tools/app-list.js";
93
- import * as appQueryScript from "../config/bundled-skills/app-builder/tools/app-query.js";
94
- import * as appUpdateScript from "../config/bundled-skills/app-builder/tools/app-update.js";
79
+ import * as appRefreshScript from "../config/bundled-skills/app-builder/tools/app-refresh.js";
95
80
 
96
81
  // ---------------------------------------------------------------------------
97
82
  // Tests
@@ -142,58 +127,6 @@ describe("app-builder skill tool scripts", () => {
142
127
  });
143
128
  });
144
129
 
145
- // ---- app-list ----------------------------------------------------------
146
-
147
- describe("app-list", () => {
148
- test("exports a run function", () => {
149
- expect(typeof appListScript.run).toBe("function");
150
- });
151
-
152
- test("delegates to executeAppList and returns result", async () => {
153
- const result = await appListScript.run({}, makeContext());
154
- expect(result.isError).toBe(false);
155
- const parsed = JSON.parse(result.content);
156
- expect(Array.isArray(parsed)).toBe(true);
157
- expect(parsed.length).toBeGreaterThan(0);
158
- expect(parsed[0].id).toBe("app-1");
159
- });
160
- });
161
-
162
- // ---- app-query ---------------------------------------------------------
163
-
164
- describe("app-query", () => {
165
- test("exports a run function", () => {
166
- expect(typeof appQueryScript.run).toBe("function");
167
- });
168
-
169
- test("delegates to executeAppQuery and returns result", async () => {
170
- const result = await appQueryScript.run(
171
- { app_id: "app-1" },
172
- makeContext(),
173
- );
174
- expect(result.isError).toBe(false);
175
- expect(JSON.parse(result.content)).toEqual([]);
176
- });
177
- });
178
-
179
- // ---- app-update --------------------------------------------------------
180
-
181
- describe("app-update", () => {
182
- test("exports a run function", () => {
183
- expect(typeof appUpdateScript.run).toBe("function");
184
- });
185
-
186
- test("delegates to executeAppUpdate and returns result", async () => {
187
- const result = await appUpdateScript.run(
188
- { app_id: "app-1", name: "Updated" },
189
- makeContext(),
190
- );
191
- expect(result.isError).toBe(false);
192
- const parsed = JSON.parse(result.content);
193
- expect(parsed.name).toBe("Updated");
194
- });
195
- });
196
-
197
130
  // ---- app-delete --------------------------------------------------------
198
131
 
199
132
  describe("app-delete", () => {
@@ -213,93 +146,22 @@ describe("app-builder skill tool scripts", () => {
213
146
  });
214
147
  });
215
148
 
216
- // ---- app-file-list -----------------------------------------------------
149
+ // ---- app-refresh -------------------------------------------------------
217
150
 
218
- describe("app-file-list", () => {
151
+ describe("app-refresh", () => {
219
152
  test("exports a run function", () => {
220
- expect(typeof appFileListScript.run).toBe("function");
153
+ expect(typeof appRefreshScript.run).toBe("function");
221
154
  });
222
155
 
223
- test("delegates to executeAppFileList and returns result", async () => {
224
- const result = await appFileListScript.run(
156
+ test("delegates to executeAppRefresh and returns result", async () => {
157
+ const result = await appRefreshScript.run(
225
158
  { app_id: "app-1" },
226
159
  makeContext(),
227
160
  );
228
161
  expect(result.isError).toBe(false);
229
- expect(JSON.parse(result.content)).toEqual(["index.html"]);
230
- });
231
- });
232
-
233
- // ---- app-file-read -----------------------------------------------------
234
-
235
- describe("app-file-read", () => {
236
- test("exports a run function", () => {
237
- expect(typeof appFileReadScript.run).toBe("function");
238
- });
239
-
240
- test("delegates to executeAppFileRead and returns formatted content", async () => {
241
- const result = await appFileReadScript.run(
242
- { app_id: "app-1", path: "index.html" },
243
- makeContext(),
244
- );
245
- expect(result.isError).toBe(false);
246
- // Content should include line numbers
247
- expect(result.content).toContain("1\t");
248
- });
249
- });
250
-
251
- // ---- app-file-edit -----------------------------------------------------
252
-
253
- describe("app-file-edit", () => {
254
- test("exports a run function", () => {
255
- expect(typeof appFileEditScript.run).toBe("function");
256
- });
257
-
258
- test("delegates to executeAppFileEdit and returns result", async () => {
259
- const result = await appFileEditScript.run(
260
- {
261
- app_id: "app-1",
262
- path: "index.html",
263
- old_string: "old",
264
- new_string: "new",
265
- },
266
- makeContext(),
267
- );
268
- expect(result.isError).toBe(false);
269
162
  const parsed = JSON.parse(result.content);
270
- expect(parsed.ok).toBe(true);
271
- });
272
-
273
- test("returns error when old_string is empty", async () => {
274
- const result = await appFileEditScript.run(
275
- {
276
- app_id: "app-1",
277
- path: "index.html",
278
- old_string: "",
279
- new_string: "new",
280
- },
281
- makeContext(),
282
- );
283
- expect(result.isError).toBe(true);
284
- });
285
- });
286
-
287
- // ---- app-file-write ----------------------------------------------------
288
-
289
- describe("app-file-write", () => {
290
- test("exports a run function", () => {
291
- expect(typeof appFileWriteScript.run).toBe("function");
292
- });
293
-
294
- test("delegates to executeAppFileWrite and returns result", async () => {
295
- const result = await appFileWriteScript.run(
296
- { app_id: "app-1", path: "new.html", content: "<div/>" },
297
- makeContext(),
298
- );
299
- expect(result.isError).toBe(false);
300
- const parsed = JSON.parse(result.content);
301
- expect(parsed.written).toBe(true);
302
- expect(parsed.path).toBe("new.html");
163
+ expect(parsed.refreshed).toBe(true);
164
+ expect(parsed.appId).toBe("app-1");
303
165
  });
304
166
  });
305
167
  });
@@ -0,0 +1,78 @@
1
+ import { execSync } from "node:child_process";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ /**
5
+ * Guard test: files outside app-store.ts must not import getAppsDir and use
6
+ * it to construct paths with an app ID. All app path construction must go
7
+ * through getAppDirPath() or resolveAppDir() from app-store.ts.
8
+ *
9
+ * This prevents regressions where new code bypasses the dirName-based path
10
+ * resolution and constructs UUID-based paths directly.
11
+ *
12
+ * Allowlist: only app-store.ts itself, app-git-service.ts (uses getAppsDir
13
+ * for the git repo root, not for per-app paths), and workspace migrations
14
+ * (self-contained, don't import from app-store).
15
+ */
16
+
17
+ /** Files that are permitted to import getAppsDir. */
18
+ const ALLOWLIST = new Set([
19
+ "assistant/src/memory/app-store.ts", // defines getAppsDir
20
+ "assistant/src/memory/app-git-service.ts", // uses getAppsDir for git repo root, not per-app paths
21
+ ]);
22
+
23
+ function isTestFile(filePath: string): boolean {
24
+ return (
25
+ filePath.includes("/__tests__/") ||
26
+ filePath.endsWith(".test.ts") ||
27
+ filePath.endsWith(".test.js") ||
28
+ filePath.endsWith(".spec.ts")
29
+ );
30
+ }
31
+
32
+ function isMigrationFile(filePath: string): boolean {
33
+ return filePath.includes("/workspace/migrations/");
34
+ }
35
+
36
+ describe("app directory path construction guard", () => {
37
+ test("no non-allowlisted production files import getAppsDir", () => {
38
+ // Search for files that import getAppsDir (not just mention it in comments)
39
+ const pattern = "import.*getAppsDir.*from|getAppsDir\\(\\)";
40
+
41
+ let grepOutput = "";
42
+ try {
43
+ grepOutput = execSync(`git grep -lE '${pattern}' -- '*.ts'`, {
44
+ encoding: "utf-8",
45
+ cwd: process.cwd() + "/..",
46
+ }).trim();
47
+ } catch (err) {
48
+ // Exit code 1 means no matches
49
+ if ((err as { status?: number }).status === 1) {
50
+ return;
51
+ }
52
+ throw err;
53
+ }
54
+
55
+ const files = grepOutput.split("\n").filter((f) => f.length > 0);
56
+ const violations = files.filter((f) => {
57
+ if (isTestFile(f)) return false;
58
+ if (isMigrationFile(f)) return false;
59
+ if (ALLOWLIST.has(f)) return false;
60
+ return true;
61
+ });
62
+
63
+ if (violations.length > 0) {
64
+ const message = [
65
+ "Found non-allowlisted production files importing or using getAppsDir().",
66
+ "Use getAppDirPath(appId) or resolveAppDir(appId) from app-store.ts instead.",
67
+ "",
68
+ "Violations:",
69
+ ...violations.map((f) => ` - ${f}`),
70
+ "",
71
+ "To fix: replace getAppsDir() + appId path construction with getAppDirPath(appId).",
72
+ "If this is an intentional exception, add it to the ALLOWLIST in app-dir-path-guard.test.ts.",
73
+ ].join("\n");
74
+
75
+ expect(violations, message).toEqual([]);
76
+ }
77
+ });
78
+ });