Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions components/execd/pkg/runtime/workingdir.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 49 additions & 0 deletions components/execd/pkg/runtime/workingdir_test.go
Original file line number Diff line number Diff line change
@@ -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()))
}
2 changes: 1 addition & 1 deletion components/execd/pkg/web/controller/pty_ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion components/execd/pkg/web/model/codeinterpreting.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions components/execd/pkg/web/model/codeinterpreting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package model

import (
"encoding/json"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion components/execd/pkg/web/model/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
}
2 changes: 1 addition & 1 deletion tests/java/src/test/resources/test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 1 addition & 2 deletions tests/javascript/tests/base_e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/python/tests/base_e2e_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading