@useatlas/create 0.0.2 → 0.0.4

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 (296) hide show
  1. package/README.md +4 -18
  2. package/index.ts +191 -31
  3. package/package.json +1 -1
  4. package/templates/docker/.env.example +3 -3
  5. package/templates/docker/Dockerfile.sidecar +28 -0
  6. package/templates/docker/bin/__tests__/plugin-cli.test.ts +9 -9
  7. package/templates/docker/bin/atlas.ts +108 -44
  8. package/templates/docker/data/demo-semantic/catalog.yml +51 -27
  9. package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
  10. package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
  11. package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
  12. package/templates/docker/data/demo-semantic/glossary.yml +104 -8
  13. package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
  14. package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
  15. package/templates/docker/docker-compose.yml +1 -1
  16. package/templates/docker/docs/deploy.md +2 -39
  17. package/templates/docker/package.json +17 -1
  18. package/templates/docker/semantic/catalog.yml +62 -3
  19. package/templates/docker/semantic/entities/accounts.yml +162 -0
  20. package/templates/docker/semantic/entities/companies.yml +143 -0
  21. package/templates/docker/semantic/entities/people.yml +132 -0
  22. package/templates/docker/semantic/glossary.yml +116 -4
  23. package/templates/docker/semantic/metrics/accounts.yml +77 -0
  24. package/templates/docker/semantic/metrics/companies.yml +63 -0
  25. package/templates/docker/sidecar/Dockerfile +5 -6
  26. package/templates/docker/sidecar/railway.json +1 -2
  27. package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
  28. package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
  29. package/templates/docker/src/api/__tests__/health.test.ts +30 -8
  30. package/templates/docker/src/api/routes/admin.ts +549 -8
  31. package/templates/docker/src/api/routes/chat.ts +5 -20
  32. package/templates/docker/src/api/routes/health.ts +39 -27
  33. package/templates/docker/src/api/routes/openapi.ts +1329 -74
  34. package/templates/docker/src/api/routes/query.ts +2 -1
  35. package/templates/docker/src/api/server.ts +27 -0
  36. package/templates/docker/src/app/api/[...route]/route.ts +2 -2
  37. package/templates/docker/src/app/globals.css +13 -12
  38. package/templates/docker/src/app/layout.tsx +9 -2
  39. package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
  40. package/templates/docker/src/components/ui/badge.tsx +48 -0
  41. package/templates/docker/src/components/ui/button.tsx +64 -0
  42. package/templates/docker/src/components/ui/card.tsx +92 -0
  43. package/templates/docker/src/components/ui/collapsible.tsx +33 -0
  44. package/templates/docker/src/components/ui/command.tsx +184 -0
  45. package/templates/docker/src/components/ui/dialog.tsx +158 -0
  46. package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
  47. package/templates/docker/src/components/ui/input.tsx +21 -0
  48. package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
  49. package/templates/docker/src/components/ui/select.tsx +190 -0
  50. package/templates/docker/src/components/ui/separator.tsx +28 -0
  51. package/templates/docker/src/components/ui/sheet.tsx +143 -0
  52. package/templates/docker/src/components/ui/sidebar.tsx +726 -0
  53. package/templates/docker/src/components/ui/skeleton.tsx +13 -0
  54. package/templates/docker/src/components/ui/table.tsx +116 -0
  55. package/templates/docker/src/components/ui/tabs.tsx +91 -0
  56. package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
  57. package/templates/docker/src/components/ui/toggle.tsx +47 -0
  58. package/templates/docker/src/components/ui/tooltip.tsx +57 -0
  59. package/templates/docker/src/hooks/use-mobile.ts +19 -0
  60. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
  61. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
  62. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  63. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
  64. package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
  65. package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  66. package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
  67. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
  68. package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
  69. package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
  70. package/templates/docker/src/lib/agent-query.ts +5 -23
  71. package/templates/docker/src/lib/agent.ts +32 -112
  72. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
  73. package/templates/docker/src/lib/auth/middleware.ts +30 -4
  74. package/templates/docker/src/lib/auth/migrate.ts +97 -0
  75. package/templates/docker/src/lib/auth/server.ts +12 -1
  76. package/templates/docker/src/lib/config.ts +37 -39
  77. package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
  78. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
  79. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  80. package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
  81. package/templates/docker/src/lib/db/connection.ts +87 -265
  82. package/templates/docker/src/lib/db/internal.ts +6 -1
  83. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  84. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  85. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  86. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
  87. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  88. package/templates/docker/src/lib/plugins/index.ts +4 -1
  89. package/templates/docker/src/lib/plugins/migrate.ts +103 -0
  90. package/templates/docker/src/lib/plugins/registry.ts +12 -6
  91. package/templates/docker/src/lib/plugins/wiring.ts +113 -4
  92. package/templates/docker/src/lib/providers.ts +6 -1
  93. package/templates/docker/src/lib/security.ts +24 -0
  94. package/templates/docker/src/lib/semantic.ts +2 -0
  95. package/templates/docker/src/lib/sidecar-types.ts +12 -1
  96. package/templates/docker/src/lib/startup.ts +71 -101
  97. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  98. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  99. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  100. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  101. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  102. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  103. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  104. package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
  105. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  106. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
  107. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  108. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  109. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  110. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
  111. package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
  112. package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
  113. package/templates/docker/src/lib/tools/explore.ts +28 -32
  114. package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
  115. package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
  116. package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
  117. package/templates/docker/src/lib/tools/python.ts +367 -0
  118. package/templates/docker/src/lib/tools/registry.ts +49 -22
  119. package/templates/docker/src/lib/tools/sql.ts +88 -88
  120. package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
  121. package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
  122. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
  123. package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
  124. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
  125. package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  126. package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
  127. package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
  128. package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
  129. package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
  130. package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
  131. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
  132. package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
  133. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
  134. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
  135. package/templates/docker/src/ui/context.tsx +1 -1
  136. package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
  137. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
  138. package/templates/docker/tsconfig.json +2 -2
  139. package/templates/nextjs-standalone/.env.example +1 -1
  140. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +9 -9
  141. package/templates/nextjs-standalone/bin/atlas.ts +108 -44
  142. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
  143. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
  144. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
  145. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
  146. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
  147. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
  148. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
  149. package/templates/nextjs-standalone/docs/deploy.md +2 -39
  150. package/templates/nextjs-standalone/package.json +11 -2
  151. package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
  152. package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
  153. package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
  154. package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
  155. package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
  156. package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
  157. package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
  158. package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
  159. package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
  160. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
  161. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
  162. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
  163. package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
  164. package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
  165. package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
  166. package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
  167. package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
  168. package/templates/nextjs-standalone/src/api/server.ts +27 -0
  169. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
  170. package/templates/nextjs-standalone/src/app/globals.css +13 -12
  171. package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
  172. package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
  173. package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
  174. package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
  175. package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
  176. package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
  177. package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
  178. package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
  179. package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
  180. package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
  181. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
  182. package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
  183. package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
  184. package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
  185. package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
  186. package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
  187. package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
  188. package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
  189. package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
  190. package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
  191. package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
  192. package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
  193. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
  194. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
  195. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  196. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
  197. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
  198. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  199. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
  200. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
  201. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
  202. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
  203. package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
  204. package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
  205. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
  206. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
  207. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
  208. package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
  209. package/templates/nextjs-standalone/src/lib/config.ts +37 -39
  210. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
  211. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
  212. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  213. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
  214. package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
  215. package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
  216. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  217. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  218. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  219. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
  220. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  221. package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
  222. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +103 -0
  223. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +12 -6
  224. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
  225. package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
  226. package/templates/nextjs-standalone/src/lib/security.ts +24 -0
  227. package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
  228. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
  229. package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
  230. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  231. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  232. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  233. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  234. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  235. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  236. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  237. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
  238. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  239. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
  240. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  241. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  242. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  243. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
  244. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
  245. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
  246. package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
  247. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
  248. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
  249. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
  250. package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
  251. package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
  252. package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
  253. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
  254. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
  255. package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
  256. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
  257. package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  258. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
  259. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
  260. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
  261. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
  262. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
  263. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
  264. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
  265. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
  266. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
  267. package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
  268. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
  269. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
  270. package/templates/nextjs-standalone/tsconfig.json +0 -1
  271. package/templates/nextjs-standalone/vercel.json +4 -1
  272. package/templates/docker/render.yaml +0 -34
  273. package/templates/docker/semantic/entities/.gitkeep +0 -0
  274. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  275. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
  276. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
  277. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
  278. package/templates/docker/src/lib/db/duckdb.ts +0 -122
  279. package/templates/docker/src/lib/db/salesforce.ts +0 -342
  280. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  281. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  282. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  283. package/templates/docker/src/lib/tools/salesforce.ts +0 -138
  284. package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
  285. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  286. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  287. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
  288. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
  289. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
  290. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
  291. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
  292. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  293. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  294. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  295. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
  296. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
