Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 2.19.12 (2026-08-11)
## 新增
1. `qshell sandbox create` 与 `qshell sandbox connect` 新增 `--user` / `-u` 参数,用于指定终端的运行用户;未指定时使用沙箱默认用户,`create` 搭配 `--detach` 时该参数不生效(不连接终端)

# 2.19.11 (2026-07-30)
## 新增
1. `qshell sandbox create` 通过 SDK 请求幂等键支持安全重试;未显式指定幂等键时 SDK 会自动生成
Expand Down
16 changes: 13 additions & 3 deletions cmd/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,11 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command {

# Create with a Kodo bucket resource mounted into the sandbox
qshell sandbox create my-template \
--resource 'type=kodo,bucket=my-bucket,mount-path=/mnt/kodo,prefix=datasets/,read-only=true'`,
--resource 'type=kodo,bucket=my-bucket,mount-path=/mnt/kodo,prefix=datasets/,read-only=true'

# Create and connect to the terminal as a specific user
qshell sandbox create my-template -u root
qshell sbx cr my-template -u root`,
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
cfg.CmdCfg.CmdId = docs.SandboxCreateType
Expand All @@ -154,6 +158,7 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command {
cmd.Flags().Int32VarP(&info.Timeout, "timeout", "t", 0, "sandbox timeout in seconds")
cmd.Flags().IntVar(&retryMax, "retry-max", 0, "maximum automatic retries for sandbox creation (0 disables retries; default uses SANDBOX_RETRY_MAX or 5)")
cmd.Flags().BoolVar(&info.Detach, "detach", false, "create sandbox without connecting terminal (sandbox stays alive until timeout)")
cmd.Flags().StringVarP(&info.User, "user", "u", "", "user to run the terminal as (default: sandbox default user; ignored with --detach)")
Comment thread
eirture marked this conversation as resolved.
cmd.Flags().StringVarP(&info.Metadata, "metadata", "m", "", "metadata key=value pairs (comma-separated)")
cmd.Flags().StringArrayVarP(&info.EnvVars, "env-var", "e", nil, "environment variables (KEY=VALUE, can be specified multiple times)")
cmd.Flags().BoolVar(&info.AutoPause, "auto-pause", false, "automatically pause sandbox when timeout expires (instead of killing)")
Expand All @@ -164,14 +169,18 @@ var sandboxCreateCmdBuilder = func(cfg *iqshell.Config) *cobra.Command {
}

var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command {
info := operations.ConnectInfo{}
retryMax := 0
cmd := &cobra.Command{
Use: "connect <sandboxID>",
Aliases: []string{"cn"},
Short: "Connect to an existing sandbox terminal (alias: cn)",
Example: ` # Connect to a sandbox by ID
qshell sandbox connect sb-xxxxxxxxxxxx
qshell sbx cn sb-xxxxxxxxxxxx`,
qshell sbx cn sb-xxxxxxxxxxxx

# Connect as a specific user
qshell sandbox connect sb-xxxxxxxxxxxx -u root`,
Run: func(cmd *cobra.Command, args []string) {
cfg.CmdCfg.CmdId = docs.SandboxConnectType
if !iqshell.CheckAndLoad(cfg, iqshell.CheckAndLoadInfo{}) {
Expand All @@ -181,14 +190,15 @@ var sandboxConnectCmdBuilder = func(cfg *iqshell.Config) *cobra.Command {
_ = cmd.Usage()
return
}
info := operations.ConnectInfo{SandboxID: args[0]}
info.SandboxID = args[0]
if cmd.Flags().Changed("retry-max") {
info.RetryMax = &retryMax
}
operations.Connect(info)
},
}
cmd.Flags().IntVar(&retryMax, "retry-max", 0, "maximum automatic retries for sandbox connection (0 disables retries; default uses SANDBOX_RETRY_MAX or 5)")
cmd.Flags().StringVarP(&info.User, "user", "u", "", "user to run the terminal as (default: sandbox default user)")
return cmd
}

Expand Down
8 changes: 8 additions & 0 deletions cmd_test/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ func TestSandboxCreateDocumentWithEnvVar(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "create"}, "-e", "FOO=bar", "-e", "BAZ=qux")
}

func TestSandboxCreateDocumentWithUser(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "create"}, "my-template", "-u", "root")
}

func TestSandboxKillDocumentWithAll(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "kill"}, "--all")
}
Expand All @@ -145,6 +149,10 @@ func TestSandboxResumeDocumentWithFlags(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "resume"}, "--all", "-m", "env=staging")
}

func TestSandboxConnectDocumentWithFlags(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "connect"}, "sb-test", "-u", "root")
}

func TestSandboxExecDocumentWithFlags(t *testing.T) {
testSubcommandDocumentWithFlags(t, []string{"sandbox", "exec"}, "sb-test", "-b", "-c", "/app", "-u", "root")
}
Expand Down
11 changes: 9 additions & 2 deletions docs/sandbox_connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

# 格式
```
qshell sandbox connect <sandboxID> [--retry-max <N>]
qshell sbx cn <sandboxID> [--retry-max <N>]
qshell sandbox connect <sandboxID> [--user <user>] [--retry-max <N>]
qshell sbx cn <sandboxID> [--user <user>] [--retry-max <N>]
```

# 帮助文档
Expand All @@ -18,6 +18,7 @@ $ qshell sandbox connect --doc

# 参数
- `sandboxID`:沙箱 ID(必填)
- `--user`/`-u`:终端运行的用户;未指定时使用沙箱默认用户(`user`)
- `--retry-max`:连接请求的最大自动重试次数;`0` 禁用重试。未传入时优先读取 `SANDBOX_RETRY_MAX`,未设置则默认重试 5 次

# 示例
Expand All @@ -26,6 +27,12 @@ $ qshell sandbox connect sb-xxxxxxxxxxxx
$ qshell sbx cn sb-xxxxxxxxxxxx
```

以 `root` 用户身份连接终端:
```
$ qshell sandbox connect sb-xxxxxxxxxxxx -u root
$ qshell sbx cn sb-xxxxxxxxxxxx --user root
```

禁用连接请求自动重试:
```
$ qshell sandbox connect sb-xxxxxxxxxxxx --retry-max 0
Expand Down
11 changes: 9 additions & 2 deletions docs/sandbox_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

# 格式
```
qshell sandbox create [template] [-t <seconds>] [--retry-max <N>] [--detach] [-m <metadata>] [-e <KEY=VALUE>...] [--auto-pause] [--injection-rule <ruleID>...] [--inline-injection <spec>...] [--resource <spec>...]
qshell sbx cr [template] [-t <seconds>] [--retry-max <N>] [--detach] [-m <metadata>] [-e <KEY=VALUE>...] [--auto-pause] [--injection-rule <ruleID>...] [--inline-injection <spec>...] [--resource <spec>...]
qshell sandbox create [template] [-t <seconds>] [--retry-max <N>] [--detach] [-u <user>] [-m <metadata>] [-e <KEY=VALUE>...] [--auto-pause] [--injection-rule <ruleID>...] [--inline-injection <spec>...] [--resource <spec>...]
qshell sbx cr [template] [-t <seconds>] [--retry-max <N>] [--detach] [-u <user>] [-m <metadata>] [-e <KEY=VALUE>...] [--auto-pause] [--injection-rule <ruleID>...] [--inline-injection <spec>...] [--resource <spec>...]
```

# 帮助文档
Expand All @@ -27,6 +27,7 @@ $ qshell sandbox create --doc
- `-t, --timeout`:沙箱超时时间(秒)
- `--retry-max`:创建请求的最大自动重试次数;`0` 禁用重试。未传入时优先读取 `SANDBOX_RETRY_MAX`,未设置则默认重试 5 次
- `--detach`:创建沙箱但不连接终端,沙箱保持存活直到超时。此参数没有短参数
- `-u, --user`:终端运行的用户;未指定时使用沙箱默认用户(`user`)。与 `--detach` 同时使用时不生效(不连接终端)
- `-m, --metadata`:元数据键值对(格式:key1=value1,key2=value2)
- `-e, --env-var`:环境变量(KEY=VALUE 格式,可多次指定)
- `--auto-pause`:超时后自动暂停沙箱,而不是终止沙箱
Expand Down Expand Up @@ -127,3 +128,9 @@ $ qshell sandbox create my-template \
$ qshell sbx cr my-template \
--resource 'type=kodo,bucket=my-bucket,mount=/mnt/kodo'
```

13. 以 `root` 用户身份连接终端
```
$ qshell sandbox create my-template -u root
$ qshell sbx cr my-template --user root
```
8 changes: 7 additions & 1 deletion iqshell/sandbox/sandbox/operations/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// ConnectInfo holds parameters for connecting to a sandbox.
type ConnectInfo struct {
SandboxID string
User string
RetryMax *int
}

Expand All @@ -36,5 +37,10 @@ func Connect(info ConnectInfo) {
}
sbClient.PrintSuccess("Connected to sandbox %s", sb.ID())

runTerminalSession(ctx, sb)
var opts []sandbox.CommandOption
if info.User != "" {
opts = append(opts, sandbox.WithCommandUser(info.User))
}

runTerminalSession(ctx, sb, opts...)
}
11 changes: 9 additions & 2 deletions iqshell/sandbox/sandbox/operations/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import (

// CreateInfo holds parameters for creating a sandbox.
type CreateInfo struct {
TemplateID string
TemplateID string
// User 终端运行的用户,仅在非 detach 模式下生效
User string
RetryMax *int
Timeout int32
Metadata string
Expand Down Expand Up @@ -109,7 +111,12 @@ func Create(info CreateInfo) {
}
}()

runTerminalSession(ctx, sb)
var opts []sandbox.CommandOption
if info.User != "" {
opts = append(opts, sandbox.WithCommandUser(info.User))
}

runTerminalSession(ctx, sb, opts...)
}

func buildSandboxInjections(ruleIDs, inlineSpecs []string) ([]sandbox.SandboxInjectionSpec, error) {
Expand Down
12 changes: 8 additions & 4 deletions iqshell/sandbox/sandbox/operations/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ func detectResize(previous terminalSize, width, height int, err error) (terminal
}

// runTerminalSession creates a PTY session and handles stdin/stdout bridging.
func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox) {
// opts 会追加到 PTY 创建选项中,用于指定运行用户等。
func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox, opts ...sandbox.CommandOption) {
// Get terminal size
width, height, err := term.GetSize(int(os.Stdin.Fd()))
if err != nil {
Expand All @@ -111,12 +112,15 @@ func runTerminalSession(ctx context.Context, sb *sandbox.Sandbox) {
defer ptyCancel()

// Create PTY session
ptyOpts := append([]sandbox.CommandOption{
sandbox.WithOnPtyData(func(data []byte) {
os.Stdout.Write(data)
}),
}, opts...)
handle, err := sb.Pty().Create(ptyCtx, sandbox.PtySize{
Cols: uint32(width),
Rows: uint32(height),
}, sandbox.WithOnPtyData(func(data []byte) {
os.Stdout.Write(data)
}))
}, ptyOpts...)
if err != nil {
sbClient.PrintError("create PTY failed: %v", err)
return
Expand Down
Loading