diff --git a/components/execd/pkg/runtime/workingdir.go b/components/execd/pkg/runtime/workingdir.go new file mode 100644 index 000000000..8c6186a45 --- /dev/null +++ b/components/execd/pkg/runtime/workingdir.go @@ -0,0 +1,37 @@ +// Copyright 2026 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "fmt" + "os" +) + +func ValidateWorkingDir(cwd string) error { + if cwd == "" { + return nil + } + fi, err := os.Stat(cwd) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("working directory does not exist: %s", cwd) + } + return fmt.Errorf("cannot access working directory %q: %w", cwd, err) + } + if !fi.IsDir() { + return fmt.Errorf("working directory path is not a directory: %s", cwd) + } + return nil +} diff --git a/components/execd/pkg/runtime/workingdir_test.go b/components/execd/pkg/runtime/workingdir_test.go new file mode 100644 index 000000000..d6a1b9bd2 --- /dev/null +++ b/components/execd/pkg/runtime/workingdir_test.go @@ -0,0 +1,49 @@ +// Copyright 2026 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package runtime + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateWorkingDir_empty(t *testing.T) { + require.NoError(t, ValidateWorkingDir("")) +} + +func TestValidateWorkingDir_notExist(t *testing.T) { + tmp := t.TempDir() + missing := filepath.Join(tmp, "definitely-missing-subdir") + err := ValidateWorkingDir(missing) + require.Error(t, err) + require.Contains(t, err.Error(), "does not exist") + require.Contains(t, err.Error(), missing) +} + +func TestValidateWorkingDir_notDir(t *testing.T) { + tmp := t.TempDir() + f := filepath.Join(tmp, "file") + require.NoError(t, os.WriteFile(f, []byte("x"), 0o600)) + err := ValidateWorkingDir(f) + require.Error(t, err) + require.Contains(t, err.Error(), "not a directory") +} + +func TestValidateWorkingDir_ok(t *testing.T) { + require.NoError(t, ValidateWorkingDir(t.TempDir())) +} diff --git a/components/execd/pkg/web/controller/pty_ws.go b/components/execd/pkg/web/controller/pty_ws.go index 6273557f5..ea36b13e4 100644 --- a/components/execd/pkg/web/controller/pty_ws.go +++ b/components/execd/pkg/web/controller/pty_ws.go @@ -50,7 +50,7 @@ const ( // 2. Acquire exclusive WS lock → 409 if already held // 3. Upgrade HTTP → WebSocket // 4. Start bash if not already running -// 5+6. AtomicAttachOutputWithSnapshot (snapshot + attach under outMu — no loss window) +// 5+6. AtomicAttachOutputWithSnapshot (snapshot + attach under outMu — no loss window) // 7. defer: detach → pumpWg.Wait → UnlockWS // 8. Send replay frame if snapshot non-empty // 9. Send connected frame diff --git a/components/execd/pkg/web/model/codeinterpreting.go b/components/execd/pkg/web/model/codeinterpreting.go index 771b6d75b..c45382a17 100644 --- a/components/execd/pkg/web/model/codeinterpreting.go +++ b/components/execd/pkg/web/model/codeinterpreting.go @@ -23,6 +23,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/alibaba/opensandbox/execd/pkg/jupyter/execute" + "github.com/alibaba/opensandbox/execd/pkg/runtime" ) // RunCodeRequest represents a code execution request. @@ -68,7 +69,7 @@ func (r *RunCommandRequest) Validate() error { if r.Gid != nil && r.Uid == nil { return errors.New("uid is required when gid is provided") } - return nil + return runtime.ValidateWorkingDir(r.Cwd) } type ServerStreamEventType string diff --git a/components/execd/pkg/web/model/codeinterpreting_test.go b/components/execd/pkg/web/model/codeinterpreting_test.go index f0903bf05..f90536b9e 100644 --- a/components/execd/pkg/web/model/codeinterpreting_test.go +++ b/components/execd/pkg/web/model/codeinterpreting_test.go @@ -16,6 +16,7 @@ package model import ( "encoding/json" + "path/filepath" "strings" "testing" @@ -49,6 +50,17 @@ func TestRunCommandRequestValidate(t *testing.T) { require.Error(t, req.Validate(), "expected validation error when command is empty") } +func TestRunCommandRequestValidateCwd(t *testing.T) { + tmp := t.TempDir() + req := RunCommandRequest{Command: "ls", Cwd: tmp} + require.NoError(t, req.Validate()) + + req.Cwd = filepath.Join(tmp, "missing-subdir") + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "working directory") +} + func ptr32(v uint32) *uint32 { return &v } func TestRunCommandRequestValidateUidGid(t *testing.T) { diff --git a/components/execd/pkg/web/model/session.go b/components/execd/pkg/web/model/session.go index fed7d48c8..13b3f6fcf 100644 --- a/components/execd/pkg/web/model/session.go +++ b/components/execd/pkg/web/model/session.go @@ -16,6 +16,8 @@ package model import ( "github.com/go-playground/validator/v10" + + "github.com/alibaba/opensandbox/execd/pkg/runtime" ) // CreateSessionRequest is the request body for creating a bash session. @@ -38,5 +40,8 @@ type RunInSessionRequest struct { // Validate validates RunInSessionRequest. func (r *RunInSessionRequest) Validate() error { validate := validator.New() - return validate.Struct(r) + if err := validate.Struct(r); err != nil { + return err + } + return runtime.ValidateWorkingDir(r.Cwd) } diff --git a/tests/java/src/test/resources/test.properties b/tests/java/src/test/resources/test.properties index a782a9f15..11f2c2361 100644 --- a/tests/java/src/test/resources/test.properties +++ b/tests/java/src/test/resources/test.properties @@ -3,4 +3,4 @@ opensandbox.test.domain=localhost:8080 opensandbox.test.protocol=http opensandbox.test.api.key=e2e-test -opensandbox.sandbox.default.image=sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:latest +opensandbox.sandbox.default.image=opensandbox/code-interpreter:latest diff --git a/tests/javascript/tests/base_e2e.ts b/tests/javascript/tests/base_e2e.ts index 166ab28a9..8a267d74e 100644 --- a/tests/javascript/tests/base_e2e.ts +++ b/tests/javascript/tests/base_e2e.ts @@ -17,8 +17,7 @@ import { ConnectionConfig } from "@alibaba-group/opensandbox"; export const DEFAULT_DOMAIN = "localhost:8080"; export const DEFAULT_PROTOCOL = "http"; export const DEFAULT_API_KEY = "e2e-test"; -export const DEFAULT_IMAGE = - "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:latest"; +export const DEFAULT_IMAGE = "opensandbox/code-interpreter:latest"; export const TEST_DOMAIN = process.env.OPENSANDBOX_TEST_DOMAIN ?? DEFAULT_DOMAIN; export const TEST_PROTOCOL = process.env.OPENSANDBOX_TEST_PROTOCOL ?? DEFAULT_PROTOCOL; diff --git a/tests/python/tests/base_e2e_test.py b/tests/python/tests/base_e2e_test.py index 89ead7d47..2c0a6fccf 100644 --- a/tests/python/tests/base_e2e_test.py +++ b/tests/python/tests/base_e2e_test.py @@ -26,7 +26,7 @@ DEFAULT_DOMAIN = "localhost:8080" DEFAULT_PROTOCOL = "http" DEFAULT_API_KEY = "e2e-test" -DEFAULT_IMAGE = "sandbox-registry.cn-zhangjiakou.cr.aliyuncs.com/opensandbox/code-interpreter:latest" +DEFAULT_IMAGE = "opensandbox/code-interpreter:latest" DEFAULT_RUNTIME = "docker" DEFAULT_USE_SERVER_PROXY = "false" DEFAULT_PVC_NAME = "opensandbox-e2e-pvc-test"