@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.
- package/README.md +4 -18
- package/index.ts +191 -31
- package/package.json +1 -1
- package/templates/docker/.env.example +3 -3
- package/templates/docker/Dockerfile.sidecar +28 -0
- package/templates/docker/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/docker/bin/atlas.ts +108 -44
- package/templates/docker/data/demo-semantic/catalog.yml +51 -27
- package/templates/docker/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/docker/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/docker/data/demo-semantic/entities/people.yml +82 -95
- package/templates/docker/data/demo-semantic/glossary.yml +104 -8
- package/templates/docker/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/docker/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/docker/docker-compose.yml +1 -1
- package/templates/docker/docs/deploy.md +2 -39
- package/templates/docker/package.json +17 -1
- package/templates/docker/semantic/catalog.yml +62 -3
- package/templates/docker/semantic/entities/accounts.yml +162 -0
- package/templates/docker/semantic/entities/companies.yml +143 -0
- package/templates/docker/semantic/entities/people.yml +132 -0
- package/templates/docker/semantic/glossary.yml +116 -4
- package/templates/docker/semantic/metrics/accounts.yml +77 -0
- package/templates/docker/semantic/metrics/companies.yml +63 -0
- package/templates/docker/sidecar/Dockerfile +5 -6
- package/templates/docker/sidecar/railway.json +1 -2
- package/templates/docker/src/api/__tests__/admin.test.ts +7 -7
- package/templates/docker/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/docker/src/api/__tests__/health.test.ts +30 -8
- package/templates/docker/src/api/routes/admin.ts +549 -8
- package/templates/docker/src/api/routes/chat.ts +5 -20
- package/templates/docker/src/api/routes/health.ts +39 -27
- package/templates/docker/src/api/routes/openapi.ts +1329 -74
- package/templates/docker/src/api/routes/query.ts +2 -1
- package/templates/docker/src/api/server.ts +27 -0
- package/templates/docker/src/app/api/[...route]/route.ts +2 -2
- package/templates/docker/src/app/globals.css +13 -12
- package/templates/docker/src/app/layout.tsx +9 -2
- package/templates/docker/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/docker/src/components/ui/badge.tsx +48 -0
- package/templates/docker/src/components/ui/button.tsx +64 -0
- package/templates/docker/src/components/ui/card.tsx +92 -0
- package/templates/docker/src/components/ui/collapsible.tsx +33 -0
- package/templates/docker/src/components/ui/command.tsx +184 -0
- package/templates/docker/src/components/ui/dialog.tsx +158 -0
- package/templates/docker/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/docker/src/components/ui/input.tsx +21 -0
- package/templates/docker/src/components/ui/scroll-area.tsx +58 -0
- package/templates/docker/src/components/ui/select.tsx +190 -0
- package/templates/docker/src/components/ui/separator.tsx +28 -0
- package/templates/docker/src/components/ui/sheet.tsx +143 -0
- package/templates/docker/src/components/ui/sidebar.tsx +726 -0
- package/templates/docker/src/components/ui/skeleton.tsx +13 -0
- package/templates/docker/src/components/ui/table.tsx +116 -0
- package/templates/docker/src/components/ui/tabs.tsx +91 -0
- package/templates/docker/src/components/ui/toggle-group.tsx +83 -0
- package/templates/docker/src/components/ui/toggle.tsx +47 -0
- package/templates/docker/src/components/ui/tooltip.tsx +57 -0
- package/templates/docker/src/hooks/use-mobile.ts +19 -0
- package/templates/docker/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/docker/src/lib/__tests__/config.test.ts +69 -19
- package/templates/docker/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/docker/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/docker/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/docker/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/docker/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/docker/src/lib/agent-query.ts +5 -23
- package/templates/docker/src/lib/agent.ts +32 -112
- package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/docker/src/lib/auth/middleware.ts +30 -4
- package/templates/docker/src/lib/auth/migrate.ts +97 -0
- package/templates/docker/src/lib/auth/server.ts +12 -1
- package/templates/docker/src/lib/config.ts +37 -39
- package/templates/docker/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/docker/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/docker/src/lib/db/connection.ts +87 -265
- package/templates/docker/src/lib/db/internal.ts +6 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/docker/src/lib/plugins/index.ts +4 -1
- package/templates/docker/src/lib/plugins/migrate.ts +103 -0
- package/templates/docker/src/lib/plugins/registry.ts +12 -6
- package/templates/docker/src/lib/plugins/wiring.ts +113 -4
- package/templates/docker/src/lib/providers.ts +6 -1
- package/templates/docker/src/lib/security.ts +24 -0
- package/templates/docker/src/lib/semantic.ts +2 -0
- package/templates/docker/src/lib/sidecar-types.ts +12 -1
- package/templates/docker/src/lib/startup.ts +71 -101
- package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/docker/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/docker/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/docker/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/docker/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/docker/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/docker/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/docker/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/docker/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/docker/src/lib/tools/explore.ts +28 -32
- package/templates/docker/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/docker/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/docker/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/docker/src/lib/tools/python.ts +367 -0
- package/templates/docker/src/lib/tools/registry.ts +49 -22
- package/templates/docker/src/lib/tools/sql.ts +88 -88
- package/templates/docker/src/types/vercel-sandbox.d.ts +7 -0
- package/templates/docker/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/docker/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/docker/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/docker/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/docker/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/docker/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/docker/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/docker/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/docker/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/docker/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/docker/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/docker/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/docker/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/docker/src/ui/context.tsx +1 -1
- package/templates/docker/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/docker/tsconfig.json +2 -2
- package/templates/nextjs-standalone/.env.example +1 -1
- package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +9 -9
- package/templates/nextjs-standalone/bin/atlas.ts +108 -44
- package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +51 -27
- package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +95 -103
- package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +88 -152
- package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +82 -95
- package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +104 -8
- package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +62 -23
- package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +52 -78
- package/templates/nextjs-standalone/docs/deploy.md +2 -39
- package/templates/nextjs-standalone/package.json +11 -2
- package/templates/nextjs-standalone/scripts/migrate-auth.ts +25 -0
- package/templates/nextjs-standalone/scripts/seed-demo.ts +94 -0
- package/templates/nextjs-standalone/semantic/catalog.yml +62 -3
- package/templates/nextjs-standalone/semantic/entities/accounts.yml +162 -0
- package/templates/nextjs-standalone/semantic/entities/companies.yml +143 -0
- package/templates/nextjs-standalone/semantic/entities/people.yml +132 -0
- package/templates/nextjs-standalone/semantic/glossary.yml +116 -4
- package/templates/nextjs-standalone/semantic/metrics/accounts.yml +77 -0
- package/templates/nextjs-standalone/semantic/metrics/companies.yml +63 -0
- package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +7 -7
- package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +7 -0
- package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +30 -8
- package/templates/nextjs-standalone/src/api/routes/admin.ts +549 -8
- package/templates/nextjs-standalone/src/api/routes/chat.ts +5 -20
- package/templates/nextjs-standalone/src/api/routes/health.ts +39 -27
- package/templates/nextjs-standalone/src/api/routes/openapi.ts +1329 -74
- package/templates/nextjs-standalone/src/api/routes/query.ts +2 -1
- package/templates/nextjs-standalone/src/api/server.ts +27 -0
- package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +2 -2
- package/templates/nextjs-standalone/src/app/globals.css +13 -12
- package/templates/nextjs-standalone/src/app/layout.tsx +9 -2
- package/templates/nextjs-standalone/src/components/ui/alert-dialog.tsx +196 -0
- package/templates/nextjs-standalone/src/components/ui/badge.tsx +48 -0
- package/templates/nextjs-standalone/src/components/ui/button.tsx +64 -0
- package/templates/nextjs-standalone/src/components/ui/card.tsx +92 -0
- package/templates/nextjs-standalone/src/components/ui/collapsible.tsx +33 -0
- package/templates/nextjs-standalone/src/components/ui/command.tsx +184 -0
- package/templates/nextjs-standalone/src/components/ui/dialog.tsx +158 -0
- package/templates/nextjs-standalone/src/components/ui/dropdown-menu.tsx +257 -0
- package/templates/nextjs-standalone/src/components/ui/input.tsx +21 -0
- package/templates/nextjs-standalone/src/components/ui/scroll-area.tsx +58 -0
- package/templates/nextjs-standalone/src/components/ui/select.tsx +190 -0
- package/templates/nextjs-standalone/src/components/ui/separator.tsx +28 -0
- package/templates/nextjs-standalone/src/components/ui/sheet.tsx +143 -0
- package/templates/nextjs-standalone/src/components/ui/sidebar.tsx +726 -0
- package/templates/nextjs-standalone/src/components/ui/skeleton.tsx +13 -0
- package/templates/nextjs-standalone/src/components/ui/table.tsx +116 -0
- package/templates/nextjs-standalone/src/components/ui/tabs.tsx +91 -0
- package/templates/nextjs-standalone/src/components/ui/toggle-group.tsx +83 -0
- package/templates/nextjs-standalone/src/components/ui/toggle.tsx +47 -0
- package/templates/nextjs-standalone/src/components/ui/tooltip.tsx +57 -0
- package/templates/nextjs-standalone/src/hooks/use-mobile.ts +19 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +17 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +69 -19
- package/templates/nextjs-standalone/src/lib/__tests__/plugin-aware-validation.test.ts +321 -0
- package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +32 -1
- package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +9 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup-first-run.test.ts +429 -0
- package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +5 -0
- package/templates/nextjs-standalone/src/lib/agent-query.ts +5 -23
- package/templates/nextjs-standalone/src/lib/agent.ts +32 -112
- package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/auth/middleware.ts +30 -4
- package/templates/nextjs-standalone/src/lib/auth/migrate.ts +97 -0
- package/templates/nextjs-standalone/src/lib/auth/server.ts +12 -1
- package/templates/nextjs-standalone/src/lib/config.ts +37 -39
- package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +89 -14
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +1 -18
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +0 -19
- package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +11 -208
- package/templates/nextjs-standalone/src/lib/db/connection.ts +87 -265
- package/templates/nextjs-standalone/src/lib/db/internal.ts +6 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +3 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +2 -2
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +355 -1
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +32 -5
- package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +228 -14
- package/templates/nextjs-standalone/src/lib/plugins/index.ts +4 -1
- package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +103 -0
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +12 -6
- package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +113 -4
- package/templates/nextjs-standalone/src/lib/providers.ts +6 -1
- package/templates/nextjs-standalone/src/lib/security.ts +24 -0
- package/templates/nextjs-standalone/src/lib/semantic.ts +2 -0
- package/templates/nextjs-standalone/src/lib/sidecar-types.ts +12 -1
- package/templates/nextjs-standalone/src/lib/startup.ts +71 -101
- package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +32 -18
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +14 -14
- package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +5 -3
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-nsjail.test.ts +515 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sandbox.test.ts +397 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python-sidecar.test.ts +365 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/python.test.ts +331 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +1 -13
- package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +38 -31
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +2 -0
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +5 -308
- package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +17 -12
- package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +25 -0
- package/templates/nextjs-standalone/src/lib/tools/explore.ts +28 -32
- package/templates/nextjs-standalone/src/lib/tools/python-nsjail.ts +396 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sandbox.ts +476 -0
- package/templates/nextjs-standalone/src/lib/tools/python-sidecar.ts +150 -0
- package/templates/nextjs-standalone/src/lib/tools/python.ts +367 -0
- package/templates/nextjs-standalone/src/lib/tools/registry.ts +49 -22
- package/templates/nextjs-standalone/src/lib/tools/sql.ts +88 -88
- package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +77 -8
- package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +25 -17
- package/templates/nextjs-standalone/src/ui/components/admin/change-password-dialog.tsx +128 -0
- package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +3 -3
- package/templates/nextjs-standalone/src/ui/components/admin/semantic-file-tree.tsx +159 -0
- package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +64 -12
- package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +25 -15
- package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +88 -42
- package/templates/nextjs-standalone/src/ui/components/chat/python-result-card.tsx +244 -0
- package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +39 -15
- package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +6 -1
- package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +12 -3
- package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +5 -2
- package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +25 -20
- package/templates/nextjs-standalone/src/ui/context.tsx +1 -1
- package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +3 -3
- package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -10
- package/templates/nextjs-standalone/tsconfig.json +0 -1
- package/templates/nextjs-standalone/vercel.json +4 -1
- package/templates/docker/render.yaml +0 -34
- package/templates/docker/semantic/entities/.gitkeep +0 -0
- package/templates/docker/semantic/metrics/.gitkeep +0 -0
- package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/docker/src/lib/db/duckdb.ts +0 -122
- package/templates/docker/src/lib/db/salesforce.ts +0 -342
- package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/docker/src/lib/tools/salesforce.ts +0 -138
- package/templates/docker/src/lib/tools/soql-validation.ts +0 -172
- package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
- package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
- package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +0 -141
- package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +0 -339
- package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +0 -217
- package/templates/nextjs-standalone/src/lib/db/duckdb.ts +0 -122
- package/templates/nextjs-standalone/src/lib/db/salesforce.ts +0 -342
- package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +0 -154
- package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +0 -303
- package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +0 -233
- package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +0 -138
- package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +0 -172
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - sandbox plugin: pluggable explore backend via the Plugin SDK (priority-sorted)
|
|
7
7
|
* - @vercel/sandbox: ephemeral microVM with networkPolicy "deny-all" (Vercel)
|
|
8
8
|
* - nsjail: Linux namespace sandbox (self-hosted Docker)
|
|
9
|
-
* - sidecar: HTTP-isolated container with no secrets (Railway
|
|
9
|
+
* - sidecar: HTTP-isolated container with no secrets (Railway)
|
|
10
10
|
* - just-bash: OverlayFs ensures read-only access; writes stay in memory (dev, or production fallback)
|
|
11
11
|
*
|
|
12
12
|
* Runtime selection priority: sandbox plugin > Vercel sandbox > nsjail (explicit) > sidecar > nsjail (auto-detect) > just-bash.
|
|
@@ -19,9 +19,6 @@ import * as path from "path";
|
|
|
19
19
|
import { createLogger } from "@atlas/api/lib/logger";
|
|
20
20
|
import { withSpan } from "@atlas/api/lib/tracing";
|
|
21
21
|
|
|
22
|
-
/** Must match SANDBOX_DEFAULT_PRIORITY in @useatlas/plugin-sdk/types. */
|
|
23
|
-
const SANDBOX_DEFAULT_PRIORITY = 60;
|
|
24
|
-
|
|
25
22
|
const log = createLogger("explore");
|
|
26
23
|
|
|
27
24
|
const SEMANTIC_ROOT = path.resolve(process.cwd(), "semantic");
|
|
@@ -108,7 +105,8 @@ function useNsjail(): boolean {
|
|
|
108
105
|
_nsjailAvailable = isNsjailAvailable();
|
|
109
106
|
} catch (err) {
|
|
110
107
|
if (
|
|
111
|
-
err
|
|
108
|
+
err != null &&
|
|
109
|
+
typeof err === "object" &&
|
|
112
110
|
"code" in err &&
|
|
113
111
|
(err as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND"
|
|
114
112
|
) {
|
|
@@ -156,7 +154,7 @@ export function getExploreBackendType(): ExploreBackendType {
|
|
|
156
154
|
if (useVercelSandbox()) return "vercel-sandbox";
|
|
157
155
|
// Explicit nsjail (ATLAS_SANDBOX=nsjail) — hard-fail if unavailable
|
|
158
156
|
if (process.env.ATLAS_SANDBOX === "nsjail" && !_nsjailFailed) return "nsjail";
|
|
159
|
-
// Sidecar takes priority over nsjail auto-detection (Railway
|
|
157
|
+
// Sidecar takes priority over nsjail auto-detection (Railway sets ATLAS_SANDBOX_URL)
|
|
160
158
|
if (useSidecar() && !_sidecarFailed) return "sidecar";
|
|
161
159
|
// nsjail auto-detect (binary on PATH)
|
|
162
160
|
if (!_nsjailFailed && useNsjail()) return "nsjail";
|
|
@@ -190,35 +188,29 @@ function getExploreBackend(): Promise<ExploreBackend> {
|
|
|
190
188
|
// Priority 0: Sandbox plugins (sorted by priority, highest first)
|
|
191
189
|
// Skipped when ATLAS_SANDBOX=nsjail — operator explicitly wants nsjail only
|
|
192
190
|
if (process.env.ATLAS_SANDBOX !== "nsjail") {
|
|
193
|
-
let sandboxPlugins: Array<{ id: string; [k: string]: unknown }> = [];
|
|
194
191
|
try {
|
|
195
192
|
const { plugins } = await import("@atlas/api/lib/plugins/registry");
|
|
196
|
-
|
|
193
|
+
const { wireSandboxPlugins } = await import("@atlas/api/lib/plugins/wiring");
|
|
194
|
+
const result = await wireSandboxPlugins(plugins, SEMANTIC_ROOT);
|
|
195
|
+
if (result.failed.length > 0) {
|
|
196
|
+
log.warn(
|
|
197
|
+
{ failed: result.failed, selectedPlugin: result.pluginId },
|
|
198
|
+
"Some sandbox plugins failed during create()",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (result.backend) {
|
|
202
|
+
_activeSandboxPluginId = result.pluginId;
|
|
203
|
+
return result.backend as ExploreBackend;
|
|
204
|
+
}
|
|
197
205
|
} catch (err) {
|
|
198
206
|
const detail = err instanceof Error ? err.message : String(err);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const pa = (a as unknown as SandboxShape).sandbox.priority ?? SANDBOX_DEFAULT_PRIORITY;
|
|
206
|
-
const pb = (b as unknown as SandboxShape).sandbox.priority ?? SANDBOX_DEFAULT_PRIORITY;
|
|
207
|
-
return pb - pa;
|
|
208
|
-
});
|
|
209
|
-
for (const sp of sorted) {
|
|
210
|
-
const sandbox = (sp as unknown as SandboxShape).sandbox;
|
|
211
|
-
try {
|
|
212
|
-
const backend = await sandbox.create(SEMANTIC_ROOT);
|
|
213
|
-
_activeSandboxPluginId = sp.id;
|
|
214
|
-
log.info({ pluginId: sp.id }, "Using sandbox plugin for explore backend");
|
|
215
|
-
return backend;
|
|
216
|
-
} catch (err) {
|
|
217
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
218
|
-
log.error({ pluginId: sp.id, err: detail }, "Sandbox plugin create() failed, trying next");
|
|
219
|
-
}
|
|
207
|
+
const isModuleError = err != null && typeof err === "object" && "code" in err
|
|
208
|
+
&& (err as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND";
|
|
209
|
+
if (isModuleError) {
|
|
210
|
+
log.debug({ err: detail }, "Plugin modules not available — skipping sandbox plugins");
|
|
211
|
+
} else {
|
|
212
|
+
log.error({ err: detail }, "Unexpected error during sandbox plugin wiring");
|
|
220
213
|
}
|
|
221
|
-
log.error({ count: sorted.length }, "All sandbox plugins failed to create a backend");
|
|
222
214
|
}
|
|
223
215
|
}
|
|
224
216
|
|
|
@@ -247,7 +239,7 @@ function getExploreBackend(): Promise<ExploreBackend> {
|
|
|
247
239
|
}
|
|
248
240
|
|
|
249
241
|
// Priority 3: Sidecar service (HTTP-isolated microservice)
|
|
250
|
-
// When ATLAS_SANDBOX_URL is set, sidecar is the intended backend (Railway
|
|
242
|
+
// When ATLAS_SANDBOX_URL is set, sidecar is the intended backend (Railway).
|
|
251
243
|
// Skips nsjail auto-detection entirely — no noisy namespace warnings.
|
|
252
244
|
if (useSidecar()) {
|
|
253
245
|
const { createSidecarBackend } = await import("./explore-sidecar");
|
|
@@ -275,10 +267,14 @@ function getExploreBackend(): Promise<ExploreBackend> {
|
|
|
275
267
|
// Priority 5: just-bash (no process isolation)
|
|
276
268
|
if (process.env.NODE_ENV === "production") {
|
|
277
269
|
log.warn(
|
|
278
|
-
"Explore tool running without process isolation. " +
|
|
270
|
+
"SECURITY DEGRADATION: Explore tool running without process isolation (just-bash fallback). " +
|
|
271
|
+
"In production, this means shell commands execute directly on the host with only OverlayFs " +
|
|
272
|
+
"read-only protection — no namespace, network, or resource isolation. " +
|
|
279
273
|
"Install nsjail, configure a sidecar (ATLAS_SANDBOX_URL), or deploy on Vercel for sandboxed execution. " +
|
|
280
274
|
"See: https://github.com/google/nsjail",
|
|
281
275
|
);
|
|
276
|
+
} else {
|
|
277
|
+
log.debug("Explore tool using just-bash backend (acceptable for development)");
|
|
282
278
|
}
|
|
283
279
|
return createBashBackend(SEMANTIC_ROOT);
|
|
284
280
|
})().catch((err) => {
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nsjail backend for the Python execution tool.
|
|
3
|
+
*
|
|
4
|
+
* Uses nsjail (Linux namespaces) to run Python code in a sandboxed process.
|
|
5
|
+
* Follows the same pattern as explore-nsjail.ts but with Python-specific
|
|
6
|
+
* configuration: bind-mounted Python runtime, data injection via stdin,
|
|
7
|
+
* chart collection from tmpfs, and higher resource limits.
|
|
8
|
+
*
|
|
9
|
+
* Security: no network (nsjail default), no host secrets, runs as nobody
|
|
10
|
+
* (65534:65534), code + data injected via tmpfs files and stdin.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PythonBackend, PythonResult } from "./python";
|
|
14
|
+
import { randomUUID } from "crypto";
|
|
15
|
+
import { mkdirSync, writeFileSync, rmSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { createLogger } from "@atlas/api/lib/logger";
|
|
18
|
+
|
|
19
|
+
const log = createLogger("python-nsjail");
|
|
20
|
+
|
|
21
|
+
/** Maximum bytes to read from stdout/stderr (1 MB). */
|
|
22
|
+
const MAX_OUTPUT = 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/** Default Python execution timeout in seconds. */
|
|
25
|
+
const DEFAULT_TIME_LIMIT = 30;
|
|
26
|
+
|
|
27
|
+
/** Default memory limit in MB. */
|
|
28
|
+
const DEFAULT_MEMORY_LIMIT = 512;
|
|
29
|
+
|
|
30
|
+
/** Default max processes. */
|
|
31
|
+
const DEFAULT_NPROC = 16;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Python wrapper script — same logic as the sidecar's PYTHON_WRAPPER.
|
|
35
|
+
*
|
|
36
|
+
* Handles: import guard (sidecar-side enforcement), data injection
|
|
37
|
+
* (JSON on stdin → DataFrame/dict), stdout capture, chart collection
|
|
38
|
+
* (PNG files + Recharts dicts), and structured output via result marker.
|
|
39
|
+
*/
|
|
40
|
+
const PYTHON_WRAPPER = `
|
|
41
|
+
import sys, json, io, base64, glob, os, ast
|
|
42
|
+
|
|
43
|
+
_marker = os.environ["ATLAS_RESULT_MARKER"]
|
|
44
|
+
_chart_dir = os.environ.get("ATLAS_CHART_DIR", "/tmp")
|
|
45
|
+
|
|
46
|
+
# --- Import guard (sidecar-side enforcement) ---
|
|
47
|
+
_BLOCKED_MODULES = {
|
|
48
|
+
"subprocess", "os", "socket", "shutil", "sys", "ctypes", "importlib",
|
|
49
|
+
"code", "signal", "multiprocessing", "threading", "pty", "fcntl",
|
|
50
|
+
"termios", "resource", "posixpath",
|
|
51
|
+
"http", "urllib", "requests", "httpx", "aiohttp", "webbrowser",
|
|
52
|
+
"pickle", "tempfile", "pathlib",
|
|
53
|
+
}
|
|
54
|
+
_BLOCKED_BUILTINS = {
|
|
55
|
+
"compile", "exec", "eval", "__import__", "open", "breakpoint",
|
|
56
|
+
"getattr", "globals", "locals", "vars", "dir", "delattr", "setattr",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_user_code = open(sys.argv[1]).read()
|
|
60
|
+
try:
|
|
61
|
+
_tree = ast.parse(_user_code)
|
|
62
|
+
except SyntaxError as e:
|
|
63
|
+
print(_marker + json.dumps({"success": False, "error": f"SyntaxError: {e.msg} (line {e.lineno})"}))
|
|
64
|
+
sys.exit(0)
|
|
65
|
+
|
|
66
|
+
_blocked = None
|
|
67
|
+
for _node in ast.walk(_tree):
|
|
68
|
+
if _blocked:
|
|
69
|
+
break
|
|
70
|
+
if isinstance(_node, ast.Import):
|
|
71
|
+
for _alias in _node.names:
|
|
72
|
+
_mod = _alias.name.split('.')[0]
|
|
73
|
+
if _mod in _BLOCKED_MODULES:
|
|
74
|
+
_blocked = f'Blocked import: "{_mod}" is not allowed'
|
|
75
|
+
break
|
|
76
|
+
elif isinstance(_node, ast.ImportFrom):
|
|
77
|
+
if _node.module:
|
|
78
|
+
_mod = _node.module.split('.')[0]
|
|
79
|
+
if _mod in _BLOCKED_MODULES:
|
|
80
|
+
_blocked = f'Blocked import: "{_mod}" is not allowed'
|
|
81
|
+
elif isinstance(_node, ast.Call):
|
|
82
|
+
_name = None
|
|
83
|
+
if isinstance(_node.func, ast.Name):
|
|
84
|
+
_name = _node.func.id
|
|
85
|
+
elif isinstance(_node.func, ast.Attribute):
|
|
86
|
+
_name = _node.func.attr
|
|
87
|
+
if _name and _name in _BLOCKED_BUILTINS:
|
|
88
|
+
_blocked = f'Blocked builtin: "{_name}()" is not allowed'
|
|
89
|
+
|
|
90
|
+
if _blocked:
|
|
91
|
+
print(_marker + json.dumps({"success": False, "error": _blocked}))
|
|
92
|
+
sys.exit(0)
|
|
93
|
+
|
|
94
|
+
# --- Data injection ---
|
|
95
|
+
_stdin_data = sys.stdin.read()
|
|
96
|
+
_atlas_data = None
|
|
97
|
+
if _stdin_data.strip():
|
|
98
|
+
_atlas_data = json.loads(_stdin_data)
|
|
99
|
+
|
|
100
|
+
data = None
|
|
101
|
+
df = None
|
|
102
|
+
if _atlas_data:
|
|
103
|
+
try:
|
|
104
|
+
import pandas as pd
|
|
105
|
+
df = pd.DataFrame(_atlas_data["rows"], columns=_atlas_data["columns"])
|
|
106
|
+
data = df
|
|
107
|
+
except ImportError:
|
|
108
|
+
data = _atlas_data
|
|
109
|
+
|
|
110
|
+
# Configure matplotlib for headless rendering
|
|
111
|
+
try:
|
|
112
|
+
import matplotlib
|
|
113
|
+
matplotlib.use('Agg')
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
def chart_path(n=0):
|
|
118
|
+
return os.path.join(_chart_dir, f"chart_{n}.png")
|
|
119
|
+
|
|
120
|
+
# --- Execute user code in isolated namespace ---
|
|
121
|
+
_old_stdout = sys.stdout
|
|
122
|
+
sys.stdout = _captured = io.StringIO()
|
|
123
|
+
|
|
124
|
+
_user_ns = {"chart_path": chart_path, "data": data, "df": df}
|
|
125
|
+
_atlas_error = None
|
|
126
|
+
try:
|
|
127
|
+
exec(_user_code, _user_ns)
|
|
128
|
+
except Exception as e:
|
|
129
|
+
_atlas_error = f"{type(e).__name__}: {e}"
|
|
130
|
+
|
|
131
|
+
_output = _captured.getvalue()
|
|
132
|
+
sys.stdout = _old_stdout
|
|
133
|
+
|
|
134
|
+
# --- Collect results ---
|
|
135
|
+
_charts = []
|
|
136
|
+
for f in sorted(glob.glob(os.path.join(_chart_dir, "chart_*.png"))):
|
|
137
|
+
with open(f, "rb") as fh:
|
|
138
|
+
_charts.append({"base64": base64.b64encode(fh.read()).decode(), "mimeType": "image/png"})
|
|
139
|
+
|
|
140
|
+
_result = {"success": _atlas_error is None}
|
|
141
|
+
if _output.strip():
|
|
142
|
+
_result["output"] = _output.strip()
|
|
143
|
+
if _atlas_error:
|
|
144
|
+
_result["error"] = _atlas_error
|
|
145
|
+
|
|
146
|
+
if "_atlas_table" in _user_ns:
|
|
147
|
+
_result["table"] = _user_ns["_atlas_table"]
|
|
148
|
+
|
|
149
|
+
if "_atlas_chart" in _user_ns:
|
|
150
|
+
_ac = _user_ns["_atlas_chart"]
|
|
151
|
+
if isinstance(_ac, dict):
|
|
152
|
+
_result["rechartsCharts"] = [_ac]
|
|
153
|
+
elif isinstance(_ac, list):
|
|
154
|
+
_result["rechartsCharts"] = _ac
|
|
155
|
+
|
|
156
|
+
if _charts:
|
|
157
|
+
_result["charts"] = _charts
|
|
158
|
+
|
|
159
|
+
print(_marker + json.dumps(_result), file=_old_stdout)
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
/** Read up to `max` bytes from a stream. */
|
|
163
|
+
async function readLimited(stream: ReadableStream, max: number): Promise<string> {
|
|
164
|
+
const reader = stream.getReader();
|
|
165
|
+
const chunks: Uint8Array[] = [];
|
|
166
|
+
let total = 0;
|
|
167
|
+
try {
|
|
168
|
+
while (true) {
|
|
169
|
+
const { done, value } = await reader.read();
|
|
170
|
+
if (done) break;
|
|
171
|
+
total += value.byteLength;
|
|
172
|
+
if (total > max) {
|
|
173
|
+
chunks.push(value.slice(0, max - (total - value.byteLength)));
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
chunks.push(value);
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
await reader.cancel().catch(() => {});
|
|
180
|
+
}
|
|
181
|
+
return new TextDecoder().decode(Buffer.concat(chunks));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Parse a positive integer from an env var, returning defaultValue on invalid input. */
|
|
185
|
+
function parsePositiveInt(envVar: string, defaultValue: number, name: string): number {
|
|
186
|
+
const raw = process.env[envVar];
|
|
187
|
+
if (raw === undefined) return defaultValue;
|
|
188
|
+
const parsed = parseInt(raw, 10);
|
|
189
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
190
|
+
log.warn({ envVar, raw, default: defaultValue }, `Invalid ${envVar} for ${name}, using default`);
|
|
191
|
+
return defaultValue;
|
|
192
|
+
}
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Build nsjail args for Python execution. */
|
|
197
|
+
export function buildPythonNsjailArgs(
|
|
198
|
+
nsjailPath: string,
|
|
199
|
+
tmpDir: string,
|
|
200
|
+
codeFile: string,
|
|
201
|
+
wrapperFile: string,
|
|
202
|
+
chartDir: string,
|
|
203
|
+
_resultMarker: string,
|
|
204
|
+
): string[] {
|
|
205
|
+
const timeLimit = parsePositiveInt("ATLAS_NSJAIL_TIME_LIMIT", DEFAULT_TIME_LIMIT, "time limit");
|
|
206
|
+
const memoryLimit = parsePositiveInt("ATLAS_NSJAIL_MEMORY_LIMIT", DEFAULT_MEMORY_LIMIT, "memory limit");
|
|
207
|
+
const nproc = DEFAULT_NPROC;
|
|
208
|
+
|
|
209
|
+
return [
|
|
210
|
+
nsjailPath,
|
|
211
|
+
"--mode", "o",
|
|
212
|
+
|
|
213
|
+
// Read-only bind mounts: system libs + Python runtime
|
|
214
|
+
"-R", "/bin",
|
|
215
|
+
"-R", "/usr/bin",
|
|
216
|
+
"-R", "/usr/local/bin",
|
|
217
|
+
"-R", "/lib",
|
|
218
|
+
"-R", "/lib64",
|
|
219
|
+
"-R", "/usr/lib",
|
|
220
|
+
"-R", "/usr/local/lib",
|
|
221
|
+
|
|
222
|
+
// Minimal /dev
|
|
223
|
+
"-R", "/dev/null",
|
|
224
|
+
"-R", "/dev/zero",
|
|
225
|
+
"-R", "/dev/urandom",
|
|
226
|
+
|
|
227
|
+
// /proc for correct namespace operation
|
|
228
|
+
"--proc_path", "/proc",
|
|
229
|
+
|
|
230
|
+
// Writable tmpfs for scratch
|
|
231
|
+
"-T", "/tmp",
|
|
232
|
+
|
|
233
|
+
// Bind-mount code files and chart dir into the jail (read-write for charts)
|
|
234
|
+
"-R", `${wrapperFile}:/tmp/wrapper.py`,
|
|
235
|
+
"-R", `${codeFile}:/tmp/user_code.py`,
|
|
236
|
+
"-B", `${chartDir}:/tmp/charts`,
|
|
237
|
+
|
|
238
|
+
// Working directory
|
|
239
|
+
"--cwd", "/tmp",
|
|
240
|
+
|
|
241
|
+
// Time limit
|
|
242
|
+
"-t", String(timeLimit),
|
|
243
|
+
|
|
244
|
+
// Resource limits (higher than explore for data science workloads)
|
|
245
|
+
"--rlimit_as", String(memoryLimit),
|
|
246
|
+
"--rlimit_fsize", "50", // 50 MB for chart output
|
|
247
|
+
"--rlimit_nproc", String(nproc),
|
|
248
|
+
"--rlimit_nofile", "128",
|
|
249
|
+
|
|
250
|
+
// Run as nobody
|
|
251
|
+
"-u", "65534",
|
|
252
|
+
"-g", "65534",
|
|
253
|
+
|
|
254
|
+
// Pass stdin through
|
|
255
|
+
"--pass_fd", "0",
|
|
256
|
+
|
|
257
|
+
// Suppress nsjail info logs
|
|
258
|
+
"--quiet",
|
|
259
|
+
|
|
260
|
+
// Command: python3 wrapper.py user_code.py
|
|
261
|
+
"--",
|
|
262
|
+
"/usr/bin/python3", "/tmp/wrapper.py", "/tmp/user_code.py",
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Minimal env for the Python jail — no secrets. */
|
|
267
|
+
function buildJailEnv(resultMarker: string): Record<string, string> {
|
|
268
|
+
return {
|
|
269
|
+
PATH: "/bin:/usr/bin:/usr/local/bin",
|
|
270
|
+
HOME: "/tmp",
|
|
271
|
+
LANG: "C.UTF-8",
|
|
272
|
+
MPLBACKEND: "Agg",
|
|
273
|
+
ATLAS_CHART_DIR: "/tmp/charts",
|
|
274
|
+
ATLAS_RESULT_MARKER: resultMarker,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Create a PythonBackend that executes code via nsjail. */
|
|
279
|
+
export function createPythonNsjailBackend(nsjailPath: string): PythonBackend {
|
|
280
|
+
return {
|
|
281
|
+
exec: async (code, data): Promise<PythonResult> => {
|
|
282
|
+
const execId = randomUUID();
|
|
283
|
+
const resultMarker = `__ATLAS_RESULT_${execId}__`;
|
|
284
|
+
const tmpDir = join("/tmp", `pyexec-${execId}`);
|
|
285
|
+
const codeFile = join(tmpDir, "user_code.py");
|
|
286
|
+
const wrapperFile = join(tmpDir, "wrapper.py");
|
|
287
|
+
const chartDir = join(tmpDir, "charts");
|
|
288
|
+
|
|
289
|
+
log.debug({ execId, codeLen: code.length }, "python nsjail execution starting");
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
// Prepare tmpfs files
|
|
293
|
+
try {
|
|
294
|
+
mkdirSync(chartDir, { recursive: true });
|
|
295
|
+
writeFileSync(codeFile, code);
|
|
296
|
+
writeFileSync(wrapperFile, PYTHON_WRAPPER);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
299
|
+
log.error({ err: detail, tmpDir, execId }, "Failed to prepare Python execution files");
|
|
300
|
+
return { success: false, error: `Infrastructure error preparing Python sandbox: ${detail}` };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const args = buildPythonNsjailArgs(nsjailPath, tmpDir, codeFile, wrapperFile, chartDir, resultMarker);
|
|
304
|
+
const env = buildJailEnv(resultMarker);
|
|
305
|
+
const stdinPayload = data ? JSON.stringify(data) : "";
|
|
306
|
+
|
|
307
|
+
let proc;
|
|
308
|
+
try {
|
|
309
|
+
proc = Bun.spawn(args, {
|
|
310
|
+
env,
|
|
311
|
+
stdin: "pipe",
|
|
312
|
+
stdout: "pipe",
|
|
313
|
+
stderr: "pipe",
|
|
314
|
+
});
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
317
|
+
log.error({ err: detail, execId }, "nsjail spawn failed for Python execution");
|
|
318
|
+
return { success: false, error: `nsjail infrastructure error: ${detail}` };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Write data to stdin
|
|
322
|
+
try {
|
|
323
|
+
proc.stdin.write(stdinPayload);
|
|
324
|
+
proc.stdin.end();
|
|
325
|
+
} catch (err) {
|
|
326
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
327
|
+
log.warn({ err: detail, execId }, "stdin write error during Python execution");
|
|
328
|
+
if (data) {
|
|
329
|
+
proc.kill();
|
|
330
|
+
return { success: false, error: `Failed to inject data into Python sandbox: ${detail}` };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const [stdout, stderr] = await Promise.all([
|
|
335
|
+
readLimited(proc.stdout, MAX_OUTPUT),
|
|
336
|
+
readLimited(proc.stderr, MAX_OUTPUT),
|
|
337
|
+
]);
|
|
338
|
+
const exitCode = await proc.exited;
|
|
339
|
+
|
|
340
|
+
log.debug({ execId, exitCode, stdoutLen: stdout.length }, "python nsjail execution finished");
|
|
341
|
+
|
|
342
|
+
// Extract structured result from the last marker line
|
|
343
|
+
const lines = stdout.split("\n");
|
|
344
|
+
const resultLine = lines.findLast((l) => l.startsWith(resultMarker));
|
|
345
|
+
|
|
346
|
+
if (resultLine) {
|
|
347
|
+
try {
|
|
348
|
+
return JSON.parse(resultLine.slice(resultMarker.length)) as PythonResult;
|
|
349
|
+
} catch {
|
|
350
|
+
log.warn({ execId, resultLine: resultLine.slice(0, 500) }, "failed to parse Python result JSON");
|
|
351
|
+
return {
|
|
352
|
+
success: false,
|
|
353
|
+
error: `Python produced unparseable output. stderr: ${stderr.trim().slice(0, 500)}`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// No structured result — process errored before the wrapper could emit one
|
|
359
|
+
if (stdout.length >= MAX_OUTPUT) {
|
|
360
|
+
return {
|
|
361
|
+
success: false,
|
|
362
|
+
error: "Python output exceeded 1 MB limit — the result was likely truncated. " +
|
|
363
|
+
"Reduce print() output or use _atlas_table for large results.",
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (exitCode > 128) {
|
|
368
|
+
const signal = exitCode - 128;
|
|
369
|
+
const signalNames: Record<number, string> = { 6: "SIGABRT", 9: "SIGKILL", 11: "SIGSEGV", 15: "SIGTERM" };
|
|
370
|
+
const name = signalNames[signal] ?? `signal ${signal}`;
|
|
371
|
+
log.warn({ execId, signal, name }, "Python process killed by signal");
|
|
372
|
+
if (signal === 9) {
|
|
373
|
+
return { success: false, error: "Python execution killed (likely exceeded time or memory limit)" };
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: `Python process terminated by ${name}${stderr.trim() ? `: ${stderr.trim().slice(0, 500)}` : ""}`,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
error: stderr.trim() || `Python execution failed (exit code ${exitCode})`,
|
|
384
|
+
};
|
|
385
|
+
} finally {
|
|
386
|
+
// Cleanup tmpfs
|
|
387
|
+
try {
|
|
388
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
391
|
+
log.warn({ err: detail, tmpDir }, "failed to clean up Python tmpdir");
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|