From 48695cb552667059398cde40042d08c53327760d Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:48:13 +0800 Subject: [PATCH 1/8] fix(execd): use correct HTTP method in auth client Post() Post() was using http.MethodPut instead of http.MethodPost due to a copy-paste error from the Put() method. This caused all Jupyter API calls that used client.Post() to send PUT requests instead of POST. Made-with: Cursor --- components/execd/pkg/jupyter/auth/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/execd/pkg/jupyter/auth/client.go b/components/execd/pkg/jupyter/auth/client.go index d642776d2..bc617235a 100644 --- a/components/execd/pkg/jupyter/auth/client.go +++ b/components/execd/pkg/jupyter/auth/client.go @@ -60,7 +60,7 @@ func (c *Client) Get(url string) (*http.Response, error) { // Post sends a POST request. func (c *Client) Post(url, contentType string, body io.Reader) (*http.Response, error) { - req, err := http.NewRequest(http.MethodPut, url, body) + req, err := http.NewRequest(http.MethodPost, url, body) if err != nil { return nil, err } From 2d2add204472e47e7979baf760a4f102b1bbc2ca Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:48:40 +0800 Subject: [PATCH 2/8] fix(execd): prevent panic on empty SQL query and check rows.Err() getQueryType() would panic with index-out-of-range when given an empty or whitespace-only query string because strings.Fields() returns an empty slice. Add a length check before accessing the first element. Also add the missing rows.Err() check after the SELECT iteration loop, which is required to detect errors that cause rows.Next() to return false prematurely. Made-with: Cursor --- components/execd/pkg/runtime/sql.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/components/execd/pkg/runtime/sql.go b/components/execd/pkg/runtime/sql.go index 282476c58..a5aa971b2 100644 --- a/components/execd/pkg/runtime/sql.go +++ b/components/execd/pkg/runtime/sql.go @@ -103,6 +103,10 @@ func (c *Controller) executeSelectSQLQuery(ctx context.Context, request *Execute } result = append(result, row) } + if err := rows.Err(); err != nil { + request.Hooks.OnExecuteError(&execute.ErrorOutput{EName: "RowIterationError", EValue: err.Error()}) + return nil + } queryResult := QueryResult{ Columns: columns, @@ -155,8 +159,11 @@ func (c *Controller) executeUpdateSQLQuery(ctx context.Context, request *Execute // getQueryType extracts the first token to decide which executor to use. func (c *Controller) getQueryType(query string) string { - firstWord := strings.ToUpper(strings.Fields(query)[0]) - return firstWord + fields := strings.Fields(query) + if len(fields) == 0 { + return "" + } + return strings.ToUpper(fields[0]) } // initDB lazily opens the local sandbox database. From 40d9d204bfc5861ccdaedb29d21edb22856d12fb Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:49:17 +0800 Subject: [PATCH 3/8] fix(execd): fix goroutine and file descriptor leaks in runCommand When cmd.Start() fails, the done channel was never closed, causing the two tailStdPipe goroutines to block forever on <-done. Close the channel and wait for the goroutines before returning. Also defer-close stdout and stderr file descriptors returned by stdLogDescriptor(), which were previously never closed. Made-with: Cursor --- components/execd/pkg/runtime/command.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/execd/pkg/runtime/command.go b/components/execd/pkg/runtime/command.go index 63c871487..d14f9bcab 100644 --- a/components/execd/pkg/runtime/command.go +++ b/components/execd/pkg/runtime/command.go @@ -97,6 +97,8 @@ func (c *Controller) runCommand(ctx context.Context, request *ExecuteCodeRequest if err != nil { return fmt.Errorf("failed to get stdlog descriptor: %w", err) } + defer stdout.Close() + defer stderr.Close() stdoutPath := c.stdoutFileName(session) stderrPath := c.stderrFileName(session) @@ -135,6 +137,8 @@ func (c *Controller) runCommand(ctx context.Context, request *ExecuteCodeRequest err = cmd.Start() if err != nil { + close(done) + wg.Wait() request.Hooks.OnExecuteInit(session) request.Hooks.OnExecuteError(&execute.ErrorOutput{EName: "CommandExecError", EValue: err.Error()}) log.Error("CommandExecError: error starting commands: %v", err) From 6651047caf71ec39cca942458644871c99ad31b0 Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:49:39 +0800 Subject: [PATCH 4/8] fix(execd): fix stdin redirection in background commands os.NewFile(uintptr(syscall.Stdin), os.DevNull) wraps the real stdin (fd 0) with a misleading name, so background commands still read from the process stdin. Use os.Open(os.DevNull) to actually open /dev/null. Made-with: Cursor --- components/execd/pkg/runtime/command.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/execd/pkg/runtime/command.go b/components/execd/pkg/runtime/command.go index d14f9bcab..208b541ab 100644 --- a/components/execd/pkg/runtime/command.go +++ b/components/execd/pkg/runtime/command.go @@ -250,7 +250,11 @@ func (c *Controller) runBackgroundCommand(ctx context.Context, cancel context.Ca cmd.Env = mergeEnvs(os.Environ(), extraEnv) // use DevNull as stdin so interactive programs exit immediately. - cmd.Stdin = os.NewFile(uintptr(syscall.Stdin), os.DevNull) + devNull, err := os.Open(os.DevNull) + if err == nil { + cmd.Stdin = devNull + defer devNull.Close() + } err = cmd.Start() kernel := &commandKernel{ From d759b6a3b4e2f00dfb0891501d5b6b2d8f1306ba Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:50:14 +0800 Subject: [PATCH 5/8] fix(execd): exit with non-zero code on server startup failure When engine.Run() fails (e.g., port already in use), the process logged the error but exited with code 0, making it invisible to process supervisors and container orchestrators. Made-with: Cursor --- components/execd/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/execd/main.go b/components/execd/main.go index f53f198d4..e9b1d7641 100644 --- a/components/execd/main.go +++ b/components/execd/main.go @@ -16,6 +16,7 @@ package main import ( "fmt" + "os" "github.com/alibaba/opensandbox/internal/version" @@ -42,5 +43,6 @@ func main() { log.Info("execd listening on %s", addr) if err := engine.Run(addr); err != nil { log.Error("failed to start execd server: %v", err) + os.Exit(1) } } From 11e6c662961cd9f8c99ba6197ad16045519dc406 Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:50:32 +0800 Subject: [PATCH 6/8] fix(server): remove duplicate sandbox service instantiation create_sandbox_service() was called twice: once at module level in lifecycle.py (used by all routes) and once in the lifespan handler in main.py (stored in app.state but never referenced). The duplicate instance maintained separate internal state (expiration timers, OSSFS ref counts, Docker client) that was never synchronized with the one actually serving requests. Made-with: Cursor --- server/src/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/main.py b/server/src/main.py index bfdff999d..ee6a89b96 100644 --- a/server/src/main.py +++ b/server/src/main.py @@ -106,10 +106,6 @@ async def lifespan(app: FastAPI): k8s_client=k8s_client, ) - # Create sandbox service after validation - from src.services.factory import create_sandbox_service - - app.state.sandbox_service = create_sandbox_service() except Exception as exc: logger.error("Secure runtime validation failed: %s", exc) raise From bed268cbb7d453df594d351fa43a4b9cc4ccdb80 Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:50:54 +0800 Subject: [PATCH 7/8] fix(sdk): add missing execd endpoint headers in code-interpreter adapter CodesAdapter was not including self.execd_endpoint.headers when building HTTP headers, unlike CommandsAdapter in the sandbox SDK which correctly spreads them. This omission could cause requests to fail in server-proxy mode where routing headers are required. Made-with: Cursor --- .../python/src/code_interpreter/adapters/code_adapter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sdks/code-interpreter/python/src/code_interpreter/adapters/code_adapter.py b/sdks/code-interpreter/python/src/code_interpreter/adapters/code_adapter.py index 4b328c774..069563f4a 100644 --- a/sdks/code-interpreter/python/src/code_interpreter/adapters/code_adapter.py +++ b/sdks/code-interpreter/python/src/code_interpreter/adapters/code_adapter.py @@ -106,6 +106,7 @@ def __init__( headers = { "User-Agent": self.connection_config.user_agent, **self.connection_config.headers, + **self.execd_endpoint.headers, } # Execd API does not require authentication From 8d40208139eaff56034ba4c2c00535006b90f3b4 Mon Sep 17 00:00:00 2001 From: wishhyt <24300810017@m.fudan.edu.cn> Date: Tue, 17 Mar 2026 22:51:29 +0800 Subject: [PATCH 8/8] fix(sdk): add OSSFS volume backend support in model converter to_api_volume() only handled host and pvc backends, silently dropping the ossfs configuration. OSSFS volumes created by users would be sent to the API without their backend config, causing mount failures. Map all OSSFS domain fields (bucket, endpoint, credentials, version, options) to the corresponding API model. Made-with: Cursor --- .../converter/sandbox_model_converter.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py index 0f6a8c96e..6fc75af6d 100644 --- a/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py +++ b/sdks/sandbox/python/src/opensandbox/adapters/converter/sandbox_model_converter.py @@ -90,6 +90,10 @@ def to_api_volume(volume: Volume): from opensandbox.api.lifecycle.models.host import ( Host as ApiHost, ) + from opensandbox.api.lifecycle.models.ossfs import ( + OSSFS as ApiOSSFS, + ) + from opensandbox.api.lifecycle.models.ossfs_version import OSSFSVersion from opensandbox.api.lifecycle.models.pvc import ( PVC as ApiPVC, ) @@ -104,6 +108,17 @@ def to_api_volume(volume: Volume): if volume.pvc is not None: api_pvc = ApiPVC(claim_name=volume.pvc.claim_name) + api_ossfs = UNSET + if volume.ossfs is not None and volume.ossfs.access_key_id is not None and volume.ossfs.access_key_secret is not None: + api_ossfs = ApiOSSFS( + bucket=volume.ossfs.bucket, + endpoint=volume.ossfs.endpoint, + access_key_id=volume.ossfs.access_key_id, + access_key_secret=volume.ossfs.access_key_secret, + version=OSSFSVersion(volume.ossfs.version), + options=volume.ossfs.options if volume.ossfs.options is not None else UNSET, + ) + api_sub_path = UNSET if volume.sub_path is not None: api_sub_path = volume.sub_path @@ -114,6 +129,7 @@ def to_api_volume(volume: Volume): read_only=volume.read_only, host=api_host, pvc=api_pvc, + ossfs=api_ossfs, sub_path=api_sub_path, )