@useatlas/create 0.0.1 → 0.0.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 (298) 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 +11 -11
  7. package/templates/docker/bin/atlas.ts +120 -56
  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 +4 -41
  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 +38 -40
  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 +104 -1
  90. package/templates/docker/src/lib/plugins/registry.ts +14 -8
  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-sdk-compat.test.ts +1 -1
  101. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  102. package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  103. package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  104. package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  105. package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
  106. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  107. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
  108. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  109. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  110. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  111. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
  112. package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
  113. package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
  114. package/templates/docker/src/lib/tools/explore.ts +28 -32
  115. package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
  116. package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
  117. package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
  118. package/templates/docker/src/lib/tools/python.ts +367 -0
  119. package/templates/docker/src/lib/tools/registry.ts +49 -22
  120. package/templates/docker/src/lib/tools/sql.ts +88 -88
  121. package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
  122. package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
  123. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
  124. package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
  125. package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
  126. package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  127. package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
  128. package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
  129. package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
  130. package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
  131. package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
  132. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
  133. package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
  134. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
  135. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
  136. package/templates/docker/src/ui/context.tsx +1 -1
  137. package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
  138. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
  139. package/templates/docker/tsconfig.json +2 -2
  140. package/templates/nextjs-standalone/.env.example +1 -1
  141. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +11 -11
  142. package/templates/nextjs-standalone/bin/atlas.ts +120 -56
  143. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
  144. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
  145. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
  146. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
  147. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
  148. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
  149. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
  150. package/templates/nextjs-standalone/docs/deploy.md +4 -41
  151. package/templates/nextjs-standalone/package.json +11 -2
  152. package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
  153. package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
  154. package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
  155. package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
  156. package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
  157. package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
  158. package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
  159. package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
  160. package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
  161. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
  162. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
  163. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
  164. package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
  165. package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
  166. package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
  167. package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
  168. package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
  169. package/templates/nextjs-standalone/src/api/server.ts +27 -0
  170. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
  171. package/templates/nextjs-standalone/src/app/globals.css +13 -12
  172. package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
  173. package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
  174. package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
  175. package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
  176. package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
  177. package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
  178. package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
  179. package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
  180. package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
  181. package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
  182. package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
  183. package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
  184. package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
  185. package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
  186. package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
  187. package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
  188. package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
  189. package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
  190. package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
  191. package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
  192. package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
  193. package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
  194. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
  195. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
  196. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
  197. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
  198. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
  199. package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
  200. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
  201. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
  202. package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
  203. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
  204. package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
  205. package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
  206. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
  207. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
  208. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
  209. package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
  210. package/templates/nextjs-standalone/src/lib/config.ts +38 -40
  211. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
  212. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
  213. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
  214. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
  215. package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
  216. package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
  217. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
  218. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
  219. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
  220. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
  221. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
  222. package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
  223. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +104 -1
  224. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +14 -8
  225. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
  226. package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
  227. package/templates/nextjs-standalone/src/lib/security.ts +24 -0
  228. package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
  229. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
  230. package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
  231. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
  232. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
  233. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
  234. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +1 -1
  235. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
  236. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
  237. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
  238. package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
  239. package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
  240. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
  241. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
  242. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
  243. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
  244. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
  245. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
  246. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
  247. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
  248. package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
  249. package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
  250. package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
  251. package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
  252. package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
  253. package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
  254. package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
  255. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
  256. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
  257. package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
  258. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
  259. package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
  260. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
  261. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
  262. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
  263. package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
  264. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
  265. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
  266. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
  267. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
  268. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
  269. package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
  270. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
  271. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
  272. package/templates/nextjs-standalone/tsconfig.json +0 -1
  273. package/templates/nextjs-standalone/vercel.json +4 -1
  274. package/templates/docker/render.yaml +0 -34
  275. package/templates/docker/semantic/entities/.gitkeep +0 -0
  276. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  277. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
  278. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
  279. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
  280. package/templates/docker/src/lib/db/duckdb.ts +0 -122
  281. package/templates/docker/src/lib/db/salesforce.ts +0 -342
  282. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  283. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  284. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  285. package/templates/docker/src/lib/tools/salesforce.ts +0 -138
  286. package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
  287. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  288. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  289. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
  290. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
  291. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
  292. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
  293. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
  294. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
  295. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
  296. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
  297. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
  298. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
