Remote sandbox mode has two issues:
Workspace path leakage: The LLM sees host paths (e.g.
/app/.xbot/users/ou_xxx/workspace) instead of the runner’s workspace. This is becauseSandboxWorkDiris""for remote mode, causing all path fallbacks to use host paths.Global skills/agents not available: Skills and agents on the server are not copied to the runner, so the LLM cannot discover or use them.
sandboxWorkDir()returns""for non-docker modesSandboxEnabledisa.sandboxMode == "docker"— false for remotebuildPrompt()only handles docker mode for workDirEnsureSynced()skips remote mode entirely- Path guard functions (
defaultWorkspaceRoot,shouldUseSandbox) gate onSandboxEnabled || SandboxWorkDir != ""
Add one method to tools.Sandbox interface:
Workspace(userID string) string
Implementations:
| Sandbox | Return |
|---|---|
| DockerSandbox | "/workspace" |
| RemoteSandbox | Runner’s reported workspace (from registration) |
| NoneSandbox | "" (empty = use host paths) |
| Field | Change |
|---|---|
SandboxWorkDir | Remove from RunConfig and ToolContext. All consumers use sandbox.Workspace(userID). |
SandboxEnabled | Change from sandboxMode == "docker" to sandbox.Name() != "none". |
SandboxReadOnlyRoots | Keep in ToolContext but leave empty for remote mode. |
SandboxMode | Keep for places that need docker vs remote distinction. |
ReadOnlyRoots | Keep — stores host paths used as sync source. |
WorkingDir | Keep — host workDir for server-side paths (MCP config, DataDir). |
WorkspaceRoot | Keep — host user workspace for server-side operations (sync source, bang output). |
All functions in tools/path_guard.go that currently read ctx.SandboxWorkDir or ctx.SandboxEnabled switch to use the Sandbox interface:
// Before
func defaultWorkspaceRoot(ctx *ToolContext) string {
if ctx.SandboxEnabled && ctx.SandboxWorkDir != "" { return ctx.SandboxWorkDir }
...
}
// After
func defaultWorkspaceRoot(ctx *ToolContext) string {
if ctx.Sandbox != nil && ctx.Sandbox.Name() != "none" {
return ctx.Sandbox.Workspace(ctx.OriginUserID)
}
...
}
Affected functions:
defaultWorkspaceRoot— usesandbox.Workspace()sandboxBaseDir— usesandbox.Workspace()shouldUseSandbox— usesandbox.Name() != "none"sandboxReadOnlyRoots— keep for docker, return as-is for remote (emptySandboxWorkDirtriggers passthrough)
Each tool that dispatches on SandboxEnabled updates its guard:
// Before
if ctx.SandboxEnabled && ctx.WorkspaceRoot != "" { ... }
// After (for tools using Sandbox API — Read, Write, Glob, Grep, Cd)
if shouldUseSandbox(ctx) { ... }
// After (for Shell tool — already uses sandbox.Name() switch)
// Already correct after previous fix
buildBaseRunConfig / buildToolExecutor:
- Remove
SandboxWorkDir: a.sandboxWorkDir() - Change
SandboxEnabled: a.sandboxMode == "docker"→SandboxEnabled: a.sandboxMode != "none"
buildPrompt / initPipelines:
promptWorkDirswitches onsandboxMode:switch sandboxMode { case "docker": promptWorkDir = "/workspace" case "remote": promptWorkDir = sandbox.Workspace(senderID) default: promptWorkDir = a.workDir }
sandboxWorkDir() method:
- Remove. All callers use
sandbox.Workspace(userID)directly.
ensureWorkspace:
- Already sandbox-aware (
sandbox.MkdirAllwhen sandbox != nil). os.MkdirAll(wsRoot)calls inengine_wire.go:545andinteractive.go:346need to use sandbox API or skip for remote mode.
When a runner connects and registers (RemoteSandbox.handleRegister):
RemoteSandboxstoresglobalSkillDirsandagentsDir(passed at init time)- On registration, triggers a one-time sync:
- Read server-side
globalSkillDirscontents viaos.ReadDir - Write each skill subdirectory to runner via
Sandbox.MkdirAll+Sandbox.WriteFile - Read server-side
agentsDircontents viaos.ReadDir - Write each agent
.mdfile to runner viaSandbox.WriteFile
- Read server-side
- Skills/agents land at
{runner_workspace}/.skills/and{runner_workspace}/.agents/ - LLM discovers them via normal workspace scanning (SkillTool, SubAgentTool)
Sync happens once per runner connection. Reconnection re-triggers sync.
SubAgent inherits Sandbox from parent ToolContext. buildSubAgentRunConfig:
- Uses
sandbox.Workspace(userID)for promptworkDir - Uses
sandbox.Name() != "none"forSandboxEnabled
| File | Change |
|---|---|
tools/sandbox.go | Add Workspace(userID) string to Sandbox interface |
tools/remote_sandbox.go | Implement Workspace(), add sync on registration |
tools/docker_sandbox.go | Implement Workspace() → "/workspace" |
tools/none_sandbox.go | Implement Workspace() → "" |
agent/engine.go | Remove SandboxWorkDir from RunConfig, update buildToolContext |
agent/engine_wire.go | Update SandboxEnabled, remove SandboxWorkDir, fix os.MkdirAll |
tools/path_guard.go | Update all guard functions to use Sandbox interface |
tools/shell.go | Already uses sandbox.Name() switch — no change needed |
tools/cd.go | Update shouldUseSandbox usage |
tools/edit.go | Update SandboxEnabled guard |
tools/read.go | Update SandboxEnabled guard |
tools/grep.go | Update SandboxEnabled guard |
tools/glob.go | Update SandboxEnabled guard |
tools/sandbox_exec.go | Update setSandboxDir to use sandbox.Workspace() |
tools/interface.go | Remove SandboxWorkDir from ToolContext |
tools/skill.go | Update resolveSkill to use sandbox.Workspace() |
agent/agent.go | Remove sandboxWorkDir(), update buildPrompt |
agent/context.go | Update initPipelines promptWorkDir |
agent/interactive.go | Fix os.MkdirAll, update SandboxEnabled |
- MCP stdio in remote mode (architecturally incompatible)
- CdTool path resolution in remote mode (CurrentDir semantics need separate design)
- SubAgent CWD inheritance in remote mode