mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
Add exec allow_remote config support in web settings (#1363)
- default tools.exec.allow_remote to true when omitted in config loading - preserve allow_remote in OpenClaw config migration and API updates - expose allow_remote in the web config form with i18n strings - add backend and config tests covering the new default behavior
This commit is contained in:
@@ -384,6 +384,13 @@ func TestDefaultConfig_OpenAIWebSearchEnabled(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig_ExecAllowRemoteEnabled(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
if !cfg.Tools.Exec.AllowRemote {
|
||||
t.Fatal("DefaultConfig().Tools.Exec.AllowRemote should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
@@ -400,6 +407,22 @@ func TestLoadConfig_OpenAIWebSearchDefaultsTrueWhenUnset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_ExecAllowRemoteDefaultsTrueWhenUnset(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
if err := os.WriteFile(configPath, []byte(`{"tools":{"exec":{"enable_deny_patterns":true}}}`), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile() error: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error: %v", err)
|
||||
}
|
||||
if !cfg.Tools.Exec.AllowRemote {
|
||||
t.Fatal("tools.exec.allow_remote should remain true when unset in config file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_OpenAIWebSearchCanBeDisabled(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
configPath := filepath.Join(dir, "config.json")
|
||||
|
||||
@@ -427,7 +427,7 @@ func DefaultConfig() *Config {
|
||||
Enabled: true,
|
||||
},
|
||||
EnableDenyPatterns: true,
|
||||
AllowRemote: false,
|
||||
AllowRemote: true,
|
||||
TimeoutSeconds: 60,
|
||||
},
|
||||
Skills: SkillsToolsConfig{
|
||||
|
||||
@@ -1111,6 +1111,7 @@ func (c ToolsConfig) ToStandardTools() config.ToolsConfig {
|
||||
Exec: config.ExecConfig{
|
||||
EnableDenyPatterns: c.Exec.EnableDenyPatterns,
|
||||
CustomDenyPatterns: c.Exec.CustomDenyPatterns,
|
||||
AllowRemote: config.DefaultConfig().Tools.Exec.AllowRemote,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,20 @@ func TestConvertToPicoClaw(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToStandardConfig_ExecAllowRemoteDefaultsTrue(t *testing.T) {
|
||||
cfg := (&PicoClawConfig{
|
||||
Tools: ToolsConfig{
|
||||
Exec: ExecConfig{
|
||||
EnableDenyPatterns: true,
|
||||
},
|
||||
},
|
||||
}).ToStandardConfig()
|
||||
|
||||
if !cfg.Tools.Exec.AllowRemote {
|
||||
t.Fatal("ToStandardConfig() should preserve the default tools.exec.allow_remote=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertToPicoClawWithQQAndDingTalk(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configPath := filepath.Join(tmpDir, "openclaw.json")
|
||||
|
||||
@@ -48,6 +48,9 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if execAllowRemoteOmitted(body) {
|
||||
cfg.Tools.Exec.AllowRemote = config.DefaultConfig().Tools.Exec.AllowRemote
|
||||
}
|
||||
|
||||
if errs := validateConfig(&cfg); len(errs) > 0 {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -68,6 +71,20 @@ func (h *Handler) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func execAllowRemoteOmitted(body []byte) bool {
|
||||
var raw struct {
|
||||
Tools *struct {
|
||||
Exec *struct {
|
||||
AllowRemote *bool `json:"allow_remote"`
|
||||
} `json:"exec"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return false
|
||||
}
|
||||
return raw.Tools == nil || raw.Tools.Exec == nil || raw.Tools.Exec.AllowRemote == nil
|
||||
}
|
||||
|
||||
// handlePatchConfig partially updates the system configuration using JSON Merge Patch (RFC 7396).
|
||||
// Only the fields present in the request body will be updated; all other fields remain unchanged.
|
||||
//
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestHandleUpdateConfig_PreservesExecAllowRemoteDefaultWhenOmitted(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
h := NewHandler(configPath)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/config", bytes.NewBufferString(`{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/.picoclaw/workspace"
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "custom-default",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "sk-default"
|
||||
}
|
||||
]
|
||||
}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
if !cfg.Tools.Exec.AllowRemote {
|
||||
t.Fatal("tools.exec.allow_remote should remain true when omitted from PUT /api/config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleUpdateConfig_DoesNotInheritDefaultModelFields(t *testing.T) {
|
||||
configPath, cleanup := setupOAuthTestEnv(t)
|
||||
defer cleanup()
|
||||
|
||||
h := NewHandler(configPath)
|
||||
mux := http.NewServeMux()
|
||||
h.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/config", bytes.NewBufferString(`{
|
||||
"agents": {
|
||||
"defaults": {
|
||||
"workspace": "~/.picoclaw/workspace"
|
||||
}
|
||||
},
|
||||
"model_list": [
|
||||
{
|
||||
"model_name": "custom-default",
|
||||
"model": "openai/gpt-4o",
|
||||
"api_key": "sk-default"
|
||||
}
|
||||
]
|
||||
}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d, body=%s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
cfg, err := config.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error = %v", err)
|
||||
}
|
||||
if got := cfg.ModelList[0].APIBase; got != "" {
|
||||
t.Fatalf("model_list[0].api_base = %q, want empty string", got)
|
||||
}
|
||||
}
|
||||
@@ -189,6 +189,11 @@ export function ConfigPage() {
|
||||
session: {
|
||||
dm_scope: dmScope,
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
allow_remote: form.allowRemote,
|
||||
},
|
||||
},
|
||||
heartbeat: {
|
||||
enabled: form.heartbeatEnabled,
|
||||
interval: heartbeatInterval,
|
||||
|
||||
@@ -63,6 +63,13 @@ export function AgentDefaultsSection({
|
||||
}
|
||||
/>
|
||||
|
||||
<SwitchCardField
|
||||
label={t("pages.config.allow_remote")}
|
||||
hint={t("pages.config.allow_remote_hint")}
|
||||
checked={form.allowRemote}
|
||||
onCheckedChange={(checked) => onFieldChange("allowRemote", checked)}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={t("pages.config.max_tokens")}
|
||||
hint={t("pages.config.max_tokens_hint")}
|
||||
|
||||
@@ -3,6 +3,7 @@ export type JsonRecord = Record<string, unknown>
|
||||
export interface CoreConfigForm {
|
||||
workspace: string
|
||||
restrictToWorkspace: boolean
|
||||
allowRemote: boolean
|
||||
maxTokens: string
|
||||
maxToolIterations: string
|
||||
summarizeMessageThreshold: string
|
||||
@@ -54,6 +55,7 @@ export const DM_SCOPE_OPTIONS = [
|
||||
export const EMPTY_FORM: CoreConfigForm = {
|
||||
workspace: "",
|
||||
restrictToWorkspace: true,
|
||||
allowRemote: true,
|
||||
maxTokens: "32768",
|
||||
maxToolIterations: "50",
|
||||
summarizeMessageThreshold: "20",
|
||||
@@ -103,6 +105,8 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
|
||||
const session = asRecord(root.session)
|
||||
const heartbeat = asRecord(root.heartbeat)
|
||||
const devices = asRecord(root.devices)
|
||||
const tools = asRecord(root.tools)
|
||||
const exec = asRecord(tools.exec)
|
||||
|
||||
return {
|
||||
workspace: asString(defaults.workspace) || EMPTY_FORM.workspace,
|
||||
@@ -110,6 +114,10 @@ export function buildFormFromConfig(config: unknown): CoreConfigForm {
|
||||
defaults.restrict_to_workspace === undefined
|
||||
? EMPTY_FORM.restrictToWorkspace
|
||||
: asBool(defaults.restrict_to_workspace),
|
||||
allowRemote:
|
||||
exec.allow_remote === undefined
|
||||
? EMPTY_FORM.allowRemote
|
||||
: asBool(exec.allow_remote),
|
||||
maxTokens: asNumberString(defaults.max_tokens, EMPTY_FORM.maxTokens),
|
||||
maxToolIterations: asNumberString(
|
||||
defaults.max_tool_iterations,
|
||||
|
||||
@@ -429,6 +429,8 @@
|
||||
"workspace_hint": "Base directory for agent file operations.",
|
||||
"restrict_workspace": "Restrict to Workspace",
|
||||
"restrict_workspace_hint": "Only allow file operations inside workspace.",
|
||||
"allow_remote": "Allow Remote Shell Execution",
|
||||
"allow_remote_hint": "When enabled, shell commands can also run for remote sessions or non-local contexts. When disabled, shell execution stays limited to local safe contexts.",
|
||||
"max_tokens": "Max Tokens",
|
||||
"max_tokens_hint": "Upper token limit per model response.",
|
||||
"max_tool_iterations": "Max Tool Iterations",
|
||||
|
||||
@@ -429,6 +429,8 @@
|
||||
"workspace_hint": "智能体执行文件读写操作时使用的基础目录。",
|
||||
"restrict_workspace": "限制工作目录访问",
|
||||
"restrict_workspace_hint": "仅允许在工作目录内执行文件操作。",
|
||||
"allow_remote": "允许远程执行 Shell 命令",
|
||||
"allow_remote_hint": "开启后,来自远程会话或非本地上下文的请求也可以执行 shell 命令;关闭后,仅允许本地安全上下文执行。",
|
||||
"max_tokens": "最大 Token 数",
|
||||
"max_tokens_hint": "单次模型响应允许的最大 Token 数。",
|
||||
"max_tool_iterations": "最大工具迭代次数",
|
||||
|
||||
Reference in New Issue
Block a user