Skip to content
Closed
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 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.

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 @@ -676,6 +676,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type SandboxCapability = {
supported: boolean;
mechanism: string;
platform: string;
reason?: string;
};

/** WebUI:沙箱在桌面端执行,浏览器的 OS 不代表执行端平台;null = 未知,显示通用文案。 */
export function inferSandboxPlatform(): "macos" | "linux" | "windows" | null {
return null;
}

/** 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 @@ -36,6 +36,7 @@ import { GatewayTranscript } from "@/components/GatewayTranscript";
import {
getNextTheme,
updateExecutionModeFromChatSelection,
updateSystem,
updateWorkspaceResourceSettings,
} from "@/lib/settings";
import { WorkdirPickerModal } from "@/pages/settings/WorkdirPickerModal";
Expand Down Expand Up @@ -681,6 +682,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,
};
}
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 @@ -339,6 +339,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.

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 @@ -32,6 +32,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
16 changes: 16 additions & 0 deletions crates/agent-gui/src-tauri/src/commands/config/settings/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -459,10 +459,25 @@ fn system_value_with_defaults(raw: Option<Value>, default_workdir: &str) -> Valu
SYSTEM_SYSTEM_PROXY_KEY.to_string(),
normalize_system_proxy_value(system.get(SYSTEM_SYSTEM_PROXY_KEY)),
);
system.insert(
SYSTEM_COMMAND_SAFETY_MODE_KEY.to_string(),
normalize_command_safety_mode_value(system.get(SYSTEM_COMMAND_SAFETY_MODE_KEY)),
);

Value::Object(system)
}

/// "ask" | "auto" | "sandbox" | "sandboxOffline",缺省 "auto",与前端
/// normalizeCommandSafetyMode 一致。
fn normalize_command_safety_mode_value(raw: Option<&Value>) -> Value {
let mode = raw
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| matches!(*value, "ask" | "auto" | "sandbox" | "sandboxOffline"))
.unwrap_or("auto");
Value::String(mode.to_string())
}

fn load_system_with_defaults(conn: &Connection, default_workdir: &str) -> Result<Value, String> {
Ok(system_value_with_defaults(
load_system(conn)?,
Expand Down Expand Up @@ -500,6 +515,7 @@ fn save_system_with_default_workdir(
SYSTEM_EXECUTION_MODE_KEY,
SYSTEM_WORKDIR_KEY,
SYSTEM_TOOL_POLICIES_KEY,
SYSTEM_COMMAND_SAFETY_MODE_KEY,
SYSTEM_WORKSPACE_PROJECTS_KEY,
SYSTEM_WORKSPACE_PROJECT_GROUPS_KEY,
SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1129,12 +1129,13 @@ mod tests {
};
let loaded = load_system(&conn).expect("load system");

assert_eq!(row_count, 11);
assert_eq!(row_count, 12);
assert_eq!(
keys,
vec![
SYSTEM_ACTIVE_WORKSPACE_PROJECT_ID_KEY.to_string(),
SYSTEM_ARCHIVED_WORKSPACE_PROJECT_PATHS_KEY.to_string(),
SYSTEM_COMMAND_SAFETY_MODE_KEY.to_string(),
SYSTEM_EXECUTION_MODE_KEY.to_string(),
SYSTEM_HIDDEN_WORKSPACE_PROJECT_PATHS_KEY.to_string(),
SYSTEM_MISSING_WORKSPACE_PROJECT_PATHS_KEY.to_string(),
Expand All @@ -1155,6 +1156,7 @@ mod tests {
"missingWorkspaceProjectPaths": [],
"archivedWorkspaceProjectPaths": [],
"workspaceResourceSettings": {},
"commandSafetyMode": "auto",
"systemProxy": default_system_proxy_json(),
"workdir": default_workdir.clone(),
"toolPolicies": { "Bash": "ask", "server:docs-mcp": "deny" },
Expand Down Expand Up @@ -1453,6 +1455,7 @@ mod tests {
"missingWorkspaceProjectPaths": [],
"archivedWorkspaceProjectPaths": [],
"workspaceResourceSettings": {},
"commandSafetyMode": "auto",
"systemProxy": default_system_proxy_json(),
"workdir": "/tmp/liveagent-default-project",
"toolPolicies": null,
Expand Down Expand Up @@ -1506,6 +1509,7 @@ mod tests {
"missingWorkspaceProjectPaths": [],
"archivedWorkspaceProjectPaths": [],
"workspaceResourceSettings": {},
"commandSafetyMode": "auto",
"systemProxy": default_system_proxy_json(),
"workdir": "/tmp/liveagent-default-project",
"toolPolicies": null,
Expand Down Expand Up @@ -1541,6 +1545,7 @@ mod tests {
"missingWorkspaceProjectPaths": [],
"archivedWorkspaceProjectPaths": [],
"workspaceResourceSettings": {},
"commandSafetyMode": "auto",
"systemProxy": default_system_proxy_json(),
"workdir": "/tmp/liveagent-default-project",
"workspaceProjects": [
Expand Down
16 changes: 15 additions & 1 deletion crates/agent-gui/src-tauri/src/commands/runtime/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,31 @@ use crate::runtime::managed_process::{
ManagedProcessLogResponse, ManagedProcessRegistry, ManagedProcessSnapshot,
ManagedProcessStartResponse, ManagedProcessStatusResponse, ManagedProcessStopResponse,
};
use crate::runtime::sandbox::SandboxOptions;

#[tauri::command(rename_all = "snake_case")]
#[allow(clippy::too_many_arguments)]
pub fn managed_process_start(
registry: State<'_, Arc<ManagedProcessRegistry>>,
workdir: String,
command: String,
cwd: Option<String>,
label: Option<String>,
isolated: Option<bool>,
sandbox: Option<bool>,
sandbox_allow_network: Option<bool>,
) -> Result<ManagedProcessStartResponse, String> {
registry.start(workdir, command, cwd, label, isolated.unwrap_or(false))
let sandbox_options = (sandbox == Some(true)).then(|| SandboxOptions {
allow_network: sandbox_allow_network.unwrap_or(true),
});
registry.start(
workdir,
command,
cwd,
label,
isolated.unwrap_or(false),
sandbox_options,
)
}

#[tauri::command(rename_all = "snake_case")]
Expand Down
Loading
Loading