@@ -1,7 +1,8 @@
1
1
  import { describe, test, expect, beforeEach, mock } from "bun:test";
2
2
  import { PluginRegistry } from "../registry";
3
3
  import type { PluginLike, PluginContextLike } from "../registry";
4
- import { wireDatasourcePlugins, wireActionPlugins, wireInteractionPlugins, wireContextPlugins } from "../wiring";
4
+ import { wireDatasourcePlugins, wireActionPlugins, wireInteractionPlugins, wireContextPlugins, wireSandboxPlugins } from "../wiring";
5
+ import type { SandboxExecBackend } from "../wiring";
5
6
 
6
7
  const minimalCtx: PluginContextLike = {
7
8
  db: null,
@@ -15,9 +16,9 @@ const minimalCtx: PluginContextLike = {
15
16
 
16
17
  function makeMockConnectionRegistry() {
17
18
  return {
18
- registered: [] as { id: string; conn: unknown; dbType: string; description?: string; validate?: unknown }[],
19
- async registerDirect(id: string, conn: unknown, dbType: string, description?: string, validate?: unknown) {
20
- this.registered.push({ id, conn, dbType, description, validate });
19
+ registered: [] as { id: string; conn: unknown; dbType: string; description?: string; validate?: unknown; meta?: unknown }[],
20
+ registerDirect(id: string, conn: unknown, dbType: string, description?: string, validate?: unknown, meta?: unknown) {
21
+ this.registered.push({ id, conn, dbType, description, validate, meta });
21
22
  },
22
23
  };
23
24
  }
@@ -49,7 +50,7 @@ function makeDatasourcePlugin(
49
50
  };
50
51
  return {
51
52
  id,
52
- type: "datasource",
53
+ types: ["datasource"],
53
54
  version: "1.0.0",
54
55
  connection: {
55
56
  create: () => conn,
@@ -66,7 +67,7 @@ function makeDatasourcePlugin(
66
67
  function makeActionPlugin(id: string, opts?: { unhealthy?: boolean }): PluginLike {
67
68
  return {
68
69
  id,
69
- type: "action",
70
+ types: ["action"],
70
71
  version: "1.0.0",
71
72
  actions: [
72
73
  {
@@ -88,7 +89,7 @@ function makeActionPlugin(id: string, opts?: { unhealthy?: boolean }): PluginLik
88
89
  function makeInteractionPlugin(id: string, routesFn: (app: unknown) => void, opts?: { unhealthy?: boolean }): PluginLike {
89
90
  return {
90
91
  id,
91
- type: "interaction",
92
+ types: ["interaction"],
92
93
  version: "1.0.0",
93
94
  routes: routesFn,
94
95
  ...(opts?.unhealthy
@@ -127,7 +128,7 @@ describe("wireDatasourcePlugins", () => {
127
128
  const validator = (q: string) => ({ valid: /^SELECT/i.test(q) });
128
129
  const plugin: PluginLike = {
129
130
  id: "validated-ds",
130
- type: "datasource",
131
+ types: ["datasource"],
131
132
  version: "1.0.0",
132
133
  connection: {
133
134
  create: () => ({ query: async () => ({ columns: [], rows: [] }), close: async () => {} }),
@@ -147,6 +148,47 @@ describe("wireDatasourcePlugins", () => {
147
148
  expect(connRegistry.registered[0].validate).toBe(validator);
148
149
  });
149
150
 
151
+ test("passes parserDialect and forbiddenPatterns through meta", async () => {
152
+ const patterns = [/^\s*(KILL)\b/i];
153
+ const plugin: PluginLike = {
154
+ id: "meta-ds",
155
+ types: ["datasource"],
156
+ version: "1.0.0",
157
+ connection: {
158
+ create: () => ({ query: async () => ({ columns: [], rows: [] }), close: async () => {} }),
159
+ dbType: "clickhouse",
160
+ parserDialect: "PostgresQL",
161
+ forbiddenPatterns: patterns,
162
+ },
163
+ };
164
+ registry.register(plugin);
165
+ await registry.initializeAll(minimalCtx);
166
+
167
+ await wireDatasourcePlugins(
168
+ registry,
169
+ connRegistry as unknown as import("@atlas/api/lib/db/connection").ConnectionRegistry,
170
+ );
171
+
172
+ expect(connRegistry.registered).toHaveLength(1);
173
+ expect(connRegistry.registered[0].meta).toEqual({
174
+ parserDialect: "PostgresQL",
175
+ forbiddenPatterns: patterns,
176
+ });
177
+ });
178
+
179
+ test("passes undefined meta when no parserDialect or forbiddenPatterns", async () => {
180
+ registry.register(makeDatasourcePlugin("plain-meta"));
181
+ await registry.initializeAll(minimalCtx);
182
+
183
+ await wireDatasourcePlugins(
184
+ registry,
185
+ connRegistry as unknown as import("@atlas/api/lib/db/connection").ConnectionRegistry,
186
+ );
187
+
188
+ expect(connRegistry.registered).toHaveLength(1);
189
+ expect(connRegistry.registered[0].meta).toBeUndefined();
190
+ });
191
+
150
192
  test("passes undefined validate when not provided", async () => {
151
193
  registry.register(makeDatasourcePlugin("plain-ds"));
152
194
  await registry.initializeAll(minimalCtx);
@@ -179,7 +221,7 @@ describe("wireDatasourcePlugins", () => {
179
221
  test("continues when one create() throws and returns failures", async () => {
180
222
  const failingPlugin: PluginLike = {
181
223
  id: "failing-ds",
182
- type: "datasource",
224
+ types: ["datasource"],
183
225
  version: "1.0.0",
184
226
  connection: {
185
227
  create: () => { throw new Error("conn failed"); },
@@ -389,7 +431,7 @@ describe("wireActionPlugins", () => {
389
431
  test("registers all actions from a multi-action plugin", async () => {
390
432
  const plugin: PluginLike = {
391
433
  id: "multi-action",
392
- type: "action",
434
+ types: ["action"],
393
435
  version: "1.0.0",
394
436
  actions: [
395
437
  { name: "action-a", description: "A", tool: {}, actionType: "t", reversible: false, defaultApproval: "manual", requiredCredentials: [] },
@@ -421,7 +463,7 @@ describe("wireActionPlugins", () => {
421
463
 
422
464
  const plugin: PluginLike = {
423
465
  id: "fail-action",
424
- type: "action",
466
+ types: ["action"],
425
467
  version: "1.0.0",
426
468
  actions: [
427
469
  { name: "fail-action", description: "Fails", tool: {}, actionType: "t", reversible: false, defaultApproval: "manual", requiredCredentials: [] },
@@ -485,7 +527,7 @@ describe("wireInteractionPlugins", () => {
485
527
  const fakeApp = { route: mock(() => {}) };
486
528
  const routelessPlugin: PluginLike = {
487
529
  id: "stdio-interaction",
488
- type: "interaction",
530
+ types: ["interaction"],
489
531
  version: "1.0.0",
490
532
  // No routes property — like MCP stdio transport
491
533
  };
@@ -506,7 +548,7 @@ describe("wireInteractionPlugins", () => {
506
548
  function makeContextPlugin(id: string, loadFn: () => Promise<string>, opts?: { unhealthy?: boolean }): PluginLike {
507
549
  return {
508
550
  id,
509
- type: "context",
551
+ types: ["context"],
510
552
  version: "1.0.0",
511
553
  contextProvider: { load: loadFn },
512
554
  ...(opts?.unhealthy
@@ -571,7 +613,7 @@ describe("wireContextPlugins", () => {
571
613
  test("skips context plugins without contextProvider", async () => {
572
614
  const noProvider: PluginLike = {
573
615
  id: "no-provider",
574
- type: "context",
616
+ types: ["context"],
575
617
  version: "1.0.0",
576
618
  };
577
619
  registry.register(noProvider);
@@ -583,3 +625,175 @@ describe("wireContextPlugins", () => {
583
625
  expect(result.failed).toEqual([]);
584
626
  });
585
627
  });
628
+
629
+ // --- wireSandboxPlugins ---
630
+
631
+ function makeMockBackend(tag: string): SandboxExecBackend {
632
+ return {
633
+ exec: async (command: string) => ({
634
+ stdout: `[${tag}] ${command}`,
635
+ stderr: "",
636
+ exitCode: 0,
637
+ }),
638
+ };
639
+ }
640
+
641
+ function makeSandboxPlugin(
642
+ id: string,
643
+ opts?: { priority?: number; createFn?: (root: string) => Promise<SandboxExecBackend> | SandboxExecBackend; unhealthy?: boolean },
644
+ ): PluginLike {
645
+ return {
646
+ id,
647
+ types: ["sandbox"],
648
+ version: "1.0.0",
649
+ sandbox: {
650
+ create: opts?.createFn ?? (async () => makeMockBackend(id)),
651
+ ...(opts?.priority !== undefined ? { priority: opts.priority } : {}),
652
+ },
653
+ ...(opts?.unhealthy
654
+ ? { initialize: async () => { throw new Error("fail"); } }
655
+ : {}),
656
+ };
657
+ }
658
+
659
+ describe("wireSandboxPlugins", () => {
660
+ let registry: PluginRegistry;
661
+
662
+ beforeEach(() => {
663
+ registry = new PluginRegistry();
664
+ });
665
+
666
+ test("returns backend from a single sandbox plugin", async () => {
667
+ registry.register(makeSandboxPlugin("my-sandbox"));
668
+ await registry.initializeAll(minimalCtx);
669
+
670
+ const result = await wireSandboxPlugins(registry, "/semantic");
671
+
672
+ expect(result.backend).not.toBeNull();
673
+ expect(result.pluginId).toBe("my-sandbox");
674
+ expect(result.failed).toEqual([]);
675
+ });
676
+
677
+ test("selects highest-priority plugin", async () => {
678
+ registry.register(makeSandboxPlugin("low", { priority: 40 }));
679
+ registry.register(makeSandboxPlugin("high", { priority: 90 }));
680
+ await registry.initializeAll(minimalCtx);
681
+
682
+ const result = await wireSandboxPlugins(registry, "/semantic");
683
+
684
+ expect(result.pluginId).toBe("high");
685
+ const output = await result.backend!.exec("ls");
686
+ expect(output.stdout).toContain("[high]");
687
+ });
688
+
689
+ test("uses default priority (60) when omitted", async () => {
690
+ registry.register(makeSandboxPlugin("explicit-low", { priority: 50 }));
691
+ registry.register(makeSandboxPlugin("default-priority")); // no priority → 60
692
+ await registry.initializeAll(minimalCtx);
693
+
694
+ const result = await wireSandboxPlugins(registry, "/semantic");
695
+
696
+ expect(result.pluginId).toBe("default-priority");
697
+ });
698
+
699
+ test("falls through to next plugin when create() throws", async () => {
700
+ registry.register(makeSandboxPlugin("broken", {
701
+ priority: 90,
702
+ createFn: async () => { throw new Error("init failed"); },
703
+ }));
704
+ registry.register(makeSandboxPlugin("working", { priority: 50 }));
705
+ await registry.initializeAll(minimalCtx);
706
+
707
+ const result = await wireSandboxPlugins(registry, "/semantic");
708
+
709
+ expect(result.pluginId).toBe("working");
710
+ expect(result.failed).toHaveLength(1);
711
+ expect(result.failed[0].pluginId).toBe("broken");
712
+ expect(result.failed[0].error).toBe("init failed");
713
+ });
714
+
715
+ test("returns null when all plugins fail", async () => {
716
+ registry.register(makeSandboxPlugin("broken-1", {
717
+ createFn: async () => { throw new Error("fail 1"); },
718
+ }));
719
+ registry.register(makeSandboxPlugin("broken-2", {
720
+ createFn: async () => { throw new Error("fail 2"); },
721
+ }));
722
+ await registry.initializeAll(minimalCtx);
723
+
724
+ const result = await wireSandboxPlugins(registry, "/semantic");
725
+
726
+ expect(result.backend).toBeNull();
727
+ expect(result.pluginId).toBeNull();
728
+ expect(result.failed).toHaveLength(2);
729
+ });
730
+
731
+ test("returns null when no sandbox plugins registered", async () => {
732
+ await registry.initializeAll(minimalCtx);
733
+
734
+ const result = await wireSandboxPlugins(registry, "/semantic");
735
+
736
+ expect(result.backend).toBeNull();
737
+ expect(result.pluginId).toBeNull();
738
+ expect(result.failed).toEqual([]);
739
+ });
740
+
741
+ test("skips unhealthy sandbox plugins", async () => {
742
+ registry.register(makeSandboxPlugin("healthy"));
743
+ registry.register(makeSandboxPlugin("unhealthy", { unhealthy: true }));
744
+ await registry.initializeAll(minimalCtx);
745
+
746
+ const result = await wireSandboxPlugins(registry, "/semantic");
747
+
748
+ expect(result.pluginId).toBe("healthy");
749
+ expect(result.failed).toEqual([]);
750
+ });
751
+
752
+ test("skips sandbox plugins missing sandbox.create()", async () => {
753
+ const noCreate: PluginLike = {
754
+ id: "no-create",
755
+ types: ["sandbox"],
756
+ version: "1.0.0",
757
+ // No sandbox property
758
+ };
759
+ registry.register(noCreate);
760
+ await registry.initializeAll(minimalCtx);
761
+
762
+ const result = await wireSandboxPlugins(registry, "/semantic");
763
+
764
+ expect(result.backend).toBeNull();
765
+ expect(result.pluginId).toBeNull();
766
+ expect(result.failed).toEqual([]);
767
+ });
768
+
769
+ test("passes semanticRoot to sandbox.create()", async () => {
770
+ let receivedRoot: string | undefined;
771
+ registry.register(makeSandboxPlugin("root-check", {
772
+ createFn: async (root: string) => {
773
+ receivedRoot = root;
774
+ return makeMockBackend("root-check");
775
+ },
776
+ }));
777
+ await registry.initializeAll(minimalCtx);
778
+
779
+ await wireSandboxPlugins(registry, "/my/custom/semantic");
780
+
781
+ expect(receivedRoot).toBe("/my/custom/semantic");
782
+ });
783
+
784
+ test("rejects backend missing exec method from create()", async () => {
785
+ registry.register(makeSandboxPlugin("bad-backend", {
786
+ createFn: async () => ({} as SandboxExecBackend),
787
+ }));
788
+ registry.register(makeSandboxPlugin("good-backend", { priority: 40 }));
789
+ await registry.initializeAll(minimalCtx);
790
+
791
+ const result = await wireSandboxPlugins(registry, "/semantic");
792
+
793
+ // bad-backend has default priority (60) > good-backend (40), tried first but rejected
794
+ expect(result.pluginId).toBe("good-backend");
795
+ expect(result.failed).toHaveLength(1);
796
+ expect(result.failed[0].pluginId).toBe("bad-backend");
797
+ expect(result.failed[0].error).toContain("missing exec method");
798
+ });
799
+ });
@@ -4,6 +4,9 @@
4
4
 
5
5
  export { plugins, PluginRegistry } from "./registry";
6
6
  export type { PluginLike, PluginContextLike, PluginHealthResult, PluginType, PluginStatus, PluginDescription } from "./registry";
7
- export { wireDatasourcePlugins, wireActionPlugins, wireInteractionPlugins, wireContextPlugins } from "./wiring";
7
+ export { wireDatasourcePlugins, wireActionPlugins, wireInteractionPlugins, wireContextPlugins, wireSandboxPlugins } from "./wiring";
8
+ export type { SandboxExecBackend } from "./wiring";
9
+ export { generateMigrationSQL, generateColumnMigrations, applyMigrations, runPluginMigrations, ensureMigrationsTable, getAppliedMigrations, diffSchema, prefixTableName } from "./migrate";
10
+ export type { MigrateDB, MigrationStatement, SchemaDiff } from "./migrate";
8
11
  export { dispatchHook } from "./hooks";
9
12
  export { getPluginTools, setPluginTools, getContextFragments, setContextFragments } from "./tools";
@@ -273,6 +273,109 @@ export async function applyMigrations(
273
273
  return { applied, skipped };
274
274
  }
275
275
 
276
+ // ---------------------------------------------------------------------------
277
+ // Column migrations (ALTER TABLE ADD COLUMN)
278
+ // ---------------------------------------------------------------------------
279
+
280
+ /**
281
+ * Query the actual columns for a given table from information_schema.
282
+ * Returns a Set of lowercase column names.
283
+ */
284
+ async function getExistingColumns(db: MigrateDB, tableName: string): Promise<Set<string>> {
285
+ const result = await db.query(
286
+ "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1",
287
+ [tableName],
288
+ );
289
+ return new Set(result.rows.map((r) => String(r.column_name).toLowerCase()));
290
+ }
291
+
292
+ /**
293
+ * Generate ALTER TABLE ADD COLUMN IF NOT EXISTS statements for fields
294
+ * that exist in the plugin schema but not in the actual database table.
295
+ *
296
+ * Column detection queries `information_schema.columns` in the `public`
297
+ * schema. Only handles column additions — column removal and type
298
+ * changes are not supported (require manual migration).
299
+ */
300
+ export async function generateColumnMigrations(
301
+ db: MigrateDB,
302
+ plugins: PluginWithSchema[],
303
+ ): Promise<MigrationStatement[]> {
304
+ const statements: MigrationStatement[] = [];
305
+
306
+ for (const plugin of plugins) {
307
+ if (!plugin.schema) continue;
308
+
309
+ for (const [tableName, tableDef] of Object.entries(plugin.schema)) {
310
+ const prefixed = prefixTableName(plugin.id, tableName);
311
+
312
+ // Check if table exists at all — skip if it doesn't (CREATE TABLE will handle it)
313
+ const existing = await getExistingColumns(db, prefixed);
314
+ if (existing.size === 0) {
315
+ log.debug({ table: prefixed, pluginId: plugin.id }, "Table not in information_schema — skipping column migration");
316
+ continue;
317
+ }
318
+
319
+ for (const [fieldName, fieldDef] of Object.entries(tableDef.fields)) {
320
+ if (existing.has(fieldName.toLowerCase())) continue;
321
+
322
+ let colDef: string;
323
+ try {
324
+ colDef = fieldToSQL(fieldName, fieldDef);
325
+ } catch (err) {
326
+ throw new Error(
327
+ `Plugin "${plugin.id}", table "${prefixed}": ${err instanceof Error ? err.message : String(err)}`,
328
+ { cause: err },
329
+ );
330
+ }
331
+ const sql = `ALTER TABLE "${prefixed}" ADD COLUMN IF NOT EXISTS ${colDef};`;
332
+
333
+ statements.push({
334
+ pluginId: plugin.id,
335
+ tableName,
336
+ prefixedName: prefixed,
337
+ sql,
338
+ hash: hashSQL(sql),
339
+ });
340
+ }
341
+ }
342
+ }
343
+
344
+ return statements;
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // High-level orchestrator
349
+ // ---------------------------------------------------------------------------
350
+
351
+ /**
352
+ * Run all plugin schema migrations: CREATE TABLE for new tables, then
353
+ * ALTER TABLE ADD COLUMN for new fields on existing tables.
354
+ *
355
+ * Idempotent — safe to call on every startup.
356
+ */
357
+ export async function runPluginMigrations(
358
+ db: MigrateDB,
359
+ plugins: PluginWithSchema[],
360
+ ): Promise<{ applied: string[]; skipped: string[] }> {
361
+ // Phase 1: CREATE TABLE
362
+ const createStatements = generateMigrationSQL(plugins);
363
+ const createResult = await applyMigrations(db, createStatements);
364
+
365
+ // Phase 2: ALTER TABLE ADD COLUMN (for tables that already existed or were just created)
366
+ const columnStatements = await generateColumnMigrations(db, plugins);
367
+ if (columnStatements.length === 0) {
368
+ return createResult;
369
+ }
370
+
371
+ const columnResult = await applyMigrations(db, columnStatements);
372
+
373
+ return {
374
+ applied: [...createResult.applied, ...columnResult.applied],
375
+ skipped: [...createResult.skipped, ...columnResult.skipped],
376
+ };
377
+ }
378
+
276
379
  // ---------------------------------------------------------------------------
277
380
  // Diff: compare declared schema vs actual tables
278
381
  // ---------------------------------------------------------------------------
@@ -41,7 +41,8 @@ export interface PluginContextLike {
41
41
  */
42
42
  export interface PluginLike {
43
43
  readonly id: string;
44
- readonly type: PluginType;
44
+ /** Plugin type(s). A plugin can implement multiple types. */
45
+ readonly types: readonly PluginType[];
45
46
  readonly version: string;
46
47
  readonly name?: string;
47
48
  initialize?(ctx: PluginContextLike): Promise<void>;
@@ -59,7 +60,7 @@ interface PluginEntry {
59
60
 
60
61
  export interface PluginDescription {
61
62
  id: string;
62
- type: PluginType;
63
+ types: readonly PluginType[];
63
64
  version: string;
64
65
  name: string;
65
66
  status: PluginStatus;
@@ -83,7 +84,7 @@ export class PluginRegistry {
83
84
  }
84
85
  this.idSet.add(plugin.id);
85
86
  this.entries.push({ plugin, status: "registered" });
86
- log.info({ pluginId: plugin.id, type: plugin.type }, "Plugin registered");
87
+ log.info({ pluginId: plugin.id, types: plugin.types }, "Plugin registered");
87
88
  }
88
89
 
89
90
  /**
@@ -190,13 +191,18 @@ export class PluginRegistry {
190
191
  return this.entries.find((e) => e.plugin.id === id)?.status;
191
192
  }
192
193
 
193
- /** Return plugins of the given type that are currently healthy. */
194
+ /** Return plugins whose types array includes the given type and are currently healthy. */
194
195
  getByType(type: PluginType): PluginLike[] {
195
196
  return this.entries
196
- .filter((e) => e.plugin.type === type && e.status === "healthy")
197
+ .filter((e) => e.plugin.types.includes(type) && e.status === "healthy")
197
198
  .map((e) => e.plugin);
198
199
  }
199
200
 
201
+ /** Return all registered plugins regardless of status (for schema migrations at boot). */
202
+ getAll(): PluginLike[] {
203
+ return this.entries.map((e) => e.plugin);
204
+ }
205
+
200
206
  /** Return all healthy plugins regardless of type (for cross-cutting hooks). */
201
207
  getAllHealthy(): PluginLike[] {
202
208
  return this.entries
@@ -208,7 +214,7 @@ export class PluginRegistry {
208
214
  describe(): PluginDescription[] {
209
215
  return this.entries.map((e) => ({
210
216
  id: e.plugin.id,
211
- type: e.plugin.type,
217
+ types: e.plugin.types,
212
218
  version: e.plugin.version,
213
219
  name: e.plugin.name ?? e.plugin.id,
214
220
  status: e.status,
@@ -28,6 +28,8 @@ interface DatasourceShape {
28
28
  create(): Promise<{ query(sql: string, timeoutMs?: number): Promise<unknown>; close(): Promise<void> }> | { query(sql: string, timeoutMs?: number): Promise<unknown>; close(): Promise<void> };
29
29
  dbType: string;
30
30
  validate?(query: string): { valid: boolean; reason?: string };
31
+ parserDialect?: string;
32
+ forbiddenPatterns?: RegExp[];
31
33
  };
32
34
  entities?: unknown[] | (() => Promise<unknown[]> | unknown[]);
33
35
  dialect?: string;
@@ -51,7 +53,7 @@ interface InteractionShape {
51
53
 
52
54
  function hasContextProvider(p: PluginLike): p is PluginLike & ContextShape {
53
55
  return (
54
- p.type === "context" &&
56
+ p.types.includes("context") &&
55
57
  typeof (p as Record<string, unknown>).contextProvider === "object" &&
56
58
  (p as Record<string, unknown>).contextProvider !== null &&
57
59
  typeof ((p as Record<string, unknown>).contextProvider as Record<string, unknown>)?.load === "function"
@@ -60,18 +62,18 @@ function hasContextProvider(p: PluginLike): p is PluginLike & ContextShape {
60
62
 
61
63
  function hasDatasource(p: PluginLike): p is PluginLike & DatasourceShape {
62
64
  return (
63
- p.type === "datasource" &&
65
+ p.types.includes("datasource") &&
64
66
  typeof (p as Record<string, unknown>).connection === "object" &&
65
67
  (p as Record<string, unknown>).connection !== null
66
68
  );
67
69
  }
68
70
 
69
71
  function hasActions(p: PluginLike): p is PluginLike & ActionShape {
70
- return p.type === "action" && Array.isArray((p as Record<string, unknown>).actions);
72
+ return p.types.includes("action") && Array.isArray((p as Record<string, unknown>).actions);
71
73
  }
72
74
 
73
75
  function hasRoutes(p: PluginLike): p is PluginLike & InteractionShape {
74
- return p.type === "interaction" && typeof (p as Record<string, unknown>).routes === "function";
76
+ return p.types.includes("interaction") && typeof (p as Record<string, unknown>).routes === "function";
75
77
  }
76
78
 
77
79
  // ---------------------------------------------------------------------------
@@ -106,12 +108,16 @@ export async function wireDatasourcePlugins(
106
108
  }
107
109
  try {
108
110
  const conn = await plugin.connection.create();
111
+ const meta = (plugin.connection.parserDialect || plugin.connection.forbiddenPatterns)
112
+ ? { parserDialect: plugin.connection.parserDialect, forbiddenPatterns: plugin.connection.forbiddenPatterns }
113
+ : undefined;
109
114
  await connRegistry.registerDirect(
110
115
  plugin.id,
111
116
  conn as Parameters<ConnectionRegistry["registerDirect"]>[1],
112
117
  plugin.connection.dbType as Parameters<ConnectionRegistry["registerDirect"]>[2],
113
118
  plugin.name ?? plugin.id,
114
119
  plugin.connection.validate,
120
+ meta,
115
121
  );
116
122
  wired.push(plugin.id);
117
123
  log.info({ pluginId: plugin.id, dbType: plugin.connection.dbType }, "Datasource plugin wired");
@@ -254,6 +260,109 @@ export async function wireInteractionPlugins(
254
260
  return { wired, failed };
255
261
  }
256
262
 
263
+ // ---------------------------------------------------------------------------
264
+ // Sandbox plugins
265
+ // ---------------------------------------------------------------------------
266
+
267
+ /**
268
+ * Duplicated from @useatlas/plugin-sdk/types to avoid runtime SDK dependency.
269
+ * Keep in sync — explore-sdk-compat.test.ts verifies structural equivalence.
270
+ */
271
+ const SANDBOX_DEFAULT_PRIORITY = 60;
272
+
273
+ /**
274
+ * Minimal interface for a sandbox execution backend.
275
+ * Structurally identical to ExploreBackend in explore.ts — duplicated here
276
+ * to avoid a circular dependency (explore.ts imports from wiring.ts).
277
+ * Changes to either interface should be mirrored.
278
+ */
279
+ export interface SandboxExecBackend {
280
+ exec(command: string): Promise<{ stdout: string; stderr: string; exitCode: number }>;
281
+ close?(): Promise<void>;
282
+ }
283
+
284
+ interface SandboxShape {
285
+ sandbox: {
286
+ create(root: string): Promise<SandboxExecBackend> | SandboxExecBackend;
287
+ priority?: number;
288
+ };
289
+ }
290
+
291
+ function hasSandbox(p: PluginLike): p is PluginLike & SandboxShape {
292
+ return (
293
+ p.types.includes("sandbox") &&
294
+ typeof (p as Record<string, unknown>).sandbox === "object" &&
295
+ (p as Record<string, unknown>).sandbox !== null &&
296
+ typeof ((p as Record<string, unknown>).sandbox as Record<string, unknown>)?.create === "function"
297
+ );
298
+ }
299
+
300
+ /**
301
+ * Discover sandbox plugins, sort by priority (highest first), and try to
302
+ * create a backend from each until one succeeds.
303
+ *
304
+ * Unlike other wire functions, sandbox plugins are not registered into a
305
+ * global registry — the caller receives a single backend instance directly.
306
+ *
307
+ * NOTE: In practice, called lazily from getExploreBackend() on the first
308
+ * explore command (not at startup), because the explore backend is cached
309
+ * as a singleton and depends on runtime environment detection.
310
+ */
311
+ export async function wireSandboxPlugins(
312
+ pluginRegistry: PluginRegistry,
313
+ semanticRoot: string,
314
+ ): Promise<{
315
+ backend: SandboxExecBackend | null;
316
+ pluginId: string | null;
317
+ failed: Array<{ pluginId: string; error: string }>;
318
+ }> {
319
+ const sandboxPlugins = pluginRegistry.getByType("sandbox");
320
+ const failed: Array<{ pluginId: string; error: string }> = [];
321
+
322
+ if (sandboxPlugins.length === 0) {
323
+ return { backend: null, pluginId: null, failed };
324
+ }
325
+
326
+ // Filter to valid sandbox plugins, sort by priority descending
327
+ const valid = sandboxPlugins.filter(hasSandbox);
328
+ if (valid.length === 0) {
329
+ for (const sp of sandboxPlugins) {
330
+ log.warn({ pluginId: sp.id }, "Sandbox plugin missing sandbox.create() — skipped");
331
+ }
332
+ return { backend: null, pluginId: null, failed };
333
+ }
334
+
335
+ const sorted = [...valid].sort((a, b) => {
336
+ const pa = a.sandbox.priority ?? SANDBOX_DEFAULT_PRIORITY;
337
+ const pb = b.sandbox.priority ?? SANDBOX_DEFAULT_PRIORITY;
338
+ return pb - pa;
339
+ });
340
+
341
+ for (const sp of sorted) {
342
+ try {
343
+ const backend = await sp.sandbox.create(semanticRoot);
344
+ if (!backend || typeof backend.exec !== "function") {
345
+ const msg = "create() returned invalid backend (missing exec method)";
346
+ failed.push({ pluginId: sp.id, error: msg });
347
+ log.error({ pluginId: sp.id }, msg);
348
+ continue;
349
+ }
350
+ log.info({ pluginId: sp.id }, "Using sandbox plugin for explore backend");
351
+ return { backend, pluginId: sp.id, failed };
352
+ } catch (err) {
353
+ const msg = err instanceof Error ? err.message : String(err);
354
+ failed.push({ pluginId: sp.id, error: msg });
355
+ log.error(
356
+ { pluginId: sp.id, err: err instanceof Error ? err : new Error(String(err)) },
357
+ "Sandbox plugin create() failed, trying next",
358
+ );
359
+ }
360
+ }
361
+
362
+ log.error({ count: sorted.length }, "All sandbox plugins failed to create a backend");
363
+ return { backend: null, pluginId: null, failed };
364
+ }
365
+
257
366
  /**
258
367
  * For each healthy context plugin, call `contextProvider.load()` and collect
259
368
  * the returned text fragments. Fragments are injected into the agent system