@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.
- 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 +11 -11
- package/templates/docker/bin/atlas.ts +120 -56
- 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 +4 -41
- 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 +38 -40
- 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 +104 -1
- package/templates/docker/src/lib/plugins/registry.ts +14 -8
- 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-sdk-compat.test.ts +1 -1
- 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 +11 -11
- package/templates/nextjs-standalone/bin/atlas.ts +120 -56
- 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 +4 -41
- 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 +38 -40
- 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 +104 -1
- package/templates/nextjs-standalone/src/lib/plugins/registry.ts +14 -8
- 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-sdk-compat.test.ts +1 -1
- 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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { describe, expect, it, mock, afterEach } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Mock logger to avoid side effects
|
|
4
|
+
mock.module("@atlas/api/lib/logger", () => ({
|
|
5
|
+
createLogger: () => ({
|
|
6
|
+
debug: () => {},
|
|
7
|
+
info: () => {},
|
|
8
|
+
warn: () => {},
|
|
9
|
+
error: () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const { executePythonViaSidecar } = await import(
|
|
14
|
+
"@atlas/api/lib/tools/python-sidecar"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const SIDECAR_URL = "http://localhost:9999";
|
|
22
|
+
|
|
23
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
24
|
+
|
|
25
|
+
function setEnv(key: string, value: string | undefined) {
|
|
26
|
+
if (!(key in savedEnv)) savedEnv[key] = process.env[key];
|
|
27
|
+
if (value === undefined) delete process.env[key];
|
|
28
|
+
else process.env[key] = value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Save original fetch and provide a type-safe mock helper
|
|
32
|
+
const originalFetch = globalThis.fetch;
|
|
33
|
+
|
|
34
|
+
/** Override globalThis.fetch with a mock function. */
|
|
35
|
+
function mockFetch(fn: (input: string | URL | Request, init?: RequestInit) => Promise<Response>) {
|
|
36
|
+
globalThis.fetch = fn as typeof globalThis.fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
// Restore env
|
|
41
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
42
|
+
if (value === undefined) delete process.env[key];
|
|
43
|
+
else process.env[key] = value;
|
|
44
|
+
}
|
|
45
|
+
for (const key of Object.keys(savedEnv)) delete savedEnv[key];
|
|
46
|
+
|
|
47
|
+
// Restore fetch
|
|
48
|
+
globalThis.fetch = originalFetch;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Tests
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
describe("executePythonViaSidecar", () => {
|
|
56
|
+
describe("URL validation", () => {
|
|
57
|
+
it("returns error for invalid sidecar URL", async () => {
|
|
58
|
+
const result = await executePythonViaSidecar(
|
|
59
|
+
"not-a-url",
|
|
60
|
+
'print("hello")',
|
|
61
|
+
);
|
|
62
|
+
expect(result.success).toBe(false);
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
expect(result.error).toContain("Invalid ATLAS_SANDBOX_URL");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("connection errors", () => {
|
|
70
|
+
it("returns error when sidecar is unreachable", async () => {
|
|
71
|
+
mockFetch(() => Promise.reject(new Error("fetch failed: ECONNREFUSED")));
|
|
72
|
+
|
|
73
|
+
const result = await executePythonViaSidecar(
|
|
74
|
+
SIDECAR_URL,
|
|
75
|
+
'print("hello")',
|
|
76
|
+
);
|
|
77
|
+
expect(result.success).toBe(false);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
expect(result.error).toContain("sidecar unreachable");
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns timeout error when request exceeds deadline", async () => {
|
|
84
|
+
mockFetch(() => Promise.reject(new Error("TimeoutError: timed out")));
|
|
85
|
+
|
|
86
|
+
const result = await executePythonViaSidecar(
|
|
87
|
+
SIDECAR_URL,
|
|
88
|
+
'print("hello")',
|
|
89
|
+
);
|
|
90
|
+
expect(result.success).toBe(false);
|
|
91
|
+
if (!result.success) {
|
|
92
|
+
expect(result.error).toContain("timed out");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("returns error for generic fetch failures", async () => {
|
|
97
|
+
mockFetch(() => Promise.reject(new Error("network error")));
|
|
98
|
+
|
|
99
|
+
const result = await executePythonViaSidecar(
|
|
100
|
+
SIDECAR_URL,
|
|
101
|
+
'print("hello")',
|
|
102
|
+
);
|
|
103
|
+
expect(result.success).toBe(false);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
expect(result.error).toContain("Sidecar request failed");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("HTTP error responses", () => {
|
|
111
|
+
it("parses structured error from HTTP 500", async () => {
|
|
112
|
+
mockFetch(() =>
|
|
113
|
+
Promise.resolve(
|
|
114
|
+
new Response(
|
|
115
|
+
JSON.stringify({ success: false, error: "crash" }),
|
|
116
|
+
{ status: 500 },
|
|
117
|
+
),
|
|
118
|
+
));
|
|
119
|
+
|
|
120
|
+
const result = await executePythonViaSidecar(
|
|
121
|
+
SIDECAR_URL,
|
|
122
|
+
'print("hello")',
|
|
123
|
+
);
|
|
124
|
+
expect(result.success).toBe(false);
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
expect(result.error).toBe("crash");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("returns raw error for non-500 HTTP errors", async () => {
|
|
131
|
+
mockFetch(() =>
|
|
132
|
+
Promise.resolve(
|
|
133
|
+
new Response("Rate limited", { status: 429 }),
|
|
134
|
+
));
|
|
135
|
+
|
|
136
|
+
const result = await executePythonViaSidecar(
|
|
137
|
+
SIDECAR_URL,
|
|
138
|
+
'print("hello")',
|
|
139
|
+
);
|
|
140
|
+
expect(result.success).toBe(false);
|
|
141
|
+
if (!result.success) {
|
|
142
|
+
expect(result.error).toContain("HTTP 429");
|
|
143
|
+
expect(result.error).toContain("Rate limited");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("returns raw error for 500 with non-JSON body", async () => {
|
|
148
|
+
mockFetch(() =>
|
|
149
|
+
Promise.resolve(
|
|
150
|
+
new Response("Internal Server Error", { status: 500 }),
|
|
151
|
+
));
|
|
152
|
+
|
|
153
|
+
const result = await executePythonViaSidecar(
|
|
154
|
+
SIDECAR_URL,
|
|
155
|
+
'print("hello")',
|
|
156
|
+
);
|
|
157
|
+
expect(result.success).toBe(false);
|
|
158
|
+
if (!result.success) {
|
|
159
|
+
expect(result.error).toContain("HTTP 500");
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("response parsing", () => {
|
|
165
|
+
it("returns error when response JSON is unparseable", async () => {
|
|
166
|
+
mockFetch(() =>
|
|
167
|
+
Promise.resolve(new Response("not json", { status: 200 })));
|
|
168
|
+
|
|
169
|
+
const result = await executePythonViaSidecar(
|
|
170
|
+
SIDECAR_URL,
|
|
171
|
+
'print("hello")',
|
|
172
|
+
);
|
|
173
|
+
expect(result.success).toBe(false);
|
|
174
|
+
if (!result.success) {
|
|
175
|
+
expect(result.error).toContain("Failed to parse sidecar response");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns error when response lacks success boolean", async () => {
|
|
180
|
+
mockFetch(() =>
|
|
181
|
+
Promise.resolve(
|
|
182
|
+
Response.json({ output: "hello" }),
|
|
183
|
+
));
|
|
184
|
+
|
|
185
|
+
const result = await executePythonViaSidecar(
|
|
186
|
+
SIDECAR_URL,
|
|
187
|
+
'print("hello")',
|
|
188
|
+
);
|
|
189
|
+
expect(result.success).toBe(false);
|
|
190
|
+
if (!result.success) {
|
|
191
|
+
expect(result.error).toContain("unexpected response format");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("successful execution", () => {
|
|
197
|
+
it("returns PythonResult on success", async () => {
|
|
198
|
+
mockFetch(() =>
|
|
199
|
+
Promise.resolve(
|
|
200
|
+
Response.json({
|
|
201
|
+
success: true,
|
|
202
|
+
output: "hello world",
|
|
203
|
+
}),
|
|
204
|
+
));
|
|
205
|
+
|
|
206
|
+
const result = await executePythonViaSidecar(
|
|
207
|
+
SIDECAR_URL,
|
|
208
|
+
'print("hello world")',
|
|
209
|
+
);
|
|
210
|
+
expect(result.success).toBe(true);
|
|
211
|
+
if (result.success) {
|
|
212
|
+
expect(result.output).toBe("hello world");
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("returns result with table data", async () => {
|
|
217
|
+
mockFetch(() =>
|
|
218
|
+
Promise.resolve(
|
|
219
|
+
Response.json({
|
|
220
|
+
success: true,
|
|
221
|
+
table: { columns: ["x"], rows: [[1], [2]] },
|
|
222
|
+
}),
|
|
223
|
+
));
|
|
224
|
+
|
|
225
|
+
const result = await executePythonViaSidecar(
|
|
226
|
+
SIDECAR_URL,
|
|
227
|
+
"code",
|
|
228
|
+
);
|
|
229
|
+
expect(result.success).toBe(true);
|
|
230
|
+
if (result.success) {
|
|
231
|
+
expect(result.table).toEqual({ columns: ["x"], rows: [[1], [2]] });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("returns result with rechartsCharts", async () => {
|
|
236
|
+
mockFetch(() =>
|
|
237
|
+
Promise.resolve(
|
|
238
|
+
Response.json({
|
|
239
|
+
success: true,
|
|
240
|
+
rechartsCharts: [
|
|
241
|
+
{ type: "bar", data: [{ x: 1 }], categoryKey: "x", valueKeys: ["x"] },
|
|
242
|
+
],
|
|
243
|
+
}),
|
|
244
|
+
));
|
|
245
|
+
|
|
246
|
+
const result = await executePythonViaSidecar(
|
|
247
|
+
SIDECAR_URL,
|
|
248
|
+
"code",
|
|
249
|
+
);
|
|
250
|
+
expect(result.success).toBe(true);
|
|
251
|
+
if (result.success) {
|
|
252
|
+
expect(result.rechartsCharts).toHaveLength(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("returns error result from sidecar", async () => {
|
|
257
|
+
mockFetch(() =>
|
|
258
|
+
Promise.resolve(
|
|
259
|
+
Response.json({
|
|
260
|
+
success: false,
|
|
261
|
+
error: "ZeroDivisionError: division by zero",
|
|
262
|
+
}),
|
|
263
|
+
));
|
|
264
|
+
|
|
265
|
+
const result = await executePythonViaSidecar(
|
|
266
|
+
SIDECAR_URL,
|
|
267
|
+
"1/0",
|
|
268
|
+
);
|
|
269
|
+
expect(result.success).toBe(false);
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
expect(result.error).toContain("ZeroDivisionError");
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe("request construction", () => {
|
|
277
|
+
it("sends auth header when SIDECAR_AUTH_TOKEN is set", async () => {
|
|
278
|
+
setEnv("SIDECAR_AUTH_TOKEN", "test-token-123");
|
|
279
|
+
let capturedHeaders: Headers | null = null;
|
|
280
|
+
|
|
281
|
+
mockFetch((_input, init) => {
|
|
282
|
+
capturedHeaders = new Headers(init?.headers);
|
|
283
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await executePythonViaSidecar(SIDECAR_URL, "code");
|
|
287
|
+
expect(capturedHeaders!.get("Authorization")).toBe("Bearer test-token-123");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("omits auth header when SIDECAR_AUTH_TOKEN is not set", async () => {
|
|
291
|
+
setEnv("SIDECAR_AUTH_TOKEN", undefined);
|
|
292
|
+
let capturedHeaders: Headers | null = null;
|
|
293
|
+
|
|
294
|
+
mockFetch((_input, init) => {
|
|
295
|
+
capturedHeaders = new Headers(init?.headers);
|
|
296
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
await executePythonViaSidecar(SIDECAR_URL, "code");
|
|
300
|
+
expect(capturedHeaders!.get("Authorization")).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("sends data payload when provided", async () => {
|
|
304
|
+
let capturedBody: string | null = null;
|
|
305
|
+
|
|
306
|
+
mockFetch((_input, init) => {
|
|
307
|
+
capturedBody = init?.body as string;
|
|
308
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const data = { columns: ["id"], rows: [[1], [2]] };
|
|
312
|
+
await executePythonViaSidecar(SIDECAR_URL, "code", data);
|
|
313
|
+
|
|
314
|
+
const parsed = JSON.parse(capturedBody!);
|
|
315
|
+
expect(parsed.data).toEqual(data);
|
|
316
|
+
expect(parsed.code).toBe("code");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("omits data from request when not provided", async () => {
|
|
320
|
+
let capturedBody: string | null = null;
|
|
321
|
+
|
|
322
|
+
mockFetch((_input, init) => {
|
|
323
|
+
capturedBody = init?.body as string;
|
|
324
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
await executePythonViaSidecar(SIDECAR_URL, "code");
|
|
328
|
+
|
|
329
|
+
const parsed = JSON.parse(capturedBody!);
|
|
330
|
+
expect(parsed.data).toBeUndefined();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("timeout configuration", () => {
|
|
335
|
+
it("uses ATLAS_PYTHON_TIMEOUT when set", async () => {
|
|
336
|
+
setEnv("ATLAS_PYTHON_TIMEOUT", "5000");
|
|
337
|
+
let capturedBody: string | null = null;
|
|
338
|
+
|
|
339
|
+
mockFetch((_input, init) => {
|
|
340
|
+
capturedBody = init?.body as string;
|
|
341
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
await executePythonViaSidecar(SIDECAR_URL, "code");
|
|
345
|
+
|
|
346
|
+
const parsed = JSON.parse(capturedBody!);
|
|
347
|
+
expect(parsed.timeout).toBe(5000);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("falls back to default on invalid ATLAS_PYTHON_TIMEOUT", async () => {
|
|
351
|
+
setEnv("ATLAS_PYTHON_TIMEOUT", "not-a-number");
|
|
352
|
+
let capturedBody: string | null = null;
|
|
353
|
+
|
|
354
|
+
mockFetch((_input, init) => {
|
|
355
|
+
capturedBody = init?.body as string;
|
|
356
|
+
return Promise.resolve(Response.json({ success: true }));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await executePythonViaSidecar(SIDECAR_URL, "code");
|
|
360
|
+
|
|
361
|
+
const parsed = JSON.parse(capturedBody!);
|
|
362
|
+
expect(parsed.timeout).toBe(30000); // DEFAULT_TIMEOUT_MS
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { describe, expect, it, mock, afterEach } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Mock logger and tracing to avoid side effects
|
|
4
|
+
mock.module("@atlas/api/lib/logger", () => ({
|
|
5
|
+
createLogger: () => ({
|
|
6
|
+
debug: () => {},
|
|
7
|
+
info: () => {},
|
|
8
|
+
warn: () => {},
|
|
9
|
+
error: () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
mock.module("@atlas/api/lib/tracing", () => ({
|
|
14
|
+
withSpan: async (_name: string, _attrs: unknown, fn: () => Promise<unknown>) => fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const { validatePythonCode } = await import(
|
|
18
|
+
"@atlas/api/lib/tools/python"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Import guard tests
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
describe("validatePythonCode", () => {
|
|
26
|
+
describe("blocked imports", () => {
|
|
27
|
+
it("rejects import subprocess", async () => {
|
|
28
|
+
const result = await validatePythonCode("import subprocess");
|
|
29
|
+
expect(result.safe).toBe(false);
|
|
30
|
+
if (!result.safe) expect(result.reason).toContain("subprocess");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects from os import path", async () => {
|
|
34
|
+
const result = await validatePythonCode("from os import path");
|
|
35
|
+
expect(result.safe).toBe(false);
|
|
36
|
+
if (!result.safe) expect(result.reason).toContain("os");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("rejects import socket", async () => {
|
|
40
|
+
const result = await validatePythonCode("import socket");
|
|
41
|
+
expect(result.safe).toBe(false);
|
|
42
|
+
if (!result.safe) expect(result.reason).toContain("socket");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("rejects import shutil", async () => {
|
|
46
|
+
const result = await validatePythonCode("import shutil");
|
|
47
|
+
expect(result.safe).toBe(false);
|
|
48
|
+
if (!result.safe) expect(result.reason).toContain("shutil");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("rejects import sys", async () => {
|
|
52
|
+
const result = await validatePythonCode("import sys");
|
|
53
|
+
expect(result.safe).toBe(false);
|
|
54
|
+
if (!result.safe) expect(result.reason).toContain("sys");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("rejects import ctypes", async () => {
|
|
58
|
+
const result = await validatePythonCode("import ctypes");
|
|
59
|
+
expect(result.safe).toBe(false);
|
|
60
|
+
if (!result.safe) expect(result.reason).toContain("ctypes");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("rejects import importlib", async () => {
|
|
64
|
+
const result = await validatePythonCode("import importlib");
|
|
65
|
+
expect(result.safe).toBe(false);
|
|
66
|
+
if (!result.safe) expect(result.reason).toContain("importlib");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects import code", async () => {
|
|
70
|
+
const result = await validatePythonCode("import code");
|
|
71
|
+
expect(result.safe).toBe(false);
|
|
72
|
+
if (!result.safe) expect(result.reason).toContain("code");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects import signal", async () => {
|
|
76
|
+
const result = await validatePythonCode("import signal");
|
|
77
|
+
expect(result.safe).toBe(false);
|
|
78
|
+
if (!result.safe) expect(result.reason).toContain("signal");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("rejects import multiprocessing", async () => {
|
|
82
|
+
const result = await validatePythonCode("import multiprocessing");
|
|
83
|
+
expect(result.safe).toBe(false);
|
|
84
|
+
if (!result.safe) expect(result.reason).toContain("multiprocessing");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("rejects from subprocess import run", async () => {
|
|
88
|
+
const result = await validatePythonCode("from subprocess import run");
|
|
89
|
+
expect(result.safe).toBe(false);
|
|
90
|
+
if (!result.safe) expect(result.reason).toContain("subprocess");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("rejects os as submodule (import os.path)", async () => {
|
|
94
|
+
const result = await validatePythonCode("import os.path");
|
|
95
|
+
expect(result.safe).toBe(false);
|
|
96
|
+
if (!result.safe) expect(result.reason).toContain("os");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Network modules (added per PR review #5)
|
|
100
|
+
it("rejects import http", async () => {
|
|
101
|
+
const result = await validatePythonCode("import http");
|
|
102
|
+
expect(result.safe).toBe(false);
|
|
103
|
+
if (!result.safe) expect(result.reason).toContain("http");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("rejects import urllib", async () => {
|
|
107
|
+
const result = await validatePythonCode("import urllib");
|
|
108
|
+
expect(result.safe).toBe(false);
|
|
109
|
+
if (!result.safe) expect(result.reason).toContain("urllib");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("rejects import requests", async () => {
|
|
113
|
+
const result = await validatePythonCode("import requests");
|
|
114
|
+
expect(result.safe).toBe(false);
|
|
115
|
+
if (!result.safe) expect(result.reason).toContain("requests");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("rejects import pickle", async () => {
|
|
119
|
+
const result = await validatePythonCode("import pickle");
|
|
120
|
+
expect(result.safe).toBe(false);
|
|
121
|
+
if (!result.safe) expect(result.reason).toContain("pickle");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("rejects import tempfile", async () => {
|
|
125
|
+
const result = await validatePythonCode("import tempfile");
|
|
126
|
+
expect(result.safe).toBe(false);
|
|
127
|
+
if (!result.safe) expect(result.reason).toContain("tempfile");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("rejects import pathlib", async () => {
|
|
131
|
+
const result = await validatePythonCode("import pathlib");
|
|
132
|
+
expect(result.safe).toBe(false);
|
|
133
|
+
if (!result.safe) expect(result.reason).toContain("pathlib");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("blocked builtins", () => {
|
|
138
|
+
it("rejects exec()", async () => {
|
|
139
|
+
const result = await validatePythonCode('exec("print(1)")');
|
|
140
|
+
expect(result.safe).toBe(false);
|
|
141
|
+
if (!result.safe) expect(result.reason).toContain("exec");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects eval()", async () => {
|
|
145
|
+
const result = await validatePythonCode('eval("1+1")');
|
|
146
|
+
expect(result.safe).toBe(false);
|
|
147
|
+
if (!result.safe) expect(result.reason).toContain("eval");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("rejects compile()", async () => {
|
|
151
|
+
const result = await validatePythonCode('compile("x=1", "<string>", "exec")');
|
|
152
|
+
expect(result.safe).toBe(false);
|
|
153
|
+
if (!result.safe) expect(result.reason).toContain("compile");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("rejects __import__()", async () => {
|
|
157
|
+
const result = await validatePythonCode('__import__("os")');
|
|
158
|
+
expect(result.safe).toBe(false);
|
|
159
|
+
if (!result.safe) expect(result.reason).toContain("__import__");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("rejects open()", async () => {
|
|
163
|
+
const result = await validatePythonCode('open("/etc/passwd")');
|
|
164
|
+
expect(result.safe).toBe(false);
|
|
165
|
+
if (!result.safe) expect(result.reason).toContain("open");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("rejects breakpoint()", async () => {
|
|
169
|
+
const result = await validatePythonCode("breakpoint()");
|
|
170
|
+
expect(result.safe).toBe(false);
|
|
171
|
+
if (!result.safe) expect(result.reason).toContain("breakpoint");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Guard bypass mitigations (PR review #1)
|
|
175
|
+
it("rejects getattr()", async () => {
|
|
176
|
+
const result = await validatePythonCode('getattr(__builtins__, "exec")("print(1)")');
|
|
177
|
+
expect(result.safe).toBe(false);
|
|
178
|
+
if (!result.safe) expect(result.reason).toContain("getattr");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("rejects globals()", async () => {
|
|
182
|
+
const result = await validatePythonCode('globals()["__builtins__"]');
|
|
183
|
+
expect(result.safe).toBe(false);
|
|
184
|
+
if (!result.safe) expect(result.reason).toContain("globals");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("rejects vars()", async () => {
|
|
188
|
+
const result = await validatePythonCode("vars()");
|
|
189
|
+
expect(result.safe).toBe(false);
|
|
190
|
+
if (!result.safe) expect(result.reason).toContain("vars");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("allowed imports", () => {
|
|
195
|
+
it("allows pandas", async () => {
|
|
196
|
+
const result = await validatePythonCode("import pandas as pd");
|
|
197
|
+
expect(result.safe).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("allows numpy", async () => {
|
|
201
|
+
const result = await validatePythonCode("import numpy as np");
|
|
202
|
+
expect(result.safe).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("allows matplotlib", async () => {
|
|
206
|
+
const result = await validatePythonCode("import matplotlib.pyplot as plt");
|
|
207
|
+
expect(result.safe).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("allows json", async () => {
|
|
211
|
+
const result = await validatePythonCode("import json");
|
|
212
|
+
expect(result.safe).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("allows math", async () => {
|
|
216
|
+
const result = await validatePythonCode("import math");
|
|
217
|
+
expect(result.safe).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("allows datetime", async () => {
|
|
221
|
+
const result = await validatePythonCode("from datetime import datetime");
|
|
222
|
+
expect(result.safe).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("allows statistics", async () => {
|
|
226
|
+
const result = await validatePythonCode("import statistics");
|
|
227
|
+
expect(result.safe).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("allows collections", async () => {
|
|
231
|
+
const result = await validatePythonCode("from collections import Counter");
|
|
232
|
+
expect(result.safe).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("syntax errors", () => {
|
|
237
|
+
it("rejects code with syntax errors", async () => {
|
|
238
|
+
const result = await validatePythonCode("def foo(");
|
|
239
|
+
expect(result.safe).toBe(false);
|
|
240
|
+
if (!result.safe) expect(result.reason).toContain("SyntaxError");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("complex code", () => {
|
|
245
|
+
it("allows legitimate data analysis code", async () => {
|
|
246
|
+
const code = `
|
|
247
|
+
import json
|
|
248
|
+
import math
|
|
249
|
+
from collections import Counter
|
|
250
|
+
|
|
251
|
+
values = [1, 2, 3, 4, 5]
|
|
252
|
+
mean = sum(values) / len(values)
|
|
253
|
+
print(f"Mean: {mean}")
|
|
254
|
+
`;
|
|
255
|
+
const result = await validatePythonCode(code);
|
|
256
|
+
expect(result.safe).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("rejects code with blocked import buried in logic", async () => {
|
|
260
|
+
const code = `
|
|
261
|
+
x = 1
|
|
262
|
+
y = 2
|
|
263
|
+
import subprocess
|
|
264
|
+
z = x + y
|
|
265
|
+
`;
|
|
266
|
+
const result = await validatePythonCode(code);
|
|
267
|
+
expect(result.safe).toBe(false);
|
|
268
|
+
if (!result.safe) expect(result.reason).toContain("subprocess");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Sidecar routing tests
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
describe("executePython tool", () => {
|
|
278
|
+
const savedEnv: Record<string, string | undefined> = {};
|
|
279
|
+
|
|
280
|
+
function saveAndSetEnv(key: string, value: string | undefined) {
|
|
281
|
+
if (!(key in savedEnv)) {
|
|
282
|
+
savedEnv[key] = process.env[key];
|
|
283
|
+
}
|
|
284
|
+
if (value === undefined) {
|
|
285
|
+
delete process.env[key];
|
|
286
|
+
} else {
|
|
287
|
+
process.env[key] = value;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
afterEach(() => {
|
|
292
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
293
|
+
if (value === undefined) {
|
|
294
|
+
delete process.env[key];
|
|
295
|
+
} else {
|
|
296
|
+
process.env[key] = value;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const key of Object.keys(savedEnv)) {
|
|
300
|
+
delete savedEnv[key];
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("rejects execution when ATLAS_SANDBOX_URL is not set", async () => {
|
|
305
|
+
saveAndSetEnv("ATLAS_SANDBOX_URL", undefined);
|
|
306
|
+
|
|
307
|
+
const { executePython } = await import("@atlas/api/lib/tools/python");
|
|
308
|
+
const execute = executePython.execute!;
|
|
309
|
+
const result = await execute(
|
|
310
|
+
{ code: 'print("hello")', explanation: "test", data: undefined },
|
|
311
|
+
{} as never,
|
|
312
|
+
) as { success: boolean; error?: string };
|
|
313
|
+
expect(result.success).toBe(false);
|
|
314
|
+
expect(result.error).toContain("ATLAS_SANDBOX_URL");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("rejects code that fails import guard before hitting sidecar", async () => {
|
|
318
|
+
saveAndSetEnv("ATLAS_SANDBOX_URL", "http://localhost:9999");
|
|
319
|
+
|
|
320
|
+
const { executePython } = await import("@atlas/api/lib/tools/python");
|
|
321
|
+
const execute = executePython.execute!;
|
|
322
|
+
const result = await execute(
|
|
323
|
+
{ code: "import subprocess", explanation: "test", data: undefined },
|
|
324
|
+
{} as never,
|
|
325
|
+
) as { success: boolean; error?: string };
|
|
326
|
+
expect(result.success).toBe(false);
|
|
327
|
+
expect(result.error).toContain("subprocess");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Registry gating (ATLAS_PYTHON_ENABLED + ATLAS_SANDBOX_URL) is tested in registry.test.ts
|