From 916eecb83574b6aba1906ce197196f04df25049a Mon Sep 17 00:00:00 2001 From: fatelei Date: Fri, 26 Dec 2025 16:25:37 +0800 Subject: [PATCH] feat: use agent-infra sandbox instead of seccomp --- .gitignore | 5 +- .golangci.yml | 102 +++++ .pre-commit-config.yaml | 36 ++ Makefile | 96 +++++ README.md | 74 +++- cmd/lib/nodejs/main.go | 2 + cmd/lib/python/main.go | 2 + cmd/test/fuzz_nodejs/main.go | 3 + cmd/test/fuzz_nodejs_amd64/main.go | 2 + cmd/test/fuzz_python/main.go | 3 + cmd/test/fuzz_python_amd64/main.go | 2 + cmd/test/permission/main.go | 2 + cmd/test/python/main.go | 2 + cmd/test/syscall_dig/main.go | 2 + cmd/test/tmp/main.go | 2 + conf/config.yaml | 12 + go.mod | 4 +- go.sum | 4 + internal/controller/run.go | 4 +- internal/core/lib/nodejs/add_seccomp.go | 2 + internal/core/lib/python/add_seccomp.go | 2 + internal/core/lib/seccomp.go | 2 + internal/core/lib/set_no_new_privs.go | 2 + internal/core/runner/nodejs/nodejs.go | 4 + internal/core/runner/nodejs/nodejs_stub.go | 33 ++ internal/core/runner/nodejs/setup.go | 2 + internal/core/runner/nodejs/setup_stub.go | 25 ++ .../nodejs_microsandbox.go | 71 ++++ .../nodejs_microsandbox_test.go | 381 ++++++++++++++++++ internal/core/runner/python/env.go | 2 + internal/core/runner/python/python.go | 4 + internal/core/runner/python/python_stub.go | 33 ++ internal/core/runner/python/setup.go | 2 + internal/core/runner/python/setup_stub.go | 47 +++ .../python_microsandbox.go | 72 ++++ .../python_microsandbox_test.go | 217 ++++++++++ .../core/runner/types/runner_interface.go | 21 + internal/server/server.go | 27 +- internal/service/nodejs.go | 50 ++- internal/service/python.go | 82 +++- internal/static/config_default_stub.go | 6 + internal/types/config.go | 7 +- .../integration_tests/nodejs_feature_test.go | 7 +- .../nodejs_malicious_test.go | 5 +- .../integration_tests/python_feature_test.go | 11 +- .../python_longchars_test.go | 3 +- .../python_malicious_test.go | 9 +- 47 files changed, 1415 insertions(+), 73 deletions(-) create mode 100644 .golangci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 Makefile create mode 100644 internal/core/runner/nodejs/nodejs_stub.go create mode 100644 internal/core/runner/nodejs/setup_stub.go create mode 100644 internal/core/runner/nodejs_microsandbox/nodejs_microsandbox.go create mode 100644 internal/core/runner/nodejs_microsandbox/nodejs_microsandbox_test.go create mode 100644 internal/core/runner/python/python_stub.go create mode 100644 internal/core/runner/python/setup_stub.go create mode 100644 internal/core/runner/python_microsandbox/python_microsandbox.go create mode 100644 internal/core/runner/python_microsandbox/python_microsandbox_test.go create mode 100644 internal/core/runner/types/runner_interface.go create mode 100644 internal/static/config_default_stub.go diff --git a/.gitignore b/.gitignore index 9d83d5ee..8cdf31e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # log logs/ +log/ # editor config .idea/ .vscode/ @@ -10,4 +11,6 @@ logs/ env main -*.gen.dockerfile \ No newline at end of file +*.gen.dockerfile +.menv/ +coverage.out diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..8ce63d05 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,102 @@ +version: 2 + +run: + timeout: 5m + tests: true + skip-dirs: + - vendor + - tests + - testdata + +linters: + default: none + enable: + - govet + - errcheck + - staticcheck + - unused + - ineffassign + - misspell + - gocyclo + - goconst + - unconvert + - prealloc + - gocritic + - revive + - copyloopvar + - nilerr + - noctx + - gosec + +formatters: + enable: + - goimports + +linters-settings: + govet: + check-shadowing: true + + errcheck: + check-type-assertions: true + check-blank: false + + gocyclo: + min-complexity: 15 + + goconst: + min-len: 2 + min-occurrences: 3 + + goimports: + local-prefixes: github.com/langgenius/dify-sandbox + + revive: + confidence: 0.8 + rules: + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: redefines-builtin-id + + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 + exclude: + - "cmd/test/" + exclude-rules: + - path: _test\.go + linters: + - gocyclo + - errcheck + - goconst + - govet + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..f558f7bc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-json + - id: check-toml + - id: detect-private-key + - id: mixed-line-ending + + - repo: https://github.com/golangci/golangci-lint + rev: v1.62.0 + hooks: + - id: golangci-lint + args: + - --config=.golangci.yml + entry: golangci-lint run --fix + pass_filenames: false + + - repo: local + hooks: + - id: go-test + name: go test + entry: go test -v ./... + language: system + pass_filenames: false + - id: go-mod-tidy + name: go mod tidy + entry: go mod tidy + language: system + pass_filenames: false diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..35abc4f9 --- /dev/null +++ b/Makefile @@ -0,0 +1,96 @@ +.PHONY: help run test lint lint-fix fmt vet install-deps clean build pre-commit-install + +# Variables +BINARY_NAME=main +GO=go +GOFLAGS=-v +DOCKER_REGISTRY=ghcr.io/agent-infra/sandbox +SANDBOX_PORT=10000 + +help: ## Show this help message + @echo "Usage: make [target]" + @echo "" + @echo "Available targets:" + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +run: ## Run the server + @echo "Starting server..." + @$(GO) run cmd/server/main.go + +build: ## Build the server binary + @echo "Building $(BINARY_NAME)..." + @$(GO) build $(GOFLAGS) -o $(BINARY_NAME) cmd/server/main.go + +test: ## Run all tests + @echo "Running tests..." + @$(GO) test -v -race -coverprofile=coverage.out -covermode=atomic ./... + @$(GO) tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + +test-unit: ## Run unit tests only (exclude integration tests) + @echo "Running unit tests..." + @$(GO) test -v -short ./... + +test-integration: ## Run integration tests + @echo "Running integration tests..." + @$(GO) test -v ./tests/integration_tests/... + +lint: ## Run golangci-lint + @echo "Running linters..." + @golangci-lint run --config=.golangci.yml + +lint-fix: ## Run golangci-lint with auto-fix + @echo "Running linters with auto-fix..." + @golangci-lint run --config=.golangci.yml --fix + +fmt: ## Run gofmt + @echo "Formatting code..." + @gofmt -s -w . + @$(GO) fmt ./... + +vet: ## Run go vet + @echo "Running go vet..." + @$(GO) vet ./... + +install-deps: ## Install development dependencies + @echo "Installing dependencies..." + @$(GO) mod download + @$(GO) mod tidy + @echo "Installing golangci-lint..." + @which golangci-lint || (go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) + @echo "Installing pre-commit..." + @which pre-commit || (brew install pre-commit || pip install pre-commit) + +pre-commit-install: ## Install pre-commit hooks + @echo "Installing pre-commit hooks..." + @pre-commit install + +pre-commit-run: ## Run pre-commit hooks manually + @echo "Running pre-commit hooks..." + @pre-commit run --all-files + +mod-tidy: ## Tidy go.mod + @echo "Tidying go.mod..." + @$(GO) mod tidy + +mod-verify: ## Verify dependencies + @echo "Verifying dependencies..." + @$(GO) mod verify + +sandbox-start: ## Start sandbox server (agent-infra/sandbox) + @echo "Starting sandbox server on port $(SANDBOX_PORT)..." + @docker run --security-opt seccomp=unconfined --rm -it -p $(SANDBOX_PORT):8080 $(DOCKER_REGISTRY):latest + +sandbox-start-cn: ## Start sandbox server for China users + @echo "Starting sandbox server (China) on port $(SANDBOX_PORT)..." + @docker run --security-opt seccomp=unconfined --rm -it -p $(SANDBOX_PORT):8080 enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest + +clean: ## Clean build artifacts + @echo "Cleaning..." + @rm -f $(BINARY_NAME) + @rm -f coverage.out coverage.html + @rm -rf logs/*.log + +all: fmt vet lint test ## Run fmt, vet, lint and test + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index bda42489..0ba1954b 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,78 @@ ## Introduction Dify-Sandbox offers a simple way to run untrusted code in a secure environment. It is designed to be used in a multi-tenant environment, where multiple users can submit code to be executed. The code is executed in a sandboxed environment, which restricts the resources and system calls that the code can access. +## Features +- **Multi-backend Support**: Supports both native Linux sandbox (chroot + seccomp) and agent-infra/sandbox for cross-platform isolation +- **Cross-platform**: When using agent-infra/sandbox backend, works on macOS, Linux, and Windows +- **Multiple Languages**: Supports Python 3 and Node.js code execution +- **Secure Isolation**: Hardware or OS-level isolation for secure code execution +- **Flexible Configuration**: Easy configuration via YAML file + ## Use -### Requirements -DifySandbox currently only supports Linux, as it's designed for docker containers. It requires the following dependencies: + +### Native Backend (Linux Only) +The native backend uses Linux chroot and seccomp for isolation. + +#### Requirements +- Linux operating system - libseccomp - pkg-config - gcc -- golang 1.20.6 +- golang 1.25.4 or higher -### Steps +#### Steps 1. Clone the repository using `git clone https://github.com/langgenius/dify-sandbox` and navigate to the project directory. -2. Run ./install.sh to install the necessary dependencies. -3. Run ./build/build_[amd64|arm64].sh to build the sandbox binary. -4. Run ./main to start the server. +2. Run `./install.sh` to install the necessary dependencies. +3. Run `./build/build_[amd64|arm64].sh` to build the sandbox binary. +4. Edit `conf/config.yaml` and set `sandbox_backend: "native"` (default). +5. Run `./main` to start the server. + +### agent-infra/sandbox Backend (Cross-platform) +The sandbox backend uses [agent-infra/sandbox](https://github.com/agent-infra/sandbox) for cross-platform isolation. + +#### Requirements +- Docker +- golang 1.25.4 or higher + +#### Installation +Run the sandbox server using Docker: + +**Default (global):** +```bash +docker run --security-opt seccomp=unconfined --rm -it -p 10000:8080 ghcr.io/agent-infra/sandbox:latest +``` + +**For users in mainland China:** +```bash +docker run --security-opt seccomp=unconfined --rm -it -p 10000:8080 enterprise-public-cn-beijing.cr.volces.com/vefaas-public/all-in-one-sandbox:latest +``` + +**Use a specific version** (format: `1.0.0.${version}`): +```bash +docker run --security-opt seccomp=unconfined --rm -it -p 10000:8080 ghcr.io/agent-infra/sandbox:1.0.0.150 +``` + +Note: The command maps port 8080 in the container to port 10000 on the host to match the default configuration. + +#### Configuration +Edit `conf/config.yaml`: +```yaml +# Use sandbox backend +sandbox_backend: "microsandbox" + +microsandbox: + enabled: true + server_address: "http://127.0.0.1:10000" # sandbox server address +``` + +#### Steps +1. Clone the repository: `git clone https://github.com/langgenius/dify-sandbox` +2. Navigate to the project directory +3. Build the Go binary: `go build -o main ./cmd/server` +4. Configure `conf/config.yaml` with sandbox settings +5. Run `./main` to start the server +### Debugging If you want to debug the server, firstly use build script to build the sandbox library binaries, then debug as you want with your IDE. @@ -25,4 +83,4 @@ Refer to the [FAQ document](FAQ.md) ## Workflow -![workflow](workflow.png) \ No newline at end of file +![workflow](workflow.png) diff --git a/cmd/lib/nodejs/main.go b/cmd/lib/nodejs/main.go index e82afc4a..455da84b 100644 --- a/cmd/lib/nodejs/main.go +++ b/cmd/lib/nodejs/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import "github.com/langgenius/dify-sandbox/internal/core/lib/nodejs" diff --git a/cmd/lib/python/main.go b/cmd/lib/python/main.go index 014b6bba..b42baf35 100644 --- a/cmd/lib/python/main.go +++ b/cmd/lib/python/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/cmd/test/fuzz_nodejs/main.go b/cmd/test/fuzz_nodejs/main.go index 1f670c33..b11a98b3 100644 --- a/cmd/test/fuzz_nodejs/main.go +++ b/cmd/test/fuzz_nodejs/main.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package main import ( diff --git a/cmd/test/fuzz_nodejs_amd64/main.go b/cmd/test/fuzz_nodejs_amd64/main.go index fd649ac8..c7ca9e7e 100644 --- a/cmd/test/fuzz_nodejs_amd64/main.go +++ b/cmd/test/fuzz_nodejs_amd64/main.go @@ -1,3 +1,5 @@ +//go:build linux && amd64 + package main import ( diff --git a/cmd/test/fuzz_python/main.go b/cmd/test/fuzz_python/main.go index ee752b5b..d752d4dc 100644 --- a/cmd/test/fuzz_python/main.go +++ b/cmd/test/fuzz_python/main.go @@ -1,3 +1,6 @@ +//go:build linux +// +build linux + package main import ( diff --git a/cmd/test/fuzz_python_amd64/main.go b/cmd/test/fuzz_python_amd64/main.go index 93b8f300..71e45e40 100644 --- a/cmd/test/fuzz_python_amd64/main.go +++ b/cmd/test/fuzz_python_amd64/main.go @@ -1,3 +1,5 @@ +//go:build linux && amd64 + package main import ( diff --git a/cmd/test/permission/main.go b/cmd/test/permission/main.go index c23c58e3..1b59c435 100644 --- a/cmd/test/permission/main.go +++ b/cmd/test/permission/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/cmd/test/python/main.go b/cmd/test/python/main.go index 8eefa320..4eda08d2 100644 --- a/cmd/test/python/main.go +++ b/cmd/test/python/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/cmd/test/syscall_dig/main.go b/cmd/test/syscall_dig/main.go index fc260385..f37a06b7 100644 --- a/cmd/test/syscall_dig/main.go +++ b/cmd/test/syscall_dig/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/cmd/test/tmp/main.go b/cmd/test/tmp/main.go index 58619bc8..47f2a575 100644 --- a/cmd/test/tmp/main.go +++ b/cmd/test/tmp/main.go @@ -1,3 +1,5 @@ +//go:build linux + package main import ( diff --git a/conf/config.yaml b/conf/config.yaml index cc6aa6fb..340d288a 100644 --- a/conf/config.yaml +++ b/conf/config.yaml @@ -13,3 +13,15 @@ proxy: socks5: '' http: '' https: '' + +# Sandbox backend configuration +# Available backends: "native" (default) or "microsandbox" +# - native: Uses Linux chroot + seccomp for isolation (Linux only) +# - microsandbox: Uses microsandbox for cross-platform isolation +sandbox_backend: "microsandbox" + +# Microsandbox configuration (only used when sandbox_backend is "microsandbox") +microsandbox: + enabled: true # Set to true to enable microsandbox backend + server_address: "http://127.0.0.1:10000" # Optional: microsandbox server address (if using remote server) + diff --git a/go.mod b/go.mod index cd746719..178e7e78 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/langgenius/dify-sandbox -go 1.24.11 +go 1.25.4 require ( + github.com/agent-infra/sandbox-sdk-go v0.0.2 github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 + github.com/microsandbox/microsandbox/sdk/go v0.0.0-20260105133323-6f13543feae0 github.com/seccomp/libseccomp-golang v0.11.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 2f7a3bc1..16d165c8 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/agent-infra/sandbox-sdk-go v0.0.2 h1:excmpAxpVup0oQG3NyBBV1ZVMSODyS04Arm6fyo5NYs= +github.com/agent-infra/sandbox-sdk-go v0.0.2/go.mod h1:LcR1ZvwCSafPmNj1+RA6myQRMHWs1DZL7HoPMFJGSDo= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -40,6 +42,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microsandbox/microsandbox/sdk/go v0.0.0-20260105133323-6f13543feae0 h1:I/t88dcssSrsh3F/BED9rjeS0mlRf+lleg8g3szuIJg= +github.com/microsandbox/microsandbox/sdk/go v0.0.0-20260105133323-6f13543feae0/go.mod h1:Dr+SHwunRAGqJgTrqraUD8kqgv9+hi51d5Rly/V/ljs= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/internal/controller/run.go b/internal/controller/run.go index 42087569..4dc3979b 100644 --- a/internal/controller/run.go +++ b/internal/controller/run.go @@ -16,11 +16,11 @@ func RunSandboxController(c *gin.Context) { }) { switch req.Language { case "python3": - c.JSON(200, service.RunPython3Code(req.Code, req.Preload, &runner_types.RunnerOptions{ + c.JSON(200, service.RunPython3Code(c, req.Code, req.Preload, &runner_types.RunnerOptions{ EnableNetwork: req.EnableNetwork, })) case "nodejs": - c.JSON(200, service.RunNodeJsCode(req.Code, req.Preload, &runner_types.RunnerOptions{ + c.JSON(200, service.RunNodeJsCode(c, req.Code, req.Preload, &runner_types.RunnerOptions{ EnableNetwork: req.EnableNetwork, })) default: diff --git a/internal/core/lib/nodejs/add_seccomp.go b/internal/core/lib/nodejs/add_seccomp.go index 401a6ca2..ebcec677 100644 --- a/internal/core/lib/nodejs/add_seccomp.go +++ b/internal/core/lib/nodejs/add_seccomp.go @@ -1,3 +1,5 @@ +//go:build linux + package nodejs import ( diff --git a/internal/core/lib/python/add_seccomp.go b/internal/core/lib/python/add_seccomp.go index d06a62a4..6dc36243 100644 --- a/internal/core/lib/python/add_seccomp.go +++ b/internal/core/lib/python/add_seccomp.go @@ -1,3 +1,5 @@ +//go:build linux + package python import ( diff --git a/internal/core/lib/seccomp.go b/internal/core/lib/seccomp.go index c4c982d5..21546d35 100644 --- a/internal/core/lib/seccomp.go +++ b/internal/core/lib/seccomp.go @@ -1,3 +1,5 @@ +//go:build linux + package lib import ( diff --git a/internal/core/lib/set_no_new_privs.go b/internal/core/lib/set_no_new_privs.go index 2ad372e5..b3f49273 100644 --- a/internal/core/lib/set_no_new_privs.go +++ b/internal/core/lib/set_no_new_privs.go @@ -1,3 +1,5 @@ +//go:build linux + package lib import ( diff --git a/internal/core/runner/nodejs/nodejs.go b/internal/core/runner/nodejs/nodejs.go index c3ad62c4..e620739f 100644 --- a/internal/core/runner/nodejs/nodejs.go +++ b/internal/core/runner/nodejs/nodejs.go @@ -1,6 +1,9 @@ +//go:build linux + package nodejs import ( + "context" _ "embed" "encoding/base64" "fmt" @@ -36,6 +39,7 @@ var ( ) func (p *NodeJsRunner) Run( + ctx context.Context, code string, timeout time.Duration, stdin []byte, diff --git a/internal/core/runner/nodejs/nodejs_stub.go b/internal/core/runner/nodejs/nodejs_stub.go new file mode 100644 index 00000000..7770befa --- /dev/null +++ b/internal/core/runner/nodejs/nodejs_stub.go @@ -0,0 +1,33 @@ +//go:build !linux + +package nodejs + +import ( + "context" + "time" + + "github.com/langgenius/dify-sandbox/internal/core/runner" + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +type NodeJsRunner struct { + runner.TempDirRunner +} + +func (p *NodeJsRunner) Run( + ctx context.Context, + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *types.RunnerOptions, +) (chan []byte, chan []byte, chan bool, error) { + log.Error("Node.js native runner is only supported on Linux. Please configure sandbox_backend to 'microsandbox' in config.yaml") + return nil, nil, nil, nil +} + +func (p *NodeJsRunner) InitializeEnvironment(code string, preload string, root_path string) (string, error) { + log.Error("Node.js native runner is only supported on Linux. Please configure sandbox_backend to 'microsandbox' in config.yaml") + return "", nil +} diff --git a/internal/core/runner/nodejs/setup.go b/internal/core/runner/nodejs/setup.go index abb49ce2..c83d350b 100644 --- a/internal/core/runner/nodejs/setup.go +++ b/internal/core/runner/nodejs/setup.go @@ -1,3 +1,5 @@ +//go:build linux + package nodejs import ( diff --git a/internal/core/runner/nodejs/setup_stub.go b/internal/core/runner/nodejs/setup_stub.go new file mode 100644 index 00000000..9d020831 --- /dev/null +++ b/internal/core/runner/nodejs/setup_stub.go @@ -0,0 +1,25 @@ +//go:build !linux + +package nodejs + +import ( + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +const ( + LIB_PATH = "/var/sandbox/sandbox-nodejs" + LIB_NAME = "nodejs.so" + PROJECT_NAME = "nodejs-project" +) + +func init() { + log.Warn("Node.js native runner is only supported on Linux. Please use microsandbox backend on this platform.") +} + +func releaseLibBinary() { + log.Warn("Cannot release Node.js lib binary on non-Linux platform") +} + +func checkLibAvaliable() bool { + return false +} diff --git a/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox.go b/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox.go new file mode 100644 index 00000000..4f32389e --- /dev/null +++ b/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox.go @@ -0,0 +1,71 @@ +package nodejs_microsandbox + +import ( + "context" + "time" + + api "github.com/agent-infra/sandbox-sdk-go" + "github.com/agent-infra/sandbox-sdk-go/client" + "github.com/agent-infra/sandbox-sdk-go/option" + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/static" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +type NodeJSMicroSandboxRunner struct { + client *client.Client +} + +func (n *NodeJSMicroSandboxRunner) Run( + ctx context.Context, + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *types.RunnerOptions, +) (chan []byte, chan []byte, chan bool, error) { + config := static.GetDifySandboxGlobalConfigurations() + + if n.client == nil { + n.client = client.NewClient( + option.WithBaseURL(config.Sandbox.ServerAddress), + ) + } + + // microseconds := int(timeout.Microseconds()) + res, err := n.client.Code.ExecuteCode(ctx, &api.CodeExecuteRequest{ + Language: api.LanguageJavascript, + Code: code, + //Timeout: µseconds, + }) + log.Info("nodejs sandbox response is %+v", res) + // Prepare channels + stdoutChan := make(chan []byte, 100) + stderrChan := make(chan []byte, 100) + + if err != nil { + stderrChan <- []byte(err.Error()) + } else { + success := res.GetSuccess() + if !*success { + var errMessage string + for _, item := range res.Data.Outputs { + for k, v := range item { + if k == "evalue" { + if innerV, ok := v.(string); ok { + errMessage += innerV + } + } + } + } + stderrChan <- []byte(errMessage) + } else { + stdoutChan <- []byte(*res.Data.GetStdout()) + } + } + + doneChan := make(chan bool, 1) + doneChan <- true + + return stdoutChan, stderrChan, doneChan, nil +} diff --git a/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox_test.go b/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox_test.go new file mode 100644 index 00000000..a389e4e6 --- /dev/null +++ b/internal/core/runner/nodejs_microsandbox/nodejs_microsandbox_test.go @@ -0,0 +1,381 @@ +package nodejs_microsandbox + +import ( + "context" + "os" + "testing" + "time" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" +) + +// TestNodeJSMicroSandboxRunner_NewRunner tests the creation of a new runner +func TestNodeJSMicroSandboxRunner_NewRunner(t *testing.T) { + runner := &NodeJSMicroSandboxRunner{} + if runner == nil { + t.Fatal("Failed to create NodeJSMicroSandboxRunner") + } +} + +func skipIfNoMsbAPIKey(t *testing.T) { + if os.Getenv("MSB_API_KEY") == "" { + t.Skip("Skipping microsandbox test: MSB_API_KEY not set") + } +} + +// TestNodeJSMicroSandboxRunner_Run_ValidCode tests running valid Node.js code +func TestNodeJSMicroSandboxRunner_Run_ValidCode(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := `console.log("Hello, World!");` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr, stderrStr string + for out := range stdout { + stdoutStr += string(out) + } + for err := range stderr { + stderrStr += string(err) + } + + // Check if output contains expected result (if microsandbox is available) + if stdoutStr == "" && stderrStr == "" { + t.Skip("Microsandbox not available, skipping test") + } + + if stderrStr != "" && stdoutStr == "" { + t.Logf("Code execution failed (may be expected if microsandbox not installed): %s", stderrStr) + } +} + +// TestNodeJSMicroSandboxRunner_Run_WithNetwork tests running code with network enabled +func TestNodeJSMicroSandboxRunner_Run_WithNetwork(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := `console.log("Network test");` + options := &types.RunnerOptions{ + EnableNetwork: true, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain channels + close(stdout) + close(stderr) + for range stdout { + } + for range stderr { + } +} + +// TestNodeJSMicroSandboxRunner_Run_WithPreload tests running code with preload +func TestNodeJSMicroSandboxRunner_WithPreload(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + preload := ` +const helper = function(x) { + return Math.sqrt(x); +}; +` + code := `console.log(helper(16));` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, preload, options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr string + for out := range stdout { + stdoutStr += string(out) + } + for range stderr { + } + + // Check if output contains expected result (if microsandbox is available) + if stdoutStr == "" { + t.Skip("Microsandbox not available, skipping test") + } +} + +// TestNodeJSMicroSandboxRunner_Run_AsyncCode tests running async JavaScript code +func TestNodeJSMicroSandboxRunner_Run_AsyncCode(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := ` +setTimeout(() => { + console.log("Async output"); +}, 100); +console.log("Sync output"); +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr string + for out := range stdout { + stdoutStr += string(out) + } + for range stderr { + } + + t.Logf("Output: %s", stdoutStr) +} + +// TestNodeJSMicroSandboxRunner_Run_Timeout tests execution timeout +func TestNodeJSMicroSandboxRunner_Run_Timeout(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := ` +const startTime = Date.now(); +while (Date.now() - startTime < 10000) { + // Busy wait for 10 seconds +} +console.log("This should not print"); +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 2*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stderrStr string + for range stdout { + } + for err := range stderr { + stderrStr += string(err) + } + + if stderrStr != "" { + t.Logf("Got expected error: %s", stderrStr) + } +} + +// TestNodeJSMicroSandboxRunner_Run_SyntaxError tests handling of syntax errors +func TestNodeJSMicroSandboxRunner_Run_SyntaxError(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := `console.log("unclosed string` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stderrStr string + for range stdout { + } + for err := range stderr { + stderrStr += string(err) + } + + // If microsandbox is available, should get error output + if stderrStr == "" { + t.Skip("Microsandbox not available, skipping test") + } + + t.Logf("Got expected error: %s", stderrStr) +} + +// TestNodeJSMicroSandboxRunner_Run_RuntimeError tests handling of runtime errors +func TestNodeJSMicroSandboxRunner_Run_RuntimeError(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := ` +throw new Error("Intentional error"); +console.log("This should not print"); +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stderrStr string + for range stdout { + } + for err := range stderr { + stderrStr += string(err) + } + + t.Logf("Error output: %s", stderrStr) +} + +// TestNodeJSMicroSandboxRunner_Run_Stdin tests code that reads from stdin +func TestNodeJSMicroSandboxRunner_Run_Stdin(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := ` +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (data) => { + console.log('Received:', data); +}); +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdin := []byte("test input") + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, stdin, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain channels + close(stdout) + close(stderr) + for range stdout { + } + for range stderr { + } +} + +// TestNodeJSMicroSandboxRunner_Run_ComplexCode tests running more complex JavaScript code +func TestNodeJSMicroSandboxRunner_Run_ComplexCode(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &NodeJSMicroSandboxRunner{} + code := ` +// Test various JavaScript features +const arr = [1, 2, 3, 4, 5]; +const sum = arr.reduce((a, b) => a + b, 0); +console.log("Sum:", sum); + +const obj = { name: "test", value: 42 }; +console.log("Object:", JSON.stringify(obj)); + +// Test arrow functions +const double = x => x * 2; +console.log("Double 5:", double(5)); + +// Test promises +Promise.resolve(42).then(val => console.log("Promise value:", val)); +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr string + for out := range stdout { + stdoutStr += string(out) + } + for range stderr { + } + + t.Logf("Complex code output: %s", stdoutStr) +} diff --git a/internal/core/runner/python/env.go b/internal/core/runner/python/env.go index 8243caab..4c1fbb05 100644 --- a/internal/core/runner/python/env.go +++ b/internal/core/runner/python/env.go @@ -1,3 +1,5 @@ +//go:build linux + package python import ( diff --git a/internal/core/runner/python/python.go b/internal/core/runner/python/python.go index d309fa4a..161e78ca 100644 --- a/internal/core/runner/python/python.go +++ b/internal/core/runner/python/python.go @@ -1,6 +1,9 @@ +//go:build linux + package python import ( + "context" "crypto/rand" _ "embed" "encoding/base64" @@ -26,6 +29,7 @@ type PythonRunner struct { var sandbox_fs []byte func (p *PythonRunner) Run( + ctx context.Context, code string, timeout time.Duration, stdin []byte, diff --git a/internal/core/runner/python/python_stub.go b/internal/core/runner/python/python_stub.go new file mode 100644 index 00000000..e5b1e17e --- /dev/null +++ b/internal/core/runner/python/python_stub.go @@ -0,0 +1,33 @@ +//go:build !linux + +package python + +import ( + "context" + "time" + + "github.com/langgenius/dify-sandbox/internal/core/runner" + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +type PythonRunner struct { + runner.TempDirRunner +} + +func (p *PythonRunner) Run( + ctx context.Context, + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *types.RunnerOptions, +) (chan []byte, chan []byte, chan bool, error) { + log.Error("Python native runner is only supported on Linux. Please configure sandbox_backend to 'microsandbox' in config.yaml") + return nil, nil, nil, nil +} + +func (p *PythonRunner) InitializeEnvironment(code string, preload string, options *types.RunnerOptions) (string, string, error) { + log.Error("Python native runner is only supported on Linux. Please configure sandbox_backend to 'microsandbox' in config.yaml") + return "", "", nil +} diff --git a/internal/core/runner/python/setup.go b/internal/core/runner/python/setup.go index f59e866e..73eab67f 100644 --- a/internal/core/runner/python/setup.go +++ b/internal/core/runner/python/setup.go @@ -1,3 +1,5 @@ +//go:build linux + package python import ( diff --git a/internal/core/runner/python/setup_stub.go b/internal/core/runner/python/setup_stub.go new file mode 100644 index 00000000..95796aaa --- /dev/null +++ b/internal/core/runner/python/setup_stub.go @@ -0,0 +1,47 @@ +//go:build !linux + +package python + +import ( + "fmt" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +const ( + LIB_PATH = "/var/sandbox/sandbox-python" + LIB_NAME = "python.so" +) + +func init() { + log.Warn("Python native runner is only supported on Linux. Please use microsandbox backend on this platform.") +} + +func releaseLibBinary(force_remove_old_lib bool) { + log.Warn("Cannot release Python lib binary on non-Linux platform") +} + +func checkLibAvaliable() bool { + return false +} + +func InstallDependencies(requirements string) error { + log.Warn("Cannot install Python dependencies on non-Linux platform") + return nil +} + +func ListDependencies() []types.Dependency { + log.Warn("Cannot list Python dependencies on non-Linux platform") + return []types.Dependency{} +} + +func RefreshDependencies() []types.Dependency { + log.Warn("Cannot refresh Python dependencies on non-Linux platform") + return []types.Dependency{} +} + +func PreparePythonDependenciesEnv() error { + log.Warn("Cannot prepare Python dependencies environment on non-Linux platform") + return fmt.Errorf("Python native runner is only supported on Linux. Please use microsandbox backend") +} diff --git a/internal/core/runner/python_microsandbox/python_microsandbox.go b/internal/core/runner/python_microsandbox/python_microsandbox.go new file mode 100644 index 00000000..035e92da --- /dev/null +++ b/internal/core/runner/python_microsandbox/python_microsandbox.go @@ -0,0 +1,72 @@ +package python_microsandbox + +import ( + "context" + "time" + + api "github.com/agent-infra/sandbox-sdk-go" + "github.com/agent-infra/sandbox-sdk-go/client" + "github.com/agent-infra/sandbox-sdk-go/option" + "github.com/langgenius/dify-sandbox/internal/core/runner/types" + "github.com/langgenius/dify-sandbox/internal/static" + "github.com/langgenius/dify-sandbox/internal/utils/log" +) + +type PythonMicroSandboxRunner struct { + client *client.Client +} + +func (n *PythonMicroSandboxRunner) Run( + ctx context.Context, + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *types.RunnerOptions, +) (chan []byte, chan []byte, chan bool, error) { + config := static.GetDifySandboxGlobalConfigurations() + + if n.client == nil { + n.client = client.NewClient( + option.WithBaseURL(config.Sandbox.ServerAddress), + ) + } + + // microseconds := int(timeout.Microseconds()) + + res, err := n.client.Code.ExecuteCode(ctx, &api.CodeExecuteRequest{ + Language: api.LanguagePython, + Code: code, + }) + + log.Info("python sandbox response is %+v", res) + // Prepare channels + stdoutChan := make(chan []byte, 100) + stderrChan := make(chan []byte, 100) + + if err != nil { + stderrChan <- []byte(err.Error()) + } else { + success := res.GetSuccess() + if !*success { + var errMessage string + for _, item := range res.Data.Outputs { + for k, v := range item { + if k == "evalue" { + if innerV, ok := v.(string); ok { + errMessage += innerV + } + } + } + } + stderrChan <- []byte(errMessage) + } else { + stdoutChan <- []byte(*res.Data.GetStdout()) + } + } + + doneChan := make(chan bool, 1) + doneChan <- true + + return stdoutChan, stderrChan, doneChan, nil +} diff --git a/internal/core/runner/python_microsandbox/python_microsandbox_test.go b/internal/core/runner/python_microsandbox/python_microsandbox_test.go new file mode 100644 index 00000000..2e71eed3 --- /dev/null +++ b/internal/core/runner/python_microsandbox/python_microsandbox_test.go @@ -0,0 +1,217 @@ +package python_microsandbox + +import ( + "context" + "os" + "testing" + "time" + + "github.com/langgenius/dify-sandbox/internal/core/runner/types" +) + +// TestPythonMicroSandboxRunner_NewRunner tests the creation of a new runner +func TestPythonMicroSandboxRunner_NewRunner(t *testing.T) { + runner := &PythonMicroSandboxRunner{} + if runner == nil { + t.Fatal("Failed to create PythonMicroSandboxRunner") + } +} + +func skipIfNoMsbAPIKey(t *testing.T) { + if os.Getenv("MSB_API_KEY") == "" { + t.Skip("Skipping microsandbox test: MSB_API_KEY not set") + } +} + +// TestPythonMicroSandboxRunner_Run_ValidCode tests running valid Python code +func TestPythonMicroSandboxRunner_Run_ValidCode(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &PythonMicroSandboxRunner{} + code := `print("Hello, World!")` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr, stderrStr string + for out := range stdout { + stdoutStr += string(out) + } + for err := range stderr { + stderrStr += string(err) + } + + // Check if output contains expected result (if microsandbox is available) + if stdoutStr == "" && stderrStr == "" { + t.Skip("Microsandbox not available, skipping test") + } + + if stderrStr != "" && stdoutStr == "" { + t.Logf("Code execution failed (may be expected if microsandbox not installed): %s", stderrStr) + } +} + +// TestPythonMicroSandboxRunner_Run_WithNetwork tests running code with network enabled +func TestPythonMicroSandboxRunner_Run_WithNetwork(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &PythonMicroSandboxRunner{} + code := `print("Network test")` + options := &types.RunnerOptions{ + EnableNetwork: true, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain channels + close(stdout) + close(stderr) + for range stdout { + } + for range stderr { + } +} + +// TestPythonMicroSandboxRunner_Run_WithPreload tests running code with preload +func TestPythonMicroSandboxRunner_WithPreload(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &PythonMicroSandboxRunner{} + preload := ` +import math +def helper(x): + return math.sqrt(x) +` + code := `print(helper(16))` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, preload, options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stdoutStr string + for out := range stdout { + stdoutStr += string(out) + } + for range stderr { + } + + // Check if output contains expected result (if microsandbox is available) + if stdoutStr == "" { + t.Skip("Microsandbox not available, skipping test") + } +} + +// TestPythonMicroSandboxRunner_Run_Timeout tests execution timeout +func TestPythonMicroSandboxRunner_Run_Timeout(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &PythonMicroSandboxRunner{} + code := ` +import time +time.sleep(10) +print("This should not print") +` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 2*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stderrStr string + for range stdout { + } + for err := range stderr { + stderrStr += string(err) + } + + if stderrStr != "" { + t.Logf("Got expected error: %s", stderrStr) + } +} + +// TestPythonMicroSandboxRunner_Run_SyntaxError tests handling of syntax errors +func TestPythonMicroSandboxRunner_Run_SyntaxError(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + skipIfNoMsbAPIKey(t) + + runner := &PythonMicroSandboxRunner{} + code := `print("unclosed string` + options := &types.RunnerOptions{ + EnableNetwork: false, + } + + stdout, stderr, done, err := runner.Run(context.TODO(), code, 5*time.Second, nil, "", options) + if err != nil { + t.Fatalf("Failed to run code: %v", err) + } + + // Wait for completion + <-done + + // Drain all output from channels + close(stdout) + close(stderr) + var stderrStr string + for range stdout { + } + for err := range stderr { + stderrStr += string(err) + } + + // If microsandbox is available, should get error output + if stderrStr == "" { + t.Skip("Microsandbox not available, skipping test") + } + + t.Logf("Got expected error: %s", stderrStr) +} diff --git a/internal/core/runner/types/runner_interface.go b/internal/core/runner/types/runner_interface.go new file mode 100644 index 00000000..8e1471ee --- /dev/null +++ b/internal/core/runner/types/runner_interface.go @@ -0,0 +1,21 @@ +package types + +import ( + "context" + "time" +) + +// CodeRunner defines the interface for code execution runners +// Both native and microsandbox runners implement this interface +type CodeRunner interface { + // Run executes code with the given parameters + // Returns channels for stdout, stderr, and done signal, along with any error + Run( + ctx context.Context, + code string, + timeout time.Duration, + stdin []byte, + preload string, + options *RunnerOptions, + ) (chan []byte, chan []byte, chan bool, error) +} diff --git a/internal/server/server.go b/internal/server/server.go index cfe3f2bb..94c1537e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -46,18 +46,23 @@ func initServer() { func initDependencies() { log.Info("installing python dependencies...") dependencies := static.GetRunnerDependencies() - err := python.InstallDependencies(dependencies.PythonRequirements) - if err != nil { - log.Panic("failed to install python dependencies: %v", err) - } - log.Info("python dependencies installed") - log.Info("initializing python dependencies sandbox...") - err = python.PreparePythonDependenciesEnv() - if err != nil { - log.Panic("failed to initialize python dependencies sandbox: %v", err) + config := static.GetDifySandboxGlobalConfigurations() + + if !config.Sandbox.Enabled { + err := python.InstallDependencies(dependencies.PythonRequirements) + if err != nil { + log.Panic("failed to install python dependencies: %v", err) + } + log.Info("python dependencies installed") + + log.Info("initializing python dependencies sandbox...") + err = python.PreparePythonDependenciesEnv() + if err != nil { + log.Panic("failed to initialize python dependencies sandbox: %v", err) + } + log.Info("python dependencies sandbox initialized") } - log.Info("python dependencies sandbox initialized") // start a ticker to update python dependencies to keep the sandbox up-to-date go func() { @@ -69,7 +74,7 @@ func initDependencies() { } ticker := time.NewTicker(tickerDuration) for range ticker.C { - if err:=updatePythonDependencies(dependencies);err!=nil{ + if err := updatePythonDependencies(dependencies); err != nil { log.Error("Failed to update Python dependencies: %v", err) } } diff --git a/internal/service/nodejs.go b/internal/service/nodejs.go index f7229504..64f92c0c 100644 --- a/internal/service/nodejs.go +++ b/internal/service/nodejs.go @@ -1,30 +1,44 @@ package service import ( + "context" "time" "github.com/langgenius/dify-sandbox/internal/core/runner/nodejs" + "github.com/langgenius/dify-sandbox/internal/core/runner/nodejs_microsandbox" runner_types "github.com/langgenius/dify-sandbox/internal/core/runner/types" "github.com/langgenius/dify-sandbox/internal/static" "github.com/langgenius/dify-sandbox/internal/types" ) -func RunNodeJsCode(code string, preload string, options *runner_types.RunnerOptions) *types.DifySandboxResponse { +func RunNodeJsCode( + ctx context.Context, + code string, + preload string, + options *runner_types.RunnerOptions) *types.DifySandboxResponse { if err := checkOptions(options); err != nil { return types.ErrorResponse(-400, err.Error()) } - if !static.GetDifySandboxGlobalConfigurations().EnablePreload { - preload = "" + preload = "" } - + timeout := time.Duration( static.GetDifySandboxGlobalConfigurations().WorkerTimeout * int(time.Second), ) - runner := nodejs.NodeJsRunner{} - stdout, stderr, done, err := runner.Run(code, timeout, nil, preload, options) + // Select runner based on configuration + var runner runner_types.CodeRunner + + if !static.GetDifySandboxGlobalConfigurations().Sandbox.Enabled { + // Default to native runner + runner = &nodejs.NodeJsRunner{} + } else { + runner = &nodejs_microsandbox.NodeJSMicroSandboxRunner{} + } + + stdout, stderr, done, err := runner.Run(ctx, code, timeout, nil, preload, options) if err != nil { return types.ErrorResponse(-500, err.Error()) } @@ -32,21 +46,29 @@ func RunNodeJsCode(code string, preload string, options *runner_types.RunnerOpti stdout_str := "" stderr_str := "" - defer close(done) - defer close(stdout) - defer close(stderr) - for { select { + case out := <-stdout: + stdout_str += string(out) + case err := <-stderr: + stderr_str += string(err) case <-done: + // Drain any remaining buffered output before returning + drain: + for { + select { + case out := <-stdout: + stdout_str += string(out) + case err := <-stderr: + stderr_str += string(err) + default: + break drain + } + } return types.SuccessResponse(&RunCodeResponse{ Stdout: stdout_str, Stderr: stderr_str, }) - case out := <-stdout: - stdout_str += string(out) - case err := <-stderr: - stderr_str += string(err) } } } diff --git a/internal/service/python.go b/internal/service/python.go index f9036c83..c6e8850f 100644 --- a/internal/service/python.go +++ b/internal/service/python.go @@ -1,9 +1,11 @@ package service import ( + "context" "time" "github.com/langgenius/dify-sandbox/internal/core/runner/python" + "github.com/langgenius/dify-sandbox/internal/core/runner/python_microsandbox" runner_types "github.com/langgenius/dify-sandbox/internal/core/runner/types" "github.com/langgenius/dify-sandbox/internal/static" "github.com/langgenius/dify-sandbox/internal/types" @@ -14,45 +16,68 @@ type RunCodeResponse struct { Stdout string `json:"stdout"` } -func RunPython3Code(code string, preload string, options *runner_types.RunnerOptions) *types.DifySandboxResponse { +func RunPython3Code( + ctx context.Context, + code string, + preload string, + options *runner_types.RunnerOptions) *types.DifySandboxResponse { if err := checkOptions(options); err != nil { return types.ErrorResponse(-400, err.Error()) } if !static.GetDifySandboxGlobalConfigurations().EnablePreload { - preload = "" + preload = "" } - + timeout := time.Duration( static.GetDifySandboxGlobalConfigurations().WorkerTimeout * int(time.Second), ) - runner := python.PythonRunner{} - stdout, stderr, done, err := runner.Run( + // Select runner based on configuration + var runner runner_types.CodeRunner + backend := static.GetDifySandboxGlobalConfigurations().SandboxBackend + + if backend == "microsandbox" && static.GetDifySandboxGlobalConfigurations().Sandbox.Enabled { + runner = &python_microsandbox.PythonMicroSandboxRunner{} + } else { + // Default to native runner + runner = &python.PythonRunner{} + } + + stdout, stderr, done, err := runner.Run(ctx, code, timeout, nil, preload, options, ) if err != nil { return types.ErrorResponse(-500, err.Error()) } - stdout_str := "" - stderr_str := "" - - defer close(done) - defer close(stdout) - defer close(stderr) + stdoutStr := "" + stderrStr := "" + // Read from stdout/stderr until we receive done, then drain any remaining buffered data for { select { + case out := <-stdout: + stdoutStr += string(out) + case err := <-stderr: + stderrStr += string(err) case <-done: + // Drain any remaining buffered output to avoid races where done is selected first + drain: + for { + select { + case out := <-stdout: + stdoutStr += string(out) + case err := <-stderr: + stderrStr += string(err) + default: + break drain + } + } return types.SuccessResponse(&RunCodeResponse{ - Stdout: stdout_str, - Stderr: stderr_str, + Stdout: stdoutStr, + Stderr: stderrStr, }) - case out := <-stdout: - stdout_str += string(out) - case err := <-stderr: - stderr_str += string(err) } } } @@ -62,8 +87,14 @@ type ListDependenciesResponse struct { } func ListPython3Dependencies() *types.DifySandboxResponse { + var deps []runner_types.Dependency + + if !static.GetDifySandboxGlobalConfigurations().Sandbox.Enabled { + deps = python.ListDependencies() + } + return types.SuccessResponse(&ListDependenciesResponse{ - Dependencies: python.ListDependencies(), + Dependencies: deps, }) } @@ -72,15 +103,26 @@ type RefreshDependenciesResponse struct { } func RefreshPython3Dependencies() *types.DifySandboxResponse { + var deps []runner_types.Dependency + + if !static.GetDifySandboxGlobalConfigurations().Sandbox.Enabled { + deps = python.RefreshDependencies() + } + return types.SuccessResponse(&RefreshDependenciesResponse{ - Dependencies: python.RefreshDependencies(), + Dependencies: deps, }) } type UpdateDependenciesResponse struct{} func UpdateDependencies() *types.DifySandboxResponse { - err := python.PreparePythonDependenciesEnv() + var err error + + if !static.GetDifySandboxGlobalConfigurations().Sandbox.Enabled { + err = python.PreparePythonDependenciesEnv() + } + if err != nil { return types.ErrorResponse(-500, err.Error()) } diff --git a/internal/static/config_default_stub.go b/internal/static/config_default_stub.go new file mode 100644 index 00000000..13af74ac --- /dev/null +++ b/internal/static/config_default_stub.go @@ -0,0 +1,6 @@ +//go:build !linux + +package static + +// Default Python lib paths for non-Linux platforms (not used, but needed for compilation) +var DEFAULT_PYTHON_LIB_REQUIREMENTS = []string{} diff --git a/internal/types/config.go b/internal/types/config.go index 34ac33b8..373091a9 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -22,4 +22,9 @@ type DifySandboxGlobalConfigurations struct { Https string `yaml:"https"` Http string `yaml:"http"` } `yaml:"proxy"` -} \ No newline at end of file + SandboxBackend string `yaml:"sandbox_backend"` // "native" or "microsandbox" + Sandbox struct { + Enabled bool `yaml:"enabled"` + ServerAddress string `yaml:"server_address"` // microsandbox server address + } `yaml:"microsandbox"` +} diff --git a/tests/integration_tests/nodejs_feature_test.go b/tests/integration_tests/nodejs_feature_test.go index 12aa3e62..6ad0581c 100644 --- a/tests/integration_tests/nodejs_feature_test.go +++ b/tests/integration_tests/nodejs_feature_test.go @@ -1,6 +1,7 @@ package integrationtests_test import ( + "context" "strings" "testing" @@ -26,7 +27,7 @@ var result = ` + "`<>${output_json}<>`" + ` console.log(result)` runMultipleTestings(t, 30, func(t *testing.T) { - resp := service.RunNodeJsCode(code, "", &types.RunnerOptions{ + resp := service.RunNodeJsCode(context.TODO(), code, "", &types.RunnerOptions{ EnableNetwork: true, }) if resp.Code != 0 { @@ -38,7 +39,7 @@ console.log(result)` func TestNodejsBase64(t *testing.T) { // Test case for base64 runMultipleTestings(t, 30, func(t *testing.T) { - resp := service.RunNodeJsCode(` + resp := service.RunNodeJsCode(context.TODO(), ` const base64 = Buffer.from("hello world").toString("base64"); console.log(Buffer.from(base64, "base64").toString()); `, "", &types.RunnerOptions{ @@ -61,7 +62,7 @@ console.log(Buffer.from(base64, "base64").toString()); func TestNodejsJSON(t *testing.T) { // Test case for json runMultipleTestings(t, 30, func(t *testing.T) { - resp := service.RunNodeJsCode(` + resp := service.RunNodeJsCode(context.TODO(), ` console.log(JSON.stringify({"hello": "world"})); `, "", &types.RunnerOptions{ EnableNetwork: true, diff --git a/tests/integration_tests/nodejs_malicious_test.go b/tests/integration_tests/nodejs_malicious_test.go index 7377f530..02b56892 100644 --- a/tests/integration_tests/nodejs_malicious_test.go +++ b/tests/integration_tests/nodejs_malicious_test.go @@ -1,6 +1,7 @@ package integrationtests_test import ( + "context" "strings" "testing" @@ -10,7 +11,7 @@ import ( func TestNodejsRunCommand(t *testing.T) { // Test case for run_command - resp := service.RunNodeJsCode(` + resp := service.RunNodeJsCode(context.TODO(), ` const { spawn } = require( 'child_process' ); const ls = spawn( 'ls', [ '-lh', '/usr' ] ); @@ -37,7 +38,7 @@ ls.on( 'close', ( code ) => { func TestNodejsRunRedeclareFunctionCommand(t *testing.T) { // Test case for run_command - resp := service.RunNodeJsCode(` + resp := service.RunNodeJsCode(context.TODO(), ` var data; function main() { diff --git a/tests/integration_tests/python_feature_test.go b/tests/integration_tests/python_feature_test.go index 3bc07982..e48e0ad8 100644 --- a/tests/integration_tests/python_feature_test.go +++ b/tests/integration_tests/python_feature_test.go @@ -1,6 +1,7 @@ package integrationtests_test import ( + "context" "strings" "testing" "time" @@ -12,7 +13,7 @@ import ( func TestPythonBase64(t *testing.T) { // Test case for base64 runMultipleTestings(t, 50, func(t *testing.T) { - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import base64 print(base64.b64decode(base64.b64encode(b"hello world")).decode()) `, "", &types.RunnerOptions{ @@ -35,7 +36,7 @@ print(base64.b64decode(base64.b64encode(b"hello world")).decode()) func TestPythonJSON(t *testing.T) { runMultipleTestings(t, 50, func(t *testing.T) { // Test case for json - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import json print(json.dumps({"hello": "world"})) `, "", &types.RunnerOptions{ @@ -58,7 +59,7 @@ print(json.dumps({"hello": "world"})) func TestPythonRequests(t *testing.T) { // Test case for http runMultipleTestings(t, 1, func(t *testing.T) { - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import requests print(requests.get("https://www.bilibili.com").content) `, "", &types.RunnerOptions{ @@ -81,7 +82,7 @@ print(requests.get("https://www.bilibili.com").content) func TestPythonHttpx(t *testing.T) { // Test case for http runMultipleTestings(t, 1, func(t *testing.T) { - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import httpx print(httpx.get("https://www.bilibili.com").content) `, "", &types.RunnerOptions{ @@ -104,7 +105,7 @@ print(httpx.get("https://www.bilibili.com").content) func TestPythonTimezone(t *testing.T) { // Test case for time runMultipleTestings(t, 1, func(t *testing.T) { - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` from datetime import datetime from zoneinfo import ZoneInfo diff --git a/tests/integration_tests/python_longchars_test.go b/tests/integration_tests/python_longchars_test.go index d73fb013..2f4ea021 100644 --- a/tests/integration_tests/python_longchars_test.go +++ b/tests/integration_tests/python_longchars_test.go @@ -1,6 +1,7 @@ package integrationtests_test import ( + "context" "testing" "github.com/langgenius/dify-sandbox/internal/core/runner/types" @@ -10,7 +11,7 @@ import ( func TestPythonLargeOutput(t *testing.T) { // Test case for base64 runMultipleTestings(t, 5, func(t *testing.T) { - resp := service.RunPython3Code(`# declare main function here + resp := service.RunPython3Code(context.TODO(), `# declare main function here def main() -> dict: original_strings_with_empty = ["apple", "", "cherry", "date", "", "fig", "grape", "honeydew", "kiwi", "", "mango", "nectarine", "orange", "papaya", "quince", "raspberry", "strawberry", "tangerine", "ugli fruit", "vanilla bean", "watermelon", "xigua", "yellow passionfruit", "zucchini"] * 5 diff --git a/tests/integration_tests/python_malicious_test.go b/tests/integration_tests/python_malicious_test.go index 860b96af..2d47b50c 100644 --- a/tests/integration_tests/python_malicious_test.go +++ b/tests/integration_tests/python_malicious_test.go @@ -1,6 +1,7 @@ package integrationtests_test import ( + "context" "strings" "testing" @@ -10,7 +11,7 @@ import ( func TestSysFork(t *testing.T) { // Test case for sys_fork - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import os print(os.fork()) print(123) @@ -29,7 +30,7 @@ print(123) func TestExec(t *testing.T) { // Test case for exec - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import os os.execl("/bin/ls", "ls") `, "", &types.RunnerOptions{ @@ -46,7 +47,7 @@ os.execl("/bin/ls", "ls") func TestRunCommand(t *testing.T) { // Test case for run_command - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` import subprocess subprocess.run(["ls", "-l"]) `, "", &types.RunnerOptions{ @@ -62,7 +63,7 @@ subprocess.run(["ls", "-l"]) } func TestReadEtcPasswd(t *testing.T) { - resp := service.RunPython3Code(` + resp := service.RunPython3Code(context.TODO(), ` print(open("/etc/passwd").read()) `, "", &types.RunnerOptions{ EnableNetwork: true,