@@ -0,0 +1,476 @@
1
+ /**
2
+ * Vercel Sandbox backend for the Python execution tool.
3
+ *
4
+ * Uses @vercel/sandbox with runtime: "python3.13" to run Python code
5
+ * in an ephemeral Firecracker microVM. Adapted from the explore-sandbox.ts
6
+ * pattern but with a different lifecycle (lazy creation, package installation):
7
+ * - Creates a Python 3.13 sandbox (initially allow-all for pip install)
8
+ * - Installs data science packages, then locks down to deny-all
9
+ * - Writes wrapper + user code to the sandbox filesystem
10
+ * - Injects data via a JSON file (runCommand does not support stdin piping)
11
+ * - Collects charts and structured output via result marker
12
+ * - Unlike explore-sandbox.ts, the sandbox is created lazily and reused
13
+ * across calls (no explicit close/stop lifecycle — invalidation stops
14
+ * the old sandbox and creates a fresh one on next call)
15
+ *
16
+ * Only loaded when ATLAS_RUNTIME=vercel or running on the Vercel platform.
17
+ */
18
+
19
+ import type { PythonBackend, PythonResult } from "./python";
20
+ import { randomUUID } from "crypto";
21
+ import { createLogger } from "@atlas/api/lib/logger";
22
+ import { SENSITIVE_PATTERNS } from "@atlas/api/lib/security";
23
+
24
+ const log = createLogger("python-sandbox");
25
+
26
+ /** Default Python execution timeout in ms. */
27
+ const DEFAULT_TIMEOUT_MS = 30_000;
28
+
29
+ /** Maximum bytes to read from stdout/stderr (1 MB). */
30
+ const MAX_OUTPUT = 1024 * 1024;
31
+
32
+ /** Packages to install in the sandbox. */
33
+ const DATA_SCIENCE_PACKAGES = [
34
+ "pandas",
35
+ "numpy",
36
+ "matplotlib",
37
+ "scipy",
38
+ "scikit-learn",
39
+ "statsmodels",
40
+ ];
41
+
42
+ /**
43
+ * Python wrapper script — adapted from the nsjail PYTHON_WRAPPER.
44
+ *
45
+ * Key difference: data is injected via a JSON file (argv[2]) instead of
46
+ * stdin, since Vercel Sandbox's runCommand does not support stdin piping.
47
+ *
48
+ * Reads user code from argv[1], executes in a restricted Python namespace,
49
+ * collects charts + structured output via result marker.
50
+ */
51
+ const PYTHON_WRAPPER = `
52
+ import sys, json, io, base64, glob, os, ast
53
+
54
+ _marker = os.environ["ATLAS_RESULT_MARKER"]
55
+ _chart_dir = os.environ.get("ATLAS_CHART_DIR", "/tmp/charts")
56
+
57
+ # --- Import guard ---
58
+ _BLOCKED_MODULES = {
59
+ "subprocess", "os", "socket", "shutil", "sys", "ctypes", "importlib",
60
+ "code", "signal", "multiprocessing", "threading", "pty", "fcntl",
61
+ "termios", "resource", "posixpath",
62
+ "http", "urllib", "requests", "httpx", "aiohttp", "webbrowser",
63
+ "pickle", "tempfile", "pathlib",
64
+ }
65
+ _BLOCKED_BUILTINS = {
66
+ "compile", "exec", "eval", "__import__", "open", "breakpoint",
67
+ "getattr", "globals", "locals", "vars", "dir", "delattr", "setattr",
68
+ }
69
+
70
+ _user_code = open(sys.argv[1]).read()
71
+ try:
72
+ _tree = ast.parse(_user_code)
73
+ except SyntaxError as e:
74
+ print(_marker + json.dumps({"success": False, "error": f"SyntaxError: {e.msg} (line {e.lineno})"}))
75
+ sys.exit(0)
76
+
77
+ _blocked = None
78
+ for _node in ast.walk(_tree):
79
+ if _blocked:
80
+ break
81
+ if isinstance(_node, ast.Import):
82
+ for _alias in _node.names:
83
+ _mod = _alias.name.split('.')[0]
84
+ if _mod in _BLOCKED_MODULES:
85
+ _blocked = f'Blocked import: "{_mod}" is not allowed'
86
+ break
87
+ elif isinstance(_node, ast.ImportFrom):
88
+ if _node.module:
89
+ _mod = _node.module.split('.')[0]
90
+ if _mod in _BLOCKED_MODULES:
91
+ _blocked = f'Blocked import: "{_mod}" is not allowed'
92
+ elif isinstance(_node, ast.Call):
93
+ _name = None
94
+ if isinstance(_node.func, ast.Name):
95
+ _name = _node.func.id
96
+ elif isinstance(_node.func, ast.Attribute):
97
+ _name = _node.func.attr
98
+ if _name and _name in _BLOCKED_BUILTINS:
99
+ _blocked = f'Blocked builtin: "{_name}()" is not allowed'
100
+
101
+ if _blocked:
102
+ print(_marker + json.dumps({"success": False, "error": _blocked}))
103
+ sys.exit(0)
104
+
105
+ # --- Data injection (from file, not stdin) ---
106
+ _atlas_data = None
107
+ if len(sys.argv) > 2:
108
+ _data_file = sys.argv[2]
109
+ if os.path.exists(_data_file):
110
+ with open(_data_file) as f:
111
+ _raw = f.read().strip()
112
+ if _raw:
113
+ _atlas_data = json.loads(_raw)
114
+
115
+ data = None
116
+ df = None
117
+ if _atlas_data:
118
+ try:
119
+ import pandas as pd
120
+ df = pd.DataFrame(_atlas_data["rows"], columns=_atlas_data["columns"])
121
+ data = df
122
+ except ImportError:
123
+ data = _atlas_data
124
+
125
+ # Configure matplotlib for headless rendering
126
+ try:
127
+ import matplotlib
128
+ matplotlib.use('Agg')
129
+ except ImportError:
130
+ pass
131
+
132
+ os.makedirs(_chart_dir, exist_ok=True)
133
+
134
+ def chart_path(n=0):
135
+ return os.path.join(_chart_dir, f"chart_{n}.png")
136
+
137
+ # --- Execute user code ---
138
+ _old_stdout = sys.stdout
139
+ sys.stdout = _captured = io.StringIO()
140
+
141
+ _user_ns = {"chart_path": chart_path, "data": data, "df": df}
142
+ _atlas_error = None
143
+ try:
144
+ exec(_user_code, _user_ns)
145
+ except Exception as e:
146
+ _atlas_error = f"{type(e).__name__}: {e}"
147
+
148
+ _output = _captured.getvalue()
149
+ sys.stdout = _old_stdout
150
+
151
+ # --- Collect results ---
152
+ _charts = []
153
+ for f in sorted(glob.glob(os.path.join(_chart_dir, "chart_*.png"))):
154
+ with open(f, "rb") as fh:
155
+ _charts.append({"base64": base64.b64encode(fh.read()).decode(), "mimeType": "image/png"})
156
+
157
+ _result = {"success": _atlas_error is None}
158
+ if _output.strip():
159
+ _result["output"] = _output.strip()
160
+ if _atlas_error:
161
+ _result["error"] = _atlas_error
162
+
163
+ if "_atlas_table" in _user_ns:
164
+ _result["table"] = _user_ns["_atlas_table"]
165
+
166
+ if "_atlas_chart" in _user_ns:
167
+ _ac = _user_ns["_atlas_chart"]
168
+ if isinstance(_ac, dict):
169
+ _result["rechartsCharts"] = [_ac]
170
+ elif isinstance(_ac, list):
171
+ _result["rechartsCharts"] = _ac
172
+
173
+ if _charts:
174
+ _result["charts"] = _charts
175
+
176
+ print(_marker + json.dumps(_result), file=_old_stdout)
177
+ `;
178
+
179
+ /** Format an error for logging, with extra detail from @vercel/sandbox APIError. */
180
+ function sandboxErrorDetail(err: unknown): string {
181
+ if (!(err instanceof Error)) return String(err);
182
+ const detail = err.message;
183
+ const json = (err as unknown as Record<string, unknown>).json;
184
+ const text = (err as unknown as Record<string, unknown>).text;
185
+ if (json) {
186
+ try {
187
+ return `${detail} — response: ${JSON.stringify(json)}`;
188
+ } catch {
189
+ return `${detail} — response: [unserializable object]`;
190
+ }
191
+ }
192
+ if (typeof text === "string" && text) return `${detail} — body: ${text.slice(0, 500)}`;
193
+ return detail;
194
+ }
195
+
196
+ /** Scrub sensitive data from error messages before exposing. */
197
+ function safeError(detail: string): string {
198
+ return SENSITIVE_PATTERNS.test(detail)
199
+ ? "sandbox API error (details in server logs)"
200
+ : detail;
201
+ }
202
+
203
+ // Sandbox base dir for relative paths
204
+ const SANDBOX_BASE = "/vercel/sandbox";
205
+
206
+ /**
207
+ * Create a Python sandbox backend using @vercel/sandbox.
208
+ *
209
+ * The sandbox is created lazily on first exec() call and reused for
210
+ * subsequent calls. If the sandbox errors, the cached promise is discarded
211
+ * (and the old sandbox stopped) so a fresh one is created on the next call.
212
+ */
213
+ export function createPythonSandboxBackend(): PythonBackend {
214
+ let sandboxPromise: Promise<SandboxInstance> | null = null;
215
+
216
+ interface SandboxInstance {
217
+ sandbox: InstanceType<(typeof import("@vercel/sandbox"))["Sandbox"]>;
218
+ packagesInstalled: boolean;
219
+ }
220
+
221
+ async function getSandbox(): Promise<SandboxInstance> {
222
+ let Sandbox: (typeof import("@vercel/sandbox"))["Sandbox"];
223
+ try {
224
+ ({ Sandbox } = await import("@vercel/sandbox"));
225
+ } catch (err) {
226
+ const detail = err instanceof Error ? err.message : String(err);
227
+ log.error({ err: detail }, "Failed to import @vercel/sandbox");
228
+ throw new Error(
229
+ "Vercel Sandbox runtime selected but @vercel/sandbox is not installed.",
230
+ { cause: err },
231
+ );
232
+ }
233
+
234
+ let sandbox: InstanceType<typeof Sandbox>;
235
+ try {
236
+ // Start with allow-all so pip can reach pypi.org during setup
237
+ sandbox = await Sandbox.create({
238
+ runtime: "python3.13",
239
+ networkPolicy: "allow-all",
240
+ });
241
+ } catch (err) {
242
+ const detail = sandboxErrorDetail(err);
243
+ log.error({ err: detail }, "Python Sandbox.create() failed");
244
+ throw new Error(
245
+ `Failed to create Python Vercel Sandbox: ${safeError(detail)}.`,
246
+ { cause: err },
247
+ );
248
+ }
249
+
250
+ // Install data science packages (requires network access)
251
+ let packagesInstalled = false;
252
+ try {
253
+ const result = await sandbox.runCommand({
254
+ cmd: "pip",
255
+ args: ["install", "--quiet", ...DATA_SCIENCE_PACKAGES],
256
+ sudo: true,
257
+ });
258
+ if (result.exitCode === 0) {
259
+ packagesInstalled = true;
260
+ log.info("Python data science packages installed in sandbox");
261
+ } else {
262
+ const stderr = await result.stderr();
263
+ log.warn(
264
+ { exitCode: result.exitCode, stderr: stderr.slice(0, 500) },
265
+ "pip install returned non-zero — some packages may be unavailable",
266
+ );
267
+ }
268
+ } catch (err) {
269
+ const detail = sandboxErrorDetail(err);
270
+ log.warn({ err: detail }, "pip install failed — continuing without data science packages");
271
+ }
272
+
273
+ // Lock down network before running any user code
274
+ try {
275
+ await sandbox.updateNetworkPolicy("deny-all");
276
+ } catch (err) {
277
+ const detail = sandboxErrorDetail(err);
278
+ log.error({ err: detail }, "Failed to set deny-all network policy");
279
+ try { await sandbox.stop(); } catch (stopErr) { log.warn({ err: stopErr instanceof Error ? stopErr.message : String(stopErr) }, "Failed to stop sandbox after network policy error"); }
280
+ throw new Error(
281
+ `Failed to lock down sandbox network: ${safeError(detail)}.`,
282
+ { cause: err },
283
+ );
284
+ }
285
+
286
+ return { sandbox, packagesInstalled };
287
+ }
288
+
289
+ function invalidate() {
290
+ const old = sandboxPromise;
291
+ sandboxPromise = null;
292
+ if (old) {
293
+ old.then(instance => instance.sandbox.stop()).catch((err) => {
294
+ log.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to stop old Python sandbox during cleanup");
295
+ });
296
+ }
297
+ }
298
+
299
+ return {
300
+ exec: async (code, data): Promise<PythonResult> => {
301
+ if (!sandboxPromise) {
302
+ sandboxPromise = getSandbox();
303
+ }
304
+
305
+ let instance: SandboxInstance;
306
+ try {
307
+ instance = await sandboxPromise;
308
+ } catch (err) {
309
+ invalidate();
310
+ const detail = err instanceof Error ? err.message : String(err);
311
+ return { success: false, error: detail };
312
+ }
313
+
314
+ const { sandbox } = instance;
315
+ const execId = randomUUID();
316
+ const resultMarker = `__ATLAS_RESULT_${execId}__`;
317
+ const execDir = `exec-${execId}`;
318
+ const chartDir = `${execDir}/charts`;
319
+ const wrapperPath = `${execDir}/wrapper.py`;
320
+ const codePath = `${execDir}/user_code.py`;
321
+ const dataPath = `${execDir}/data.json`;
322
+
323
+ try {
324
+ // Create directories
325
+ try {
326
+ await sandbox.mkDir(execDir);
327
+ await sandbox.mkDir(chartDir);
328
+ } catch (err) {
329
+ const detail = sandboxErrorDetail(err);
330
+ log.error({ err: detail, execId }, "Failed to create exec dirs in sandbox");
331
+ invalidate();
332
+ return { success: false, error: `Sandbox infrastructure error: ${safeError(detail)}` };
333
+ }
334
+
335
+ // Write files
336
+ const files: { path: string; content: Buffer }[] = [
337
+ { path: wrapperPath, content: Buffer.from(PYTHON_WRAPPER) },
338
+ { path: codePath, content: Buffer.from(code) },
339
+ ];
340
+ if (data) {
341
+ files.push({ path: dataPath, content: Buffer.from(JSON.stringify(data)) });
342
+ }
343
+
344
+ try {
345
+ await sandbox.writeFiles(files);
346
+ } catch (err) {
347
+ const detail = sandboxErrorDetail(err);
348
+ log.error({ err: detail, execId }, "Failed to write Python files to sandbox");
349
+ invalidate();
350
+ return { success: false, error: `Sandbox infrastructure error: ${safeError(detail)}` };
351
+ }
352
+
353
+ // Build command args
354
+ const pythonArgs = [
355
+ `${SANDBOX_BASE}/${wrapperPath}`,
356
+ `${SANDBOX_BASE}/${codePath}`,
357
+ ];
358
+ if (data) {
359
+ pythonArgs.push(`${SANDBOX_BASE}/${dataPath}`);
360
+ }
361
+
362
+ // Execute with timeout enforcement
363
+ const timeout = parseInt(
364
+ process.env.ATLAS_PYTHON_TIMEOUT ?? String(DEFAULT_TIMEOUT_MS),
365
+ 10,
366
+ ) || DEFAULT_TIMEOUT_MS;
367
+
368
+ let result;
369
+ try {
370
+ const commandPromise = sandbox.runCommand({
371
+ cmd: "python3",
372
+ args: pythonArgs,
373
+ cwd: `${SANDBOX_BASE}/${execDir}`,
374
+ env: {
375
+ ATLAS_RESULT_MARKER: resultMarker,
376
+ ATLAS_CHART_DIR: `${SANDBOX_BASE}/${chartDir}`,
377
+ MPLBACKEND: "Agg",
378
+ HOME: "/tmp",
379
+ LANG: "C.UTF-8",
380
+ },
381
+ });
382
+ const timeoutPromise = new Promise<never>((_, reject) =>
383
+ setTimeout(() => reject(new Error(`Python execution timed out after ${timeout}ms`)), timeout),
384
+ );
385
+ result = await Promise.race([commandPromise, timeoutPromise]);
386
+ } catch (err) {
387
+ const detail = err instanceof Error ? err.message : String(err);
388
+ if (detail.includes("timed out")) {
389
+ log.warn({ execId, timeout }, "Python sandbox execution timed out");
390
+ return { success: false, error: detail };
391
+ }
392
+ const fullDetail = sandboxErrorDetail(err);
393
+ log.error({ err: fullDetail, execId }, "Sandbox runCommand failed for Python");
394
+ invalidate();
395
+ return {
396
+ success: false,
397
+ error: `Sandbox infrastructure error: ${safeError(fullDetail)}. Will retry with a fresh sandbox.`,
398
+ };
399
+ }
400
+
401
+ let stdout: string;
402
+ let stderr: string;
403
+ try {
404
+ [stdout, stderr] = await Promise.all([
405
+ result.stdout(),
406
+ result.stderr(),
407
+ ]);
408
+ } catch (err) {
409
+ const detail = sandboxErrorDetail(err);
410
+ log.error({ err: detail, execId }, "Failed to read stdout/stderr from sandbox");
411
+ invalidate();
412
+ return { success: false, error: `Failed to read execution output: ${safeError(detail)}` };
413
+ }
414
+
415
+ // Output size guard (matches nsjail's 1 MB limit)
416
+ if (stdout.length > MAX_OUTPUT) {
417
+ return {
418
+ success: false,
419
+ error: "Python output exceeded 1 MB limit — reduce print() output or use _atlas_table for large results.",
420
+ };
421
+ }
422
+
423
+ log.debug(
424
+ { execId, exitCode: result.exitCode, stdoutLen: stdout.length },
425
+ "python sandbox execution finished",
426
+ );
427
+
428
+ // Extract structured result from the last marker line
429
+ const lines = stdout.split("\n");
430
+ const resultLine = lines.findLast((l) => l.startsWith(resultMarker));
431
+
432
+ if (resultLine) {
433
+ try {
434
+ return JSON.parse(resultLine.slice(resultMarker.length)) as PythonResult;
435
+ } catch (parseErr) {
436
+ log.warn(
437
+ { execId, resultLine: resultLine.slice(0, 500), parseError: String(parseErr) },
438
+ "failed to parse Python result JSON",
439
+ );
440
+ const userOutput = stdout.split(resultMarker)[0].trim();
441
+ return {
442
+ success: false,
443
+ error: `Python produced unparseable output.${userOutput ? ` Output: ${userOutput.slice(0, 500)}` : ""} stderr: ${stderr.trim().slice(0, 500)}`,
444
+ };
445
+ }
446
+ }
447
+
448
+ // No structured result — process errored before the wrapper could emit one
449
+ if (result.exitCode > 128) {
450
+ const signal = result.exitCode - 128;
451
+ const signalNames: Record<number, string> = {
452
+ 6: "SIGABRT", 9: "SIGKILL", 11: "SIGSEGV", 15: "SIGTERM",
453
+ };
454
+ const name = signalNames[signal] ?? `signal ${signal}`;
455
+ if (signal === 9) {
456
+ return { success: false, error: "Python execution killed (likely exceeded time or memory limit)" };
457
+ }
458
+ return {
459
+ success: false,
460
+ error: `Python process terminated by ${name}${stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : ""}`,
461
+ };
462
+ }
463
+
464
+ return {
465
+ success: false,
466
+ error: stderr.trim() || `Python execution failed (exit code ${result.exitCode})`,
467
+ };
468
+ } catch (err) {
469
+ const detail = err instanceof Error ? err.message : String(err);
470
+ log.error({ err: detail, execId }, "Unexpected error in Python sandbox execution");
471
+ invalidate();
472
+ return { success: false, error: detail };
473
+ }
474
+ },
475
+ };
476
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Sidecar backend for the Python execution tool.
3
+ *
4
+ * Calls the sandbox sidecar's POST /exec-python endpoint to run Python code
5
+ * in an isolated container with no secrets and no host access. The sidecar
6
+ * handles data injection, chart collection, and structured output.
7
+ *
8
+ * Configured via ATLAS_SANDBOX_URL (same as the explore sidecar).
9
+ */
10
+
11
+ import type { SidecarPythonRequest } from "@atlas/api/lib/sidecar-types";
12
+ import type { PythonResult } from "./python";
13
+ import { createLogger } from "@atlas/api/lib/logger";
14
+
15
+ const log = createLogger("python-sidecar");
16
+
17
+ /** Default timeout for Python execution (ms). */
18
+ const DEFAULT_TIMEOUT_MS = 30_000;
19
+
20
+ /** HTTP-level timeout — longer than the execution timeout to allow for overhead. */
21
+ const HTTP_OVERHEAD_MS = 10_000;
22
+
23
+ /** Shorthand for building error results. */
24
+ function pythonError(error: string): PythonResult & { success: false } {
25
+ return { success: false, error };
26
+ }
27
+
28
+ export async function executePythonViaSidecar(
29
+ sidecarUrl: string,
30
+ code: string,
31
+ data?: { columns: string[]; rows: unknown[][] },
32
+ ): Promise<PythonResult> {
33
+ const authToken = process.env.SIDECAR_AUTH_TOKEN;
34
+
35
+ let baseUrl: URL;
36
+ try {
37
+ baseUrl = new URL(sidecarUrl);
38
+ } catch {
39
+ return pythonError(
40
+ `Invalid ATLAS_SANDBOX_URL: "${sidecarUrl}". Expected a valid URL (e.g. http://sandbox-sidecar:8080).`,
41
+ );
42
+ }
43
+
44
+ const execUrl = new URL("/exec-python", baseUrl).toString();
45
+ const rawTimeout = parseInt(process.env.ATLAS_PYTHON_TIMEOUT ?? String(DEFAULT_TIMEOUT_MS), 10);
46
+ if (Number.isNaN(rawTimeout)) {
47
+ log.warn({ value: process.env.ATLAS_PYTHON_TIMEOUT }, "Invalid ATLAS_PYTHON_TIMEOUT, using default");
48
+ }
49
+ const timeout = Number.isNaN(rawTimeout) ? DEFAULT_TIMEOUT_MS : rawTimeout;
50
+
51
+ const requestBody: SidecarPythonRequest = { code, timeout };
52
+ if (data) {
53
+ requestBody.data = data;
54
+ }
55
+
56
+ let response: Response;
57
+ try {
58
+ response = await fetch(execUrl, {
59
+ method: "POST",
60
+ headers: {
61
+ "Content-Type": "application/json",
62
+ ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
63
+ },
64
+ body: JSON.stringify(requestBody),
65
+ signal: AbortSignal.timeout(timeout + HTTP_OVERHEAD_MS),
66
+ });
67
+ } catch (err) {
68
+ const detail = err instanceof Error ? err.message : String(err);
69
+
70
+ if (
71
+ detail.includes("ECONNREFUSED") ||
72
+ detail.includes("fetch failed") ||
73
+ detail.includes("Failed to connect")
74
+ ) {
75
+ log.error({ err: detail, url: execUrl }, "Sidecar connection failed for Python execution");
76
+ return pythonError(
77
+ `Python sidecar unreachable at ${baseUrl.origin}: ${detail}. Check that the sandbox-sidecar service is running.`,
78
+ );
79
+ }
80
+
81
+ if (detail.includes("TimeoutError") || detail.includes("timed out") || detail.includes("aborted")) {
82
+ log.warn({ timeout }, "Python sidecar request timed out");
83
+ return pythonError(`Python execution timed out after ${timeout}ms`);
84
+ }
85
+
86
+ log.error({ err: detail }, "Python sidecar request failed");
87
+ return pythonError(`Sidecar request failed: ${detail}`);
88
+ }
89
+
90
+ if (!response.ok) {
91
+ let errorBody: string;
92
+ try {
93
+ errorBody = await response.text();
94
+ } catch {
95
+ errorBody = `HTTP ${response.status}`;
96
+ }
97
+
98
+ log.error(
99
+ {
100
+ status: response.status,
101
+ contentLength: response.headers.get("content-length"),
102
+ body: errorBody.slice(0, 500),
103
+ },
104
+ "Python sidecar returned HTTP error",
105
+ );
106
+
107
+ // Try to parse as structured error (500 with PythonResult shape)
108
+ if (response.status === 500) {
109
+ try {
110
+ const parsed = JSON.parse(errorBody) as PythonResult;
111
+ if (typeof parsed.success === "boolean") {
112
+ return parsed;
113
+ }
114
+ } catch {
115
+ // Not structured — fall through
116
+ }
117
+ }
118
+
119
+ return pythonError(
120
+ `Python sidecar error (HTTP ${response.status}): ${errorBody.slice(0, 500)}`,
121
+ );
122
+ }
123
+
124
+ let parsed: unknown;
125
+ try {
126
+ parsed = await response.json();
127
+ } catch (err) {
128
+ const detail = err instanceof Error ? err.message : String(err);
129
+ log.error(
130
+ {
131
+ err: detail,
132
+ status: response.status,
133
+ contentLength: response.headers.get("content-length"),
134
+ },
135
+ "Failed to parse Python sidecar response",
136
+ );
137
+ return pythonError(`Failed to parse sidecar response: ${detail}`);
138
+ }
139
+
140
+ const result = parsed as PythonResult;
141
+ if (typeof result.success !== "boolean") {
142
+ log.error(
143
+ { body: JSON.stringify(parsed).slice(0, 500) },
144
+ "Python sidecar returned unexpected response shape",
145
+ );
146
+ return pythonError("Sidecar returned an unexpected response format.");
147
+ }
148
+
149
+ return result;
150
+ }