@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
@@ -0,0 +1,220 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+
5
+ import { parseFrontmatterFields } from "../skills/frontmatter.js";
6
+ import { parseInlineCommandExpansions } from "../skills/inline-command-expansions.js";
7
+
8
+ /**
9
+ * Guard test: scan bundled and first-party skills for malformed inline
10
+ * command expansion syntax (`!\`...\``).
11
+ *
12
+ * This guard ensures that:
13
+ * 1. No skill ships with malformed inline expansion tokens (empty commands,
14
+ * unmatched backticks, nested backticks) that would be rejected by the
15
+ * parser at runtime.
16
+ * 2. Fenced-code examples containing `!\`...\`` syntax are correctly
17
+ * ignored by the parser and not treated as live commands — verified by
18
+ * fixture coverage below.
19
+ *
20
+ * See assistant/docs/skills.md "Inline Command Expansions" for the full
21
+ * syntax specification and security model.
22
+ */
23
+
24
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
25
+
26
+ function getRepoRoot(): string {
27
+ return join(process.cwd(), "..");
28
+ }
29
+
30
+ function getBundledSkillsDir(): string {
31
+ return join(process.cwd(), "src", "config", "bundled-skills");
32
+ }
33
+
34
+ function getFirstPartySkillsDir(): string {
35
+ return join(getRepoRoot(), "skills");
36
+ }
37
+
38
+ /**
39
+ * Discover all SKILL.md files under a given directory (one level deep).
40
+ * Returns an array of { id, skillFilePath } entries.
41
+ */
42
+ function discoverSkillFiles(
43
+ dir: string,
44
+ ): Array<{ id: string; skillFilePath: string }> {
45
+ if (!existsSync(dir)) return [];
46
+
47
+ const results: Array<{ id: string; skillFilePath: string }> = [];
48
+ const entries = readdirSync(dir, { withFileTypes: true });
49
+
50
+ for (const entry of entries) {
51
+ if (!entry.isDirectory()) continue;
52
+ const skillFilePath = join(dir, entry.name, "SKILL.md");
53
+ if (existsSync(skillFilePath) && statSync(skillFilePath).isFile()) {
54
+ results.push({ id: entry.name, skillFilePath });
55
+ }
56
+ }
57
+
58
+ return results.sort((a, b) => a.id.localeCompare(b.id));
59
+ }
60
+
61
+ /**
62
+ * Extract the markdown body from a SKILL.md file (strip frontmatter).
63
+ * Returns the body text, or null if the file has no valid frontmatter.
64
+ */
65
+ function extractBody(filePath: string): string | undefined {
66
+ const content = readFileSync(filePath, "utf-8");
67
+ const result = parseFrontmatterFields(content);
68
+ return result?.body;
69
+ }
70
+
71
+ // ─── Tests ────────────────────────────────────────────────────────────────────
72
+
73
+ describe("inline skill authoring guard", () => {
74
+ test("bundled skills contain no malformed inline command expansion tokens", () => {
75
+ const bundledDir = getBundledSkillsDir();
76
+ const skills = discoverSkillFiles(bundledDir);
77
+
78
+ const violations: string[] = [];
79
+
80
+ for (const { id, skillFilePath } of skills) {
81
+ const body = extractBody(skillFilePath);
82
+ if (body === undefined) continue;
83
+
84
+ const result = parseInlineCommandExpansions(body);
85
+ if (result.errors.length > 0) {
86
+ for (const error of result.errors) {
87
+ violations.push(
88
+ `bundled/${id}: ${error.reason} at offset ${error.offset} — ${JSON.stringify(error.raw)}`,
89
+ );
90
+ }
91
+ }
92
+ }
93
+
94
+ if (violations.length > 0) {
95
+ const message = [
96
+ "Found bundled skills with malformed inline command expansion tokens.",
97
+ "Fix the syntax or move examples inside fenced code blocks.",
98
+ "",
99
+ "Violations:",
100
+ ...violations.map((v) => ` - ${v}`),
101
+ ].join("\n");
102
+
103
+ expect(violations, message).toEqual([]);
104
+ }
105
+ });
106
+
107
+ test("first-party skills contain no malformed inline command expansion tokens", () => {
108
+ const firstPartyDir = getFirstPartySkillsDir();
109
+ const skills = discoverSkillFiles(firstPartyDir);
110
+
111
+ const violations: string[] = [];
112
+
113
+ for (const { id, skillFilePath } of skills) {
114
+ const body = extractBody(skillFilePath);
115
+ if (body === undefined) continue;
116
+
117
+ const result = parseInlineCommandExpansions(body);
118
+ if (result.errors.length > 0) {
119
+ for (const error of result.errors) {
120
+ violations.push(
121
+ `skills/${id}: ${error.reason} at offset ${error.offset} — ${JSON.stringify(error.raw)}`,
122
+ );
123
+ }
124
+ }
125
+ }
126
+
127
+ if (violations.length > 0) {
128
+ const message = [
129
+ "Found first-party skills with malformed inline command expansion tokens.",
130
+ "Fix the syntax or move examples inside fenced code blocks.",
131
+ "",
132
+ "Violations:",
133
+ ...violations.map((v) => ` - ${v}`),
134
+ ].join("\n");
135
+
136
+ expect(violations, message).toEqual([]);
137
+ }
138
+ });
139
+
140
+ test("fenced-code examples containing inline expansion syntax are not treated as live commands", () => {
141
+ // This fixture verifies the parser's fenced-code-block exclusion logic.
142
+ // Skills that document the `!\`command\`` syntax in fenced code blocks
143
+ // must not have those examples picked up as live expansions.
144
+ const fixture = [
145
+ "# Example Skill",
146
+ "",
147
+ "Use inline commands to inject dynamic context:",
148
+ "",
149
+ "```markdown",
150
+ "Current branch: !`git branch --show-current`",
151
+ "Recent commits: !`git log --oneline -5`",
152
+ "```",
153
+ "",
154
+ "~~~",
155
+ "!`echo hello`",
156
+ "~~~",
157
+ "",
158
+ "````",
159
+ "```",
160
+ "!`nested example`",
161
+ "```",
162
+ "````",
163
+ "",
164
+ "The above examples are documentation only and should not execute.",
165
+ ].join("\n");
166
+
167
+ const result = parseInlineCommandExpansions(fixture);
168
+
169
+ expect(result.expansions).toHaveLength(0);
170
+ expect(result.errors).toHaveLength(0);
171
+ });
172
+
173
+ test("skills docs file itself contains no accidentally live inline expansion tokens", () => {
174
+ // The skills.md documentation describes the `!\`command\`` syntax
175
+ // extensively. Verify that all examples are inside fenced code blocks
176
+ // and the parser does not detect any live or malformed tokens.
177
+ const docsPath = join(process.cwd(), "docs", "skills.md");
178
+ if (!existsSync(docsPath)) {
179
+ // Skip if the docs file doesn't exist (should not happen in normal checkout)
180
+ return;
181
+ }
182
+
183
+ const content = readFileSync(docsPath, "utf-8");
184
+ const result = parseInlineCommandExpansions(content);
185
+
186
+ if (result.errors.length > 0) {
187
+ const message = [
188
+ "assistant/docs/skills.md contains malformed inline command expansion tokens.",
189
+ "Ensure all examples are inside fenced code blocks.",
190
+ "",
191
+ "Errors:",
192
+ ...result.errors.map(
193
+ (e) =>
194
+ ` - ${e.reason} at offset ${e.offset}: ${JSON.stringify(e.raw)}`,
195
+ ),
196
+ ].join("\n");
197
+
198
+ expect(result.errors, message).toEqual([]);
199
+ }
200
+
201
+ // Also verify no tokens are accidentally detected as live expansions
202
+ // outside of fenced code blocks. The docs should only have examples
203
+ // inside fences.
204
+ if (result.expansions.length > 0) {
205
+ const message = [
206
+ "assistant/docs/skills.md has inline expansion tokens outside fenced code blocks.",
207
+ "These would be treated as live commands if the file were loaded as a skill.",
208
+ "Move all examples inside ``` or ~~~ fenced blocks.",
209
+ "",
210
+ "Live tokens found:",
211
+ ...result.expansions.map(
212
+ (e) =>
213
+ " - !" + "`" + e.command + "`" + " at offset " + e.startOffset,
214
+ ),
215
+ ].join("\n");
216
+
217
+ expect(result.expansions, message).toEqual([]);
218
+ }
219
+ });
220
+ });
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Tests for inline-command skill load permission handling.
3
+ *
4
+ * When a skill contains inline command expansions (!\`...\`) and the
5
+ * feature_flags.inline-skill-commands.enabled flag is on, the permission
6
+ * system must:
7
+ *
8
+ * 1. Emit skill_load_dynamic:<id>@<hash> / skill_load_dynamic:<id> candidates
9
+ * instead of skill_load:<id>@<hash> / skill_load:<id>.
10
+ * 2. Match the default ask rule for skill_load_dynamic:* (prompting by default).
11
+ * 3. Allow exact-hash rules to auto-allow pinned versions.
12
+ * 4. Re-prompt when the transitive hash changes (skill edited).
13
+ * 5. Continue matching the existing skill_load:* flow for non-dynamic skills.
14
+ */
15
+
16
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
20
+
21
+ // ── Mock setup (must be before any imports from the project) ──────────────
22
+
23
+ const testDir = mkdtempSync(join(tmpdir(), "inline-skill-perm-test-"));
24
+
25
+ mock.module("../util/platform.js", () => ({
26
+ getRootDir: () => testDir,
27
+ getDataDir: () => join(testDir, "data"),
28
+ getWorkspaceSkillsDir: () => join(testDir, "skills"),
29
+ isMacOS: () => process.platform === "darwin",
30
+ isLinux: () => process.platform === "linux",
31
+ isWindows: () => process.platform === "win32",
32
+ getPidPath: () => join(testDir, "test.pid"),
33
+ getDbPath: () => join(testDir, "test.db"),
34
+ getLogPath: () => join(testDir, "test.log"),
35
+ ensureDataDir: () => {},
36
+ }));
37
+
38
+ mock.module("../util/logger.js", () => ({
39
+ getLogger: () =>
40
+ new Proxy({} as Record<string, unknown>, {
41
+ get: () => () => {},
42
+ }),
43
+ }));
44
+
45
+ interface TestConfig {
46
+ permissions: { mode: "strict" | "workspace" };
47
+ skills: { load: { extraDirs: string[] } };
48
+ sandbox: { enabled: boolean };
49
+ assistantFeatureFlagValues?: Record<string, boolean>;
50
+ [key: string]: unknown;
51
+ }
52
+
53
+ const testConfig: TestConfig = {
54
+ permissions: { mode: "workspace" },
55
+ skills: { load: { extraDirs: [] } },
56
+ sandbox: { enabled: true },
57
+ assistantFeatureFlagValues: {
58
+ "feature_flags.inline-skill-commands.enabled": true,
59
+ },
60
+ };
61
+
62
+ mock.module("../config/loader.js", () => ({
63
+ getConfig: () => testConfig,
64
+ loadConfig: () => testConfig,
65
+ invalidateConfigCache: () => {},
66
+ saveConfig: () => {},
67
+ loadRawConfig: () => ({}),
68
+ saveRawConfig: () => {},
69
+ getNestedValue: () => undefined,
70
+ setNestedValue: () => {},
71
+ }));
72
+
73
+ // ── Imports (after mocks) ─────────────────────────────────────────────────
74
+
75
+ import { check, generateAllowlistOptions } from "../permissions/checker.js";
76
+ import { addRule, clearCache } from "../permissions/trust-store.js";
77
+ import { computeSkillVersionHash } from "../skills/version-hash.js";
78
+
79
+ // ── Helpers ───────────────────────────────────────────────────────────────
80
+
81
+ function ensureSkillsDir(): void {
82
+ mkdirSync(join(testDir, "skills"), { recursive: true });
83
+ }
84
+
85
+ /** Write a plain skill (no inline command expansions). */
86
+ function writePlainSkill(
87
+ skillId: string,
88
+ name: string,
89
+ description = "Test skill",
90
+ ): void {
91
+ const skillDir = join(testDir, "skills", skillId);
92
+ mkdirSync(skillDir, { recursive: true });
93
+ writeFileSync(
94
+ join(skillDir, "SKILL.md"),
95
+ `---\nname: "${name}"\ndescription: "${description}"\n---\n\nPlain skill body.\n`,
96
+ );
97
+ }
98
+
99
+ /** Write a skill with inline command expansions. */
100
+ function writeDynamicSkill(
101
+ skillId: string,
102
+ name: string,
103
+ command = "echo hello",
104
+ description = "Dynamic test skill",
105
+ ): void {
106
+ const skillDir = join(testDir, "skills", skillId);
107
+ mkdirSync(skillDir, { recursive: true });
108
+ writeFileSync(
109
+ join(skillDir, "SKILL.md"),
110
+ `---\nname: "${name}"\ndescription: "${description}"\n---\n\nThis skill uses !\`${command}\` inline.\n`,
111
+ );
112
+ }
113
+
114
+ // ── Tests ─────────────────────────────────────────────────────────────────
115
+
116
+ describe("inline-command skill_load permissions", () => {
117
+ beforeEach(() => {
118
+ clearCache();
119
+ testConfig.permissions = { mode: "workspace" };
120
+ testConfig.skills = { load: { extraDirs: [] } };
121
+ testConfig.assistantFeatureFlagValues = {
122
+ "feature_flags.inline-skill-commands.enabled": true,
123
+ };
124
+ try {
125
+ rmSync(join(testDir, "protected", "trust.json"));
126
+ } catch {
127
+ /* may not exist */
128
+ }
129
+ try {
130
+ rmSync(join(testDir, "skills"), { recursive: true, force: true });
131
+ } catch {
132
+ /* may not exist */
133
+ }
134
+ });
135
+
136
+ // ── Default prompt behavior ──────────────────────────────────────────
137
+
138
+ describe("default prompt behavior", () => {
139
+ test("dynamic skill prompts by default (matches skill_load_dynamic:* ask rule)", async () => {
140
+ ensureSkillsDir();
141
+ writeDynamicSkill("dynamic-prompt", "Dynamic Prompt Skill");
142
+
143
+ const result = await check(
144
+ "skill_load",
145
+ { skill: "dynamic-prompt" },
146
+ "/tmp",
147
+ );
148
+ expect(result.decision).toBe("prompt");
149
+ expect(result.matchedRule).toBeDefined();
150
+ expect(result.matchedRule!.pattern).toBe("skill_load_dynamic:*");
151
+ expect(result.matchedRule!.decision).toBe("ask");
152
+ });
153
+
154
+ test("dynamic skill prompts in strict mode too", async () => {
155
+ ensureSkillsDir();
156
+ writeDynamicSkill("dynamic-strict", "Dynamic Strict Skill");
157
+ testConfig.permissions.mode = "strict";
158
+
159
+ const result = await check(
160
+ "skill_load",
161
+ { skill: "dynamic-strict" },
162
+ "/tmp",
163
+ );
164
+ expect(result.decision).toBe("prompt");
165
+ expect(result.matchedRule).toBeDefined();
166
+ expect(result.matchedRule!.pattern).toBe("skill_load_dynamic:*");
167
+ });
168
+ });
169
+
170
+ // ── Exact-hash allow rules ───────────────────────────────────────────
171
+
172
+ describe("exact-hash allow rules", () => {
173
+ test("exact skill_load_dynamic:<id>@<hash> rule auto-allows", async () => {
174
+ ensureSkillsDir();
175
+ writeDynamicSkill("dynamic-pinned", "Dynamic Pinned Skill");
176
+
177
+ // Compute the transitive hash to create a version-pinned rule.
178
+ const { computeTransitiveSkillVersionHash } =
179
+ await import("../skills/transitive-version-hash.js");
180
+ const { indexCatalogById } = await import("../skills/include-graph.js");
181
+ const { loadSkillCatalog } = await import("../config/skills.js");
182
+
183
+ const catalog = loadSkillCatalog();
184
+ const index = indexCatalogById(catalog);
185
+ const transitiveHash = computeTransitiveSkillVersionHash(
186
+ "dynamic-pinned",
187
+ index,
188
+ );
189
+
190
+ // Add an exact hash rule
191
+ addRule(
192
+ "skill_load",
193
+ `skill_load_dynamic:dynamic-pinned@${transitiveHash}`,
194
+ "everywhere",
195
+ "allow",
196
+ 2000,
197
+ );
198
+
199
+ const result = await check(
200
+ "skill_load",
201
+ { skill: "dynamic-pinned" },
202
+ "/tmp",
203
+ );
204
+ expect(result.decision).toBe("allow");
205
+ expect(result.matchedRule).toBeDefined();
206
+ expect(result.matchedRule!.pattern).toBe(
207
+ `skill_load_dynamic:dynamic-pinned@${transitiveHash}`,
208
+ );
209
+ });
210
+ });
211
+
212
+ // ── Any-version allow rules ──────────────────────────────────────────
213
+
214
+ describe("any-version allow rules", () => {
215
+ test("skill_load_dynamic:<id> rule auto-allows any version", async () => {
216
+ ensureSkillsDir();
217
+ writeDynamicSkill("dynamic-anyver", "Dynamic Any Version Skill");
218
+
219
+ addRule(
220
+ "skill_load",
221
+ "skill_load_dynamic:dynamic-anyver",
222
+ "everywhere",
223
+ "allow",
224
+ 2000,
225
+ );
226
+
227
+ const result = await check(
228
+ "skill_load",
229
+ { skill: "dynamic-anyver" },
230
+ "/tmp",
231
+ );
232
+ expect(result.decision).toBe("allow");
233
+ expect(result.matchedRule).toBeDefined();
234
+ expect(result.matchedRule!.pattern).toBe(
235
+ "skill_load_dynamic:dynamic-anyver",
236
+ );
237
+ });
238
+ });
239
+
240
+ // ── Changed transitive hash re-prompting ─────────────────────────────
241
+
242
+ describe("changed transitive hash re-prompting", () => {
243
+ test("editing a dynamic skill changes the hash, causing version-pinned rule to stop matching", async () => {
244
+ ensureSkillsDir();
245
+ writeDynamicSkill("dynamic-reprompt", "Dynamic Reprompt", "echo v1");
246
+
247
+ const { computeTransitiveSkillVersionHash } =
248
+ await import("../skills/transitive-version-hash.js");
249
+ const { indexCatalogById } = await import("../skills/include-graph.js");
250
+ const { loadSkillCatalog } = await import("../config/skills.js");
251
+
252
+ const catalog1 = loadSkillCatalog();
253
+ const index1 = indexCatalogById(catalog1);
254
+ const hashV1 = computeTransitiveSkillVersionHash(
255
+ "dynamic-reprompt",
256
+ index1,
257
+ );
258
+
259
+ // Add a version-specific rule for v1
260
+ addRule(
261
+ "skill_load",
262
+ `skill_load_dynamic:dynamic-reprompt@${hashV1}`,
263
+ "everywhere",
264
+ "allow",
265
+ 2000,
266
+ );
267
+
268
+ // v1: should auto-allow
269
+ const resultV1 = await check(
270
+ "skill_load",
271
+ { skill: "dynamic-reprompt" },
272
+ "/tmp",
273
+ );
274
+ expect(resultV1.decision).toBe("allow");
275
+ expect(resultV1.matchedRule!.pattern).toBe(
276
+ `skill_load_dynamic:dynamic-reprompt@${hashV1}`,
277
+ );
278
+
279
+ // Edit the skill (change the command)
280
+ writeDynamicSkill("dynamic-reprompt", "Dynamic Reprompt", "echo v2");
281
+
282
+ const catalog2 = loadSkillCatalog();
283
+ const index2 = indexCatalogById(catalog2);
284
+ const hashV2 = computeTransitiveSkillVersionHash(
285
+ "dynamic-reprompt",
286
+ index2,
287
+ );
288
+ expect(hashV2).not.toBe(hashV1);
289
+
290
+ // v2: the version-specific rule no longer matches; falls through
291
+ // to the default skill_load_dynamic:* ask rule
292
+ const resultV2 = await check(
293
+ "skill_load",
294
+ { skill: "dynamic-reprompt" },
295
+ "/tmp",
296
+ );
297
+ expect(resultV2.decision).toBe("prompt");
298
+ expect(resultV2.matchedRule!.pattern).toBe("skill_load_dynamic:*");
299
+ });
300
+ });
301
+
302
+ // ── Non-dynamic skills continue matching skill_load:* ────────────────
303
+
304
+ describe("non-dynamic skills continue to use skill_load:* flow", () => {
305
+ test("plain skill (no inline expansions) matches skill_load:* allow rule", async () => {
306
+ ensureSkillsDir();
307
+ writePlainSkill("plain-skill", "Plain Skill");
308
+
309
+ const result = await check(
310
+ "skill_load",
311
+ { skill: "plain-skill" },
312
+ "/tmp",
313
+ );
314
+ expect(result.decision).toBe("allow");
315
+ expect(result.matchedRule).toBeDefined();
316
+ expect(result.matchedRule!.pattern).toBe("skill_load:*");
317
+ });
318
+
319
+ test("plain skill with version-specific rule still uses skill_load: namespace", async () => {
320
+ ensureSkillsDir();
321
+ writePlainSkill("plain-pinned", "Plain Pinned Skill");
322
+ testConfig.permissions.mode = "strict";
323
+
324
+ const skillDir = join(testDir, "skills", "plain-pinned");
325
+ const diskHash = computeSkillVersionHash(skillDir);
326
+
327
+ addRule(
328
+ "skill_load",
329
+ `skill_load:plain-pinned@${diskHash}`,
330
+ "everywhere",
331
+ "allow",
332
+ 2000,
333
+ );
334
+
335
+ const result = await check(
336
+ "skill_load",
337
+ { skill: "plain-pinned" },
338
+ "/tmp",
339
+ );
340
+ expect(result.decision).toBe("allow");
341
+ expect(result.matchedRule!.pattern).toBe(
342
+ `skill_load:plain-pinned@${diskHash}`,
343
+ );
344
+ });
345
+ });
346
+
347
+ // ── Feature flag disabled ────────────────────────────────────────────
348
+
349
+ describe("feature flag disabled", () => {
350
+ test("dynamic skill falls through to skill_load:* when flag is off", async () => {
351
+ ensureSkillsDir();
352
+ writeDynamicSkill("dynamic-flag-off", "Dynamic Flag Off Skill");
353
+
354
+ // Disable the feature flag
355
+ testConfig.assistantFeatureFlagValues = {
356
+ "feature_flags.inline-skill-commands.enabled": false,
357
+ };
358
+
359
+ const result = await check(
360
+ "skill_load",
361
+ { skill: "dynamic-flag-off" },
362
+ "/tmp",
363
+ );
364
+ // With the flag off, the skill is treated as a normal skill_load
365
+ expect(result.decision).toBe("allow");
366
+ expect(result.matchedRule!.pattern).toBe("skill_load:*");
367
+ });
368
+ });
369
+
370
+ // ── Allowlist options ────────────────────────────────────────────────
371
+
372
+ describe("allowlist options", () => {
373
+ test("dynamic skill allowlist options use skill_load_dynamic: namespace", async () => {
374
+ ensureSkillsDir();
375
+ writeDynamicSkill("dynamic-opts", "Dynamic Opts Skill");
376
+
377
+ const options = await generateAllowlistOptions("skill_load", {
378
+ skill: "dynamic-opts",
379
+ });
380
+
381
+ expect(options.length).toBeGreaterThanOrEqual(1);
382
+ // All options should use skill_load_dynamic: prefix
383
+ for (const option of options) {
384
+ expect(option.pattern).toMatch(/^skill_load_dynamic:/);
385
+ }
386
+
387
+ // Should have an any-version option
388
+ const anyVersionOption = options.find(
389
+ (o) => o.pattern === "skill_load_dynamic:dynamic-opts",
390
+ );
391
+ expect(anyVersionOption).toBeDefined();
392
+ expect(anyVersionOption!.description).toBe("This skill (any version)");
393
+ });
394
+
395
+ test("plain skill allowlist options use skill_load: namespace", async () => {
396
+ ensureSkillsDir();
397
+ writePlainSkill("plain-opts", "Plain Opts Skill");
398
+
399
+ const options = await generateAllowlistOptions("skill_load", {
400
+ skill: "plain-opts",
401
+ });
402
+
403
+ expect(options.length).toBeGreaterThanOrEqual(1);
404
+ // Should use skill_load: prefix, not skill_load_dynamic:
405
+ for (const option of options) {
406
+ expect(option.pattern).toMatch(/^skill_load:/);
407
+ expect(option.pattern).not.toMatch(/^skill_load_dynamic:/);
408
+ }
409
+ });
410
+ });
411
+
412
+ // ── Default rule priority ────────────────────────────────────────────
413
+
414
+ describe("default rule priority", () => {
415
+ test("skill_load_dynamic:* ask rule has higher priority than skill_load:* allow rule", async () => {
416
+ const { getDefaultRuleTemplates } =
417
+ await import("../permissions/defaults.js");
418
+ const rules = getDefaultRuleTemplates();
419
+
420
+ const dynamicRule = rules.find(
421
+ (r) => r.id === "default:ask-skill_load_dynamic-global",
422
+ );
423
+ const loadRule = rules.find(
424
+ (r) => r.id === "default:allow-skill_load-global",
425
+ );
426
+
427
+ expect(dynamicRule).toBeDefined();
428
+ expect(loadRule).toBeDefined();
429
+ expect(dynamicRule!.priority).toBeGreaterThan(loadRule!.priority);
430
+ expect(dynamicRule!.decision).toBe("ask");
431
+ expect(dynamicRule!.pattern).toBe("skill_load_dynamic:*");
432
+ expect(dynamicRule!.tool).toBe("skill_load");
433
+ });
434
+ });
435
+ });