Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
48f2f8e
feat(sandbox): 对话框内命令执行方式选择器 + OS 级沙箱执行
coder-hhx Aug 16, 2026
66b1f4a
Merge remote-tracking branch 'origin/main' into feature/rust-embed-cu…
coder-hhx Aug 16, 2026
f777ac6
docs: 命令执行方式选择器截图(auto/下拉/沙箱态)
coder-hhx Aug 16, 2026
cece82e
fix(sandbox): reject sensitive workspaces and preserve isolated proce…
coder-hhx Aug 16, 2026
dce9119
fix(gateway): 透传 commandSafetyMode 至远端会话链路,修复沙箱模式被忽略 (PR505 P1#1)
coder-hhx Aug 16, 2026
453890c
Merge remote-tracking branch 'origin/main' into feature/rust-embed-cu…
coder-hhx Aug 16, 2026
8d70769
feat(sandbox): Windows 免管理员 workspace-write 沙箱后端
coder-hhx Aug 16, 2026
4c43d67
docs(sandbox): add Windows-state selector & composer screenshots for …
coder-hhx Aug 16, 2026
fb009ee
Merge remote-tracking branch 'origin/main' into feature/rust-embed-cu…
su-fen Aug 17, 2026
92d0176
fix(sandbox): enforce fences for resumable shell sessions
coder-hhx Aug 17, 2026
4b739fd
merge: combine base OS sandbox with latest main
coder-hhx Aug 17, 2026
c264631
merge: add Windows workspace-write sandbox backend
coder-hhx Aug 17, 2026
f6104aa
fix(sandbox): enforce policy across all shell execution paths
coder-hhx Aug 17, 2026
bef07fe
feat(sandbox): Windows offline backend via AppContainer + fix Git Bas…
coder-hhx Aug 18, 2026
c125b58
docs(ui): update Windows sandboxOffline copy for AppContainer backend
coder-hhx Aug 18, 2026
ecfdc28
fix(sandbox): close bypass paths and enforce the fence from the backend
coder-hhx Aug 19, 2026
a227eca
merge: bring sandbox work up to date with main
coder-hhx Aug 19, 2026
96c124b
fix(sandbox): close remaining TMPDIR, MCP, and bwrap bypasses
su-fen Aug 19, 2026
5fd878d
fix(sandbox): unblock PowerShell CNG init under WRITE_RESTRICTED
su-fen Aug 19, 2026
a53b3f1
fix(sandbox): recognise managed-runtime startup death and prefer cmd.…
coder-hhx Aug 19, 2026
d0e3738
merge: take the CNG root-cause fix over my cmd.exe fallback
coder-hhx Aug 19, 2026
a2fc8b1
style(sandbox): separate the trailing doc paragraph from the exit-cod…
coder-hhx Aug 19, 2026
561af38
fix(sandbox): preserve workspace access across Windows modes
su-fen Aug 19, 2026
f475066
fix(sandbox): gate Unix temp helpers by platform
su-fen Aug 19, 2026
e02f39d
style(composer): align the sandbox dropdown with model and branch sel…
su-fen Aug 19, 2026
bb0a787
fix(stt): close duplicate-sequence sockets before adapter teardown
su-fen Aug 19, 2026
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 crates/agent-gateway/internal/chatcmd/chatcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func NormalizeRequestBody(body *handler.ChatRequestBody) error {
body.ClientRequestID = strings.TrimSpace(body.ClientRequestID)
body.ExecutionMode = handler.NormalizeExecutionMode(body.ExecutionMode)
body.Workdir = handler.NormalizeWorkdir(body.Workdir)
body.CommandSafetyMode = handler.NormalizeCommandSafetyMode(body.CommandSafetyMode)
body.QueuePolicy = normalizeQueuePolicy(body.QueuePolicy)
body.UploadedFiles = handler.NormalizeChatUploadedFiles(body.UploadedFiles)
body.RuntimeControls = handler.NormalizeChatRuntimeControls(body.RuntimeControls)
Expand Down Expand Up @@ -274,6 +275,7 @@ func buildUserMessageAppendedPayload(
"uploaded_files": body.UploadedFiles,
"execution_mode": body.ExecutionMode,
"workdir": body.Workdir,
"command_safety_mode": body.CommandSafetyMode,
"runtime_controls": body.RuntimeControls,
"selected_model": body.SelectedModel,
}
Expand Down Expand Up @@ -324,6 +326,7 @@ func buildProtoRequest(body handler.ChatRequestBody) *gatewayv2.ChatRequest {
RuntimeControls: handler.ToProtoChatRuntimeControls(body.RuntimeControls),
ExecutionMode: body.ExecutionMode,
Workdir: body.Workdir,
CommandSafetyMode: body.CommandSafetyMode,
UploadedFiles: handler.ToProtoChatUploadedFiles(body.UploadedFiles),
QueuePolicy: body.QueuePolicy,
}
Expand Down Expand Up @@ -356,6 +359,7 @@ func RequestBodyFromProto(req *gatewayv2.ChatRequest) handler.ChatRequestBody {
Message: req.GetMessage(),
ExecutionMode: req.GetExecutionMode(),
Workdir: req.GetWorkdir(),
CommandSafetyMode: req.GetCommandSafetyMode(),
QueuePolicy: req.GetQueuePolicy(),
}
if selected := req.GetSelectedModel(); selected != nil {
Expand Down
12 changes: 12 additions & 0 deletions crates/agent-gateway/internal/handler/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type ChatRequestBody struct {
RuntimeControls *ChatRuntimeControlsBody `json:"runtime_controls,omitempty"`
ExecutionMode string `json:"execution_mode,omitempty"`
Workdir string `json:"workdir,omitempty"`
CommandSafetyMode string `json:"command_safety_mode,omitempty"`
UploadedFiles []ChatUploadedFileBody `json:"uploaded_files,omitempty"`
QueuePolicy string `json:"queue_policy,omitempty"`
}
Expand Down Expand Up @@ -143,6 +144,17 @@ func NormalizeWorkdir(value string) string {
return normalizeTrimmedText(value)
}

// NormalizeCommandSafetyMode 归一化命令安全模式。仅放行四个合法值;空串或未知值
// 归为空串,表示"远端未指定",桌面端据此回落到本地 settings.system.commandSafetyMode。
func NormalizeCommandSafetyMode(value string) string {
switch normalizeTrimmedText(value) {
case "ask", "auto", "sandbox", "sandboxOffline":
return normalizeTrimmedText(value)
default:
return ""
}
}

func NormalizeChatUploadedFiles(input []ChatUploadedFileBody) []ChatUploadedFileBody {
out := make([]ChatUploadedFileBody, 0, len(input))
seen := make(map[string]struct{}, len(input))
Expand Down
22 changes: 22 additions & 0 deletions crates/agent-gateway/internal/handler/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ func TestNormalizeExecutionMode(t *testing.T) {
}
}

func TestNormalizeCommandSafetyMode(t *testing.T) {
t.Parallel()

// 空串/未知值归为空串(表示"远端未指定"),桌面端据此回落本地设置;
// 绝不默认成某个具体模式,以免静默下调桌面端已选的更严格模式。
cases := map[string]string{
"": "",
"unknown": "",
" ask ": "ask",
"ask": "ask",
"auto": "auto",
"sandbox": "sandbox",
"sandboxOffline": "sandboxOffline",
}

for input, want := range cases {
if got := NormalizeCommandSafetyMode(input); got != want {
t.Fatalf("NormalizeCommandSafetyMode(%q) = %q, want %q", input, got, want)
}
}
}

func TestNormalizeChatSelectedModelAcceptsGemini(t *testing.T) {
t.Parallel()

Expand Down
19 changes: 15 additions & 4 deletions crates/agent-gateway/internal/proto/v2/gateway.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions crates/agent-gateway/internal/stt/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ func (m *Manager) WebSocketHandler(token string) http.Handler {
}
}()
defer func() {
// Stop the writer before cancelling the adapter. Cancel can still
// emit error/closed events; if the writer is running those frames
// race out after a protocol violation and look like a live session.
close(writerStop)
<-writerDone
if activeID != "" {
m.Cancel(activeID)
}
close(writerStop)
<-writerDone
}()
windowStart := time.Now()
frames := 0
Expand Down Expand Up @@ -172,6 +175,7 @@ func (m *Manager) WebSocketHandler(token string) http.Handler {
nextSequence = 0
case *gatewayv2.SttClientFrame_Audio:
if activeID == "" || payload.Audio.GetSessionId() != activeID || payload.Audio.GetSequence() != nextSequence || len(payload.Audio.GetPcm()) == 0 || len(payload.Audio.GetPcm())%2 != 0 || len(payload.Audio.GetPcm()) > 6400 || m.Send(activeID, Command{Audio: &AudioChunk{Sequence: payload.Audio.GetSequence(), PCM: append([]byte(nil), payload.Audio.GetPcm()...)}}) != nil {
_ = conn.Close()
return
}
nextSequence++
Expand Down
8 changes: 6 additions & 2 deletions crates/agent-gateway/internal/stt/stt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,8 +364,12 @@ func TestSttServerEventMappingAndWebSocketSequenceValidation(t *testing.T) {
t.Fatal(err)
}
conn.SetReadDeadline(time.Now().Add(time.Second))
if _, _, err := conn.ReadMessage(); err == nil {
t.Fatal("duplicate sequence must close the connection")
kind, data, err := conn.ReadMessage()
if err == nil {
t.Fatalf("duplicate sequence must close the connection, got kind=%d payload=%x", kind, data)
}
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
t.Fatal("duplicate sequence left the connection open until the read deadline")
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/agent-gateway/proto/v2/gateway.proto
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,9 @@ message ChatRequest {
string client_request_id = 8;
ChatRuntimeControls runtime_controls = 9;
string queue_policy = 10;
// 命令安全模式(ask/auto/sandbox/sandboxOffline)。远端 WebUI 直带,桌面端据此
// 覆盖本地 settings.system.commandSafetyMode;空串表示未指定(回落本地设置)。
string command_safety_mode = 11;
}

message ChatMessageRef {
Expand Down
10 changes: 7 additions & 3 deletions crates/agent-gateway/test/websocket/v2_chat_terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ func TestV2ChatCommandAcceptedFlow(t *testing.T) {
ChatCommand: &gatewayv2.ChatCommandRequest{
Type: "chat.submit",
Request: &gatewayv2.ChatRequest{
ConversationId: "conv-cmd",
ClientRequestId: "client-cmd-1",
Message: "hello v2",
ConversationId: "conv-cmd",
ClientRequestId: "client-cmd-1",
Message: "hello v2",
CommandSafetyMode: "sandboxOffline",
},
},
},
Expand All @@ -53,6 +54,9 @@ func TestV2ChatCommandAcceptedFlow(t *testing.T) {
if command.GetType() != "chat.submit" || command.GetRequest().GetMessage() != "hello v2" {
t.Fatalf("agent chat command = %#v, want chat.submit hello v2", command)
}
if got := command.GetRequest().GetCommandSafetyMode(); got != "sandboxOffline" {
t.Fatalf("agent command_safety_mode = %q, want sandboxOffline", got)
}
}

func TestV2TerminalBrowserRequiresAgentID(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1301,8 +1301,9 @@ test("GatewayWebSocketClient chatCommand sends the command frame and parses the
clientRequestId: "req-1",
queuePolicy: "append",
systemSettings: {
executionMode: "agent",
executionMode: "tools",
workdir: "/workspace/project",
commandSafetyMode: "sandboxOffline",
},
});
const socket = await connectAndAuth(codec);
Expand All @@ -1314,6 +1315,11 @@ test("GatewayWebSocketClient chatCommand sends the command frame and parses the
assert.equal(command.json.chat_command.request.client_request_id, "req-1");
assert.equal(command.json.chat_command.request.queue_policy, "append");
assert.equal(command.json.chat_command.request.workdir, "/workspace/project");
assert.equal(
command.json.chat_command.request.command_safety_mode,
"sandboxOffline",
"the WebUI must serialize the selected sandbox mode into chat_command",
);

socket.receiveBinary(
codec.encodeServerFrame({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type SandboxCapability = {
supported: boolean;
mechanism: string;
platform: string;
/** 是否支持断网变体(sandboxOffline);由桌面端运行时探测得出。 */
network_control: boolean;
reason?: string;
};

/** WebUI:沙箱在桌面端执行,浏览器侧无从探测;null 表示能力未知(由桌面端裁决)。 */
export function useSandboxCapability(): SandboxCapability | null {
return null;
}
9 changes: 9 additions & 0 deletions crates/agent-gateway/web/src/app/GatewayAppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import type { SttProviderId } from "@/lib/settings";
import {
getNextTheme,
updateExecutionModeFromChatSelection,
updateSystem,
updateWorkspaceResourceSettings,
} from "@/lib/settings";
import { createWebSttSettingsService } from "@/lib/stt/webSttSettingsService";
Expand Down Expand Up @@ -785,6 +786,14 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
modelOptions={modelOptions}
selectedValue={selectedValue}
chatRuntimeControls={chatRuntimeControlsForCurrentProvider}
commandSafetyMode={settings.system.commandSafetyMode}
onCommandSafetyModeChange={(mode) =>
setSettings((prev) =>
prev.system.commandSafetyMode === mode
? prev
: updateSystem(prev, { commandSafetyMode: mode }),
)
}
reasoningOptions={chatRuntimeReasoningOptions}
thinkingAlwaysOn={chatRuntimeThinkingAlwaysOn}
contextUsageTokensSource={contextUsageTokensSource}
Expand Down
1 change: 1 addition & 0 deletions crates/agent-gateway/web/src/app/chatEventUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ export function buildGatewaySystemSettings(settings: AppSettings, workdirOverrid
return {
executionMode: settings.system.executionMode,
workdir: workdirOverride ?? settings.system.workdir.trim(),
commandSafetyMode: settings.system.commandSafetyMode,
};
}
9 changes: 6 additions & 3 deletions crates/agent-gateway/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,21 @@
inset 0 0 0 1px rgba(167, 139, 250, 0.12);
}

.composer-branch-dropdown[data-state="open"] {
.composer-branch-dropdown[data-state="open"],
.composer-safety-dropdown[data-state="open"] {
animation: composerReasoningDropdownIn 0.18s cubic-bezier(0.16, 1, 0.3, 1);
transform-origin: var(--transform-origin, bottom left);
}

.composer-branch-dropdown[data-state="closed"] {
.composer-branch-dropdown[data-state="closed"],
.composer-safety-dropdown[data-state="closed"] {
animation: composerReasoningDropdownOut 0.12s cubic-bezier(0.4, 0, 1, 1) forwards;
transform-origin: var(--transform-origin, bottom left);
}

@media (prefers-reduced-motion: reduce) {
.composer-branch-dropdown {
.composer-branch-dropdown,
.composer-safety-dropdown {
animation: none !important;
}

Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gateway/web/src/lib/gatewaySocketShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type GatewayRequestOptions = {
export type GatewayChatSystemSettings = {
executionMode?: string;
workdir?: string;
commandSafetyMode?: string;
};

export type GatewayChatCommandInput = {
Expand Down Expand Up @@ -469,6 +470,7 @@ export function buildChatCommandPayload(input: GatewayChatCommandInput) {
client_request_id: clientRequestId,
execution_mode: systemSettings?.executionMode?.trim() || "text",
workdir: systemSettings?.workdir?.trim() || "",
command_safety_mode: systemSettings?.commandSafetyMode?.trim() || "",
uploaded_files:
input.uploadedFiles?.map((file) => ({
relative_path: file.relativePath,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ function buildChatCommand(body: J) {
: undefined,
executionMode: str(inner.execution_mode),
workdir: str(inner.workdir),
commandSafetyMode: str(inner.command_safety_mode),
uploadedFiles: uploadedFiles.map((file) => {
const raw = rec(file);
return create(ChatUploadedFileSchema, {
Expand Down

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion crates/agent-gateway/web/src/styles/base-chat.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ html[data-liveagent-webui="gateway"] .composer-model-trigger,
html[data-liveagent-webui="gateway"] .composer-model-label,
html[data-liveagent-webui="gateway"] .model-selector-dropdown,
html[data-liveagent-webui="gateway"] .model-selector-group-label,
html[data-liveagent-webui="gateway"] .model-selector-item {
html[data-liveagent-webui="gateway"] .model-selector-item,
html[data-liveagent-webui="gateway"] .composer-safety-dropdown,
html[data-liveagent-webui="gateway"] .composer-safety-item {
font-family: var(--app-font-family);
}

Expand Down
14 changes: 13 additions & 1 deletion crates/agent-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,19 @@ objc2 = { version = "0.6", default-features = false, features = ["std"] }
block2 = "0.6"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
windows-sys = { version = "0.61", features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_Security_Authorization",
"Win32_Security_Isolation",
"Win32_System_Threading",
"Win32_System_JobObjects",
"Win32_System_Console",
"Win32_System_Environment",
"Win32_System_Registry",
"Win32_System_LibraryLoader",
] }

[dev-dependencies]
tauri = { version = "2.11.5", features = ["test"] }
5 changes: 5 additions & 0 deletions crates/agent-gui/src-tauri/src/commands/app/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2486,6 +2486,11 @@ pub(crate) fn system_create_project_folder_sync(
})
}

#[tauri::command(rename_all = "snake_case")]
pub fn system_sandbox_capability() -> crate::runtime::sandbox::SandboxCapability {
crate::runtime::sandbox::capability()
}

#[tauri::command(rename_all = "snake_case")]
pub async fn system_pick_folder(initial_workdir: Option<String>) -> Result<Option<String>, String> {
tauri::async_runtime::spawn_blocking(move || {
Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gui/src-tauri/src/commands/automation/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub(crate) fn run_hook_script_sync(
None,
token.clone(),
&context,
// Hook 脚本是用户显式配置的自动化,不属于模型驱动面,不套沙箱。
None,
);

if let (Some(scope), Some(token)) = (&scope_id, &token) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ const SYSTEM_WORKDIR_KEY: &str = "workdir";
// 工具审批策略(按工具名/`group:`/`server:` 键 → allow/ask/deny)。此前未纳入
// 保存白名单,导致重启后设置丢失;补入本键持久化。
const SYSTEM_TOOL_POLICIES_KEY: &str = "toolPolicies";
// 命令执行方式("ask"/"auto"/"sandbox"/"sandboxOffline"),与前端
// SystemSettings.commandSafetyMode 对齐;sandbox* 由执行层映射为 OS 沙箱参数。
const SYSTEM_COMMAND_SAFETY_MODE_KEY: &str = "commandSafetyMode";
const SYSTEM_WORKSPACE_PROJECTS_KEY: &str = "workspaceProjects";
const SYSTEM_WORKSPACE_PROJECT_GROUPS_KEY: &str = "workspaceProjectGroups";
const SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY: &str = "activeWorkspaceProjectId";
Expand Down
Loading
Loading