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
195 changes: 168 additions & 27 deletions agent/app/service/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,14 @@
defaultToolsSessionVisibility = "all"
maxCommunityAIAgents = int64(5)
openclawPluginBaseDir = "/home/node/.openclaw/extensions"
openclawGatewayPort = 18789
openclawCaddyPort = 8443
openclawCaddyDataPerm = 0777
openclawCaddyLoopbackAddress = "https://127.0.0.1:8443"
openclawTrustedProxyLoopback = "127.0.0.1/32"
)

func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {

Check failure on line 86 in agent/app/service/agents.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 68 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZzmyXhjnvODKcpvov1D&open=AZzmyXhjnvODKcpvov1D&pullRequest=12168
agentType := normalizeAgentType(req.AgentType)
if !isSupportedAgentType(agentType) {
return nil, fmt.Errorf("agent type is invalid")
Expand All @@ -86,14 +91,6 @@
if err := checkPortExist(req.WebUIPort); err != nil {
return nil, err
}
if agentType == constant.AppOpenclaw {
if req.BridgePort <= 0 {
return nil, fmt.Errorf("bridge port is required")
}
if err := checkPortExist(req.BridgePort); err != nil {
return nil, err
}
}
if exist, _ := agentRepo.GetFirst(repo.WithByLowerName(req.Name)); exist != nil && exist.ID > 0 {
return nil, buserr.New("ErrNameIsExist")
}
Expand Down Expand Up @@ -142,7 +139,7 @@
return nil, err
}
if len(allowedOrigins) == 0 {
return nil, fmt.Errorf("allowed origins is required")

Check failure on line 142 in agent/app/service/agents.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "allowed origins is required" 4 times.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZzmyXhjnvODKcpvov1C&open=AZzmyXhjnvODKcpvov1C&pullRequest=12168
}
provider = strings.ToLower(strings.TrimSpace(req.Provider))
if !isSupportedAgentProvider(provider) {
Expand Down Expand Up @@ -209,12 +206,12 @@
}

params := map[string]interface{}{
"PANEL_APP_PORT_HTTP": req.WebUIPort,
constant.CPUS: "0",
constant.MemoryLimit: "0",
constant.HostIP: "",
constant.CPUS: "0",
constant.MemoryLimit: "0",
constant.HostIP: "",
}
if agentType == constant.AppOpenclaw {
params["PANEL_APP_PORT_HTTPS"] = req.WebUIPort
params["PROVIDER"] = provider
params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType
Expand All @@ -223,7 +220,8 @@
params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token
params["PANEL_APP_PORT_BRIDGE"] = req.BridgePort
} else {
params["PANEL_APP_PORT_HTTP"] = req.WebUIPort
}

if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
Expand Down Expand Up @@ -898,6 +896,9 @@
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
return err
}
if err := writeOpenclawCaddyfile(agent.ConfigPath, allowedOrigins); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -1011,6 +1012,7 @@
}

func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
ensureGatewaySecurityDefaults(conf)
payload, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
Expand Down Expand Up @@ -1046,7 +1048,7 @@
func normalizeAllowedOrigin(origin string) (string, error) {
parsed, err := url.Parse(strings.TrimSpace(origin))
if err != nil {
return "", fmt.Errorf("invalid allowed origin: %s", origin)

Check failure on line 1051 in agent/app/service/agents.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "invalid allowed origin: %s" 6 times.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZzmyXhjnvODKcpvov1B&open=AZzmyXhjnvODKcpvov1B&pullRequest=12168
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("invalid allowed origin: %s", origin)
Expand All @@ -1060,14 +1062,14 @@
if pathValue := strings.TrimSpace(parsed.EscapedPath()); pathValue != "" && pathValue != "/" {
return "", fmt.Errorf("invalid allowed origin: %s", origin)
}
if parsed.Port() == "" {
return "", fmt.Errorf("invalid allowed origin: %s", origin)
}
host := parsed.Hostname()
if strings.Contains(host, ":") {
host = "[" + host + "]"
}
normalized := parsed.Scheme + "://" + host
if port := parsed.Port(); port != "" {
normalized += ":" + port
}
normalized := "https://" + host + ":" + parsed.Port()
return normalized, nil
}

Expand Down Expand Up @@ -1099,18 +1101,60 @@
}

func setSecurityConfig(conf map[string]interface{}, config dto.AgentSecurityConfig) {
ensureGatewaySecurityDefaults(conf)
gateway := ensureChildMap(conf, "gateway")
controlUi := ensureChildMap(gateway, "controlUi")
if _, ok := controlUi["dangerouslyDisableDeviceAuth"]; !ok {
controlUi["dangerouslyDisableDeviceAuth"] = true
}
allowedOrigins := append([]string(nil), config.AllowedOrigins...)
if len(allowedOrigins) > 0 {
controlUi["allowedOrigins"] = allowedOrigins
} else {
delete(controlUi, "allowedOrigins")
}
}

func ensureGatewaySecurityDefaults(conf map[string]interface{}) {
gateway := ensureChildMap(conf, "gateway")
controlUi := ensureChildMap(gateway, "controlUi")
if _, ok := controlUi["dangerouslyDisableDeviceAuth"]; !ok {
controlUi["dangerouslyDisableDeviceAuth"] = true
}
delete(controlUi, "dangerouslyAllowHostHeaderOriginFallback")
setTrustedProxies(gateway)
}

func setTrustedProxies(gateway map[string]interface{}) {

Check failure on line 1125 in agent/app/service/agents.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZzmyXhjnvODKcpvov1E&open=AZzmyXhjnvODKcpvov1E&pullRequest=12168
proxies := make([]string, 0, 4)
seen := map[string]struct{}{}
switch values := gateway["trustedProxies"].(type) {
case []interface{}:
for _, value := range values {
text := strings.TrimSpace(fmt.Sprintf("%v", value))
if text == "" {
continue
}
if _, ok := seen[text]; ok {
continue
}
seen[text] = struct{}{}
proxies = append(proxies, text)
}
case []string:
for _, value := range values {
text := strings.TrimSpace(value)
if text == "" {
continue
}
if _, ok := seen[text]; ok {
continue
}
seen[text] = struct{}{}
proxies = append(proxies, text)
}
}
if _, ok := seen[openclawTrustedProxyLoopback]; !ok {
proxies = append(proxies, openclawTrustedProxyLoopback)
}
gateway["trustedProxies"] = proxies
}

func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
Expand Down Expand Up @@ -1531,7 +1575,11 @@
if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName
item.AppVersion = appInstall.Version
item.WebUIPort = appInstall.HttpPort
if agentType == constant.AppOpenclaw {
item.WebUIPort = appInstall.HttpsPort
} else {
item.WebUIPort = appInstall.HttpPort
}
item.Path = appInstall.GetPath()
item.Status = appInstall.Status
item.Message = appInstall.Message
Expand All @@ -1544,6 +1592,82 @@
return item
}

func writeOpenclawCaddyfile(configPath string, allowedOrigins []string) error {
content, err := buildOpenclawCaddyfile(allowedOrigins)
if err != nil {
return err
}
dataDir := path.Dir(path.Dir(configPath))
caddyDir := path.Join(dataDir, "caddy")
fileOp := files.NewFileOp()
if !fileOp.Stat(caddyDir) {
if err := fileOp.CreateDir(caddyDir, constant.DirPerm); err != nil {
return err
}
}
caddyDataDir := path.Join(caddyDir, "data")
if !fileOp.Stat(caddyDataDir) {
if err := fileOp.CreateDir(caddyDataDir, constant.DirPerm); err != nil {
return err
}
}
if err := fileOp.ChmodR(caddyDataDir, openclawCaddyDataPerm, false); err != nil {
return err
}
return fileOp.SaveFile(path.Join(caddyDir, "Caddyfile"), content, 0644)
}

func buildOpenclawCaddyfile(allowedOrigins []string) (string, error) {
if len(allowedOrigins) == 0 {
return "", fmt.Errorf("allowed origins is required")
}
addresses := make([]string, 0, len(allowedOrigins))
seen := make(map[string]struct{}, len(allowedOrigins))
for _, origin := range allowedOrigins {
normalized, err := normalizeAllowedOrigin(origin)
if err != nil {
return "", err
}
parsed, err := url.Parse(normalized)
if err != nil {
return "", err
}
host := parsed.Hostname()
if strings.Contains(host, ":") {
host = "[" + host + "]"
}
address := fmt.Sprintf("https://%s:%d", host, openclawCaddyPort)
if _, ok := seen[address]; ok {
continue
}
seen[address] = struct{}{}
addresses = append(addresses, address)
}
if len(addresses) == 0 {
return "", fmt.Errorf("allowed origins is required")
}
if _, ok := seen[openclawCaddyLoopbackAddress]; !ok {
addresses = append(addresses, openclawCaddyLoopbackAddress)
}
content := `{
admin off
auto_https disable_redirects
default_sni 127.0.0.1
skip_install_trust
storage file_system {
root /data/caddy
}
}

` + strings.Join(addresses, ", ") + ` {
bind 0.0.0.0
tls internal
reverse_proxy 127.0.0.1:` + strconv.Itoa(openclawGatewayPort) + `
}
`
return content, nil
}

func checkAgentUpgradable(install model.AppInstall) bool {
if install.ID == 0 || install.Version == "" || install.Version == "latest" {
return false
Expand Down Expand Up @@ -1651,11 +1775,12 @@
}

type gatewayConfig struct {
Mode string `json:"mode"`
Bind string `json:"bind"`
Port int `json:"port"`
Auth gatewayAuth `json:"auth"`
ControlUi gatewayControlUi `json:"controlUi"`
Mode string `json:"mode"`
Bind string `json:"bind"`
Port int `json:"port"`
Auth gatewayAuth `json:"auth"`
ControlUi gatewayControlUi `json:"controlUi"`
TrustedProxies []string `json:"trustedProxies,omitempty"`
}

type gatewayControlUi struct {
Expand Down Expand Up @@ -1718,7 +1843,7 @@
DefaultProfile string `json:"defaultProfile"`
}

func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string, allowedOrigins []string) error {

Check failure on line 1846 in agent/app/service/agents.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 51 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=1Panel-dev_1Panel&issues=AZzmyXhjnvODKcpvov1F&open=AZzmyXhjnvODKcpvov1F&pullRequest=12168
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
}
Expand All @@ -1739,7 +1864,7 @@
Gateway: gatewayConfig{
Mode: "local",
Bind: "lan",
Port: 18789,
Port: openclawGatewayPort,
Auth: gatewayAuth{
Mode: "token",
Token: token,
Expand All @@ -1748,6 +1873,7 @@
DangerouslyDisableDeviceAuth: true,
AllowedOrigins: append([]string(nil), allowedOrigins...),
},
TrustedProxies: []string{openclawTrustedProxyLoopback},
},
Agents: agentsConfig{
Defaults: agentDefaults{
Expand Down Expand Up @@ -1830,7 +1956,17 @@
modelMap := ensureChildMap(defaultsMap, "model")
modelMap["primary"] = cfg.Agents.Defaults.Model.Primary

ensureGatewaySecurityDefaults(conf)
gatewayMap := ensureChildMap(conf, "gateway")
if _, ok := gatewayMap["mode"]; !ok {
gatewayMap["mode"] = "local"
}
if _, ok := gatewayMap["bind"]; !ok {
gatewayMap["bind"] = "lan"
}
if _, ok := gatewayMap["port"]; !ok {
gatewayMap["port"] = openclawGatewayPort
}
authMap := ensureChildMap(gatewayMap, "auth")
if _, ok := authMap["mode"]; !ok {
authMap["mode"] = "token"
Expand All @@ -1843,6 +1979,11 @@
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err
}
if allowedOrigins != nil {
if err := writeOpenclawCaddyfile(configPath, allowedOrigins); err != nil {
return err
}
}

envPath := path.Join(confDir, ".env")
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -679,13 +679,13 @@ const message = {
copawType: 'CoPaw',
appVersion: 'App Version',
webuiPort: 'WebUI Port',
bridgePort: 'Bridge Port',
allowedOrigins: 'Access Addresses',
allowedOriginsHelper:
'Enter one full access address per line, for example http://192.168.1.2:18789. Fill it manually if the default access address is not configured.',
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
'Enter one full HTTPS access address per line, for example https://192.168.1.2:18789. Fill it manually if the default access address is not configured.',
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
allowedOriginsRequired: 'Enter at least one access address',
allowedOriginsInvalid: 'Use the format http(s)://host-or-ip:port',
allowedOriginsHttpsOnly: 'Access addresses must start with https://',
provider: 'Provider',
apiKey: 'API Key',
baseUrl: 'Base URL',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/lang/modules/es-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -687,13 +687,13 @@ const message = {
copawType: 'CoPaw',
appVersion: 'Versiรณn de la app',
webuiPort: 'Puerto WebUI',
bridgePort: 'Puerto Bridge',
allowedOrigins: 'Direcciones de acceso',
allowedOriginsHelper:
'Introduce una direcciรณn de acceso completa por lรญnea, por ejemplo http://192.168.1.2:18789. Si no hay una direcciรณn predeterminada configurada, rellรฉnala manualmente.',
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
'Introduce una direcciรณn de acceso HTTPS completa por lรญnea, por ejemplo https://192.168.1.2:18789. Si no hay una direcciรณn predeterminada configurada, rellรฉnala manualmente.',
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
allowedOriginsRequired: 'Introduce al menos una direcciรณn de acceso',
allowedOriginsInvalid: 'Usa el formato http(s)://host-o-ip:puerto',
allowedOriginsHttpsOnly: 'Las direcciones de acceso deben comenzar con https://',
provider: 'Proveedor de modelos',
apiKey: 'Clave API',
baseUrl: 'URL base',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,13 @@ const message = {
copawType: 'CoPaw',
appVersion: 'ใ‚ขใƒ—ใƒชใƒใƒผใ‚ธใƒงใƒณ',
webuiPort: 'WebUI ใƒใƒผใƒˆ',
bridgePort: 'Bridge ใƒใƒผใƒˆ',
allowedOrigins: 'ใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚น',
allowedOriginsHelper:
'1 ่กŒใซ 1 ใคใšใคๅฎŒๅ…จใชใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ไพ‹: http://192.168.1.2:18789ใ€‚ใƒ‡ใƒ•ใ‚ฉใƒซใƒˆใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใŒๆœช่จญๅฎšใฎๅ ดๅˆใฏๆ‰‹ๅ‹•ใงๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚',
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
'1 ่กŒใซ 1 ใคใšใคๅฎŒๅ…จใช HTTPS ใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚ไพ‹: https://192.168.1.2:18789ใ€‚ใƒ‡ใƒ•ใ‚ฉใƒซใƒˆใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใŒๆœช่จญๅฎšใฎๅ ดๅˆใฏๆ‰‹ๅ‹•ใงๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„ใ€‚',
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
allowedOriginsRequired: 'ๅฐ‘ใชใใจใ‚‚ 1 ใคใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„',
allowedOriginsInvalid: 'http(s)://host-or-ip:port ใฎๅฝขๅผใงๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„',
allowedOriginsHttpsOnly: 'ใ‚ขใ‚ฏใ‚ปใ‚นใ‚ขใƒ‰ใƒฌใ‚นใฏ https:// ใงๅง‹ใ‚ใฆใใ ใ•ใ„',
provider: 'ใƒขใƒ‡ใƒซใƒ—ใƒญใƒใ‚คใƒ€ใƒผ',
apiKey: 'API ใ‚ญใƒผ',
baseUrl: 'ใƒ™ใƒผใ‚นURL',
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -672,13 +672,13 @@ const message = {
copawType: 'CoPaw',
appVersion: '์•ฑ ๋ฒ„์ „',
webuiPort: 'WebUI ํฌํŠธ',
bridgePort: 'Bridge ํฌํŠธ',
allowedOrigins: '์ ‘์† ์ฃผ์†Œ',
allowedOriginsHelper:
'ํ•œ ์ค„์— ํ•˜๋‚˜์˜ ์ „์ฒด ์ ‘์† ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. ์˜ˆ: http://192.168.1.2:18789. ๊ธฐ๋ณธ ์ ‘์† ์ฃผ์†Œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ˆ˜๋™์œผ๋กœ ์ž…๋ ฅํ•˜์„ธ์š”.',
allowedOriginsPlaceholder: 'http://192.168.1.2:18789',
'ํ•œ ์ค„์— ํ•˜๋‚˜์˜ ์ „์ฒด HTTPS ์ ‘์† ์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. ์˜ˆ: https://192.168.1.2:18789. ๊ธฐ๋ณธ ์ ‘์† ์ฃผ์†Œ๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ˆ˜๋™์œผ๋กœ ์ž…๋ ฅํ•˜์„ธ์š”.',
allowedOriginsPlaceholder: 'https://192.168.1.2:18789',
allowedOriginsRequired: '์ ‘์† ์ฃผ์†Œ๋ฅผ ํ•˜๋‚˜ ์ด์ƒ ์ž…๋ ฅํ•˜์„ธ์š”',
allowedOriginsInvalid: 'http(s)://host-or-ip:port ํ˜•์‹์œผ๋กœ ์ž…๋ ฅํ•˜์„ธ์š”',
allowedOriginsHttpsOnly: '์ ‘์† ์ฃผ์†Œ๋Š” https:// ๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค',
provider: '๋ชจ๋ธ ์ œ๊ณต์ž',
apiKey: 'API ํ‚ค',
baseUrl: '๊ธฐ๋ณธ URL',
Expand Down
Loading
Loading