Files
dingtalk-workspace-cli/internal/pat/browser_policy.go

261 lines
7.4 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pat
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/contract"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
const browserPolicyFile = "pat_policy.json"
var (
patPolicyReadFile = os.ReadFile
patPolicyUserHomeDir = os.UserHomeDir
patPolicyMarshal = json.MarshalIndent
patPolicyAtomicWrite = helpers.AtomicWriteJSON
)
type browserPolicyValue struct {
OpenBrowser bool `json:"openBrowser"`
}
type BrowserPolicy struct {
Default *browserPolicyValue `json:"default,omitempty"`
Agents map[string]browserPolicyValue `json:"agents,omitempty"`
}
type BrowserPolicySelection struct {
Scope string `json:"scope"`
AgentCode string `json:"agentCode,omitempty"`
OpenBrowser bool `json:"openBrowser"`
Source string `json:"source"`
}
func patPolicyPath(configDir string) string {
return filepath.Join(configDir, browserPolicyFile)
}
func patConfigDir() string {
if envDir := os.Getenv("DWS_CONFIG_DIR"); envDir != "" {
return envDir
}
if fn := edition.Get().ConfigDir; fn != nil {
return fn()
}
homeDir, err := patPolicyUserHomeDir()
if err != nil {
return ".dws"
}
return filepath.Join(homeDir, ".dws")
}
func LoadBrowserPolicy(configDir string) (*BrowserPolicy, error) {
path := patPolicyPath(configDir)
data, err := patPolicyReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &BrowserPolicy{}, nil
}
return nil, fmt.Errorf("reading PAT browser policy: %w", err)
}
var policy BrowserPolicy
if err := json.Unmarshal(data, &policy); err != nil {
return nil, fmt.Errorf("parsing PAT browser policy: %w", err)
}
if policy.Agents == nil {
policy.Agents = map[string]browserPolicyValue{}
}
return &policy, nil
}
func saveBrowserPolicy(configDir string, policy *BrowserPolicy) error {
if policy == nil {
policy = &BrowserPolicy{}
}
if policy.Agents == nil {
policy.Agents = map[string]browserPolicyValue{}
}
data, err := patPolicyMarshal(policy, "", " ")
if err != nil {
return fmt.Errorf("marshaling PAT browser policy: %w", err)
}
data = append(data, '\n')
if err := patPolicyAtomicWrite(patPolicyPath(configDir), data); err != nil {
return fmt.Errorf("writing PAT browser policy: %w", err)
}
return nil
}
func ResolveBrowserPolicy(configDir, explicitAgentCode string) (BrowserPolicySelection, error) {
agentCode, err := resolveAgentCode(explicitAgentCode)
if err != nil {
return BrowserPolicySelection{}, err
}
policy, err := LoadBrowserPolicy(configDir)
if err != nil {
return BrowserPolicySelection{}, err
}
if agentCode != "" {
if entry, ok := policy.Agents[agentCode]; ok {
return BrowserPolicySelection{
Scope: "agent",
AgentCode: agentCode,
OpenBrowser: entry.OpenBrowser,
Source: "agent",
}, nil
}
}
if policy.Default != nil {
return BrowserPolicySelection{
Scope: "default",
OpenBrowser: policy.Default.OpenBrowser,
Source: "default",
}, nil
}
return BrowserPolicySelection{
Scope: "builtin_default",
AgentCode: agentCode,
OpenBrowser: true,
Source: "builtin_default",
}, nil
}
func EffectiveOpenBrowser(configDir string) bool {
selection, err := ResolveBrowserPolicy(configDir, "")
if err != nil {
return true
}
return selection.OpenBrowser
}
func resolveBrowserPolicyWriteAgentCode(explicitAgentCode string) (string, error) {
agentCode := strings.TrimSpace(explicitAgentCode)
if agentCode == "" {
return "", nil
}
if err := validateAgentCode(agentCode); err != nil {
return "", err
}
return agentCode, nil
}
func SetBrowserPolicy(configDir, explicitAgentCode string, enabled bool) (BrowserPolicySelection, error) {
agentCode, err := resolveBrowserPolicyWriteAgentCode(explicitAgentCode)
if err != nil {
return BrowserPolicySelection{}, err
}
policy, err := LoadBrowserPolicy(configDir)
if err != nil {
return BrowserPolicySelection{}, err
}
if policy.Agents == nil {
policy.Agents = map[string]browserPolicyValue{}
}
if agentCode != "" {
policy.Agents[agentCode] = browserPolicyValue{OpenBrowser: enabled}
if err := saveBrowserPolicy(configDir, policy); err != nil {
return BrowserPolicySelection{}, err
}
return BrowserPolicySelection{
Scope: "agent",
AgentCode: agentCode,
OpenBrowser: enabled,
Source: "agent",
}, nil
}
policy.Default = &browserPolicyValue{OpenBrowser: enabled}
if err := saveBrowserPolicy(configDir, policy); err != nil {
return BrowserPolicySelection{}, err
}
return BrowserPolicySelection{
Scope: "default",
OpenBrowser: enabled,
Source: "default",
}, nil
}
func newBrowserPolicyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "browser-policy",
Short: "配置 PAT 授权时是否打开浏览器",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
if !cmd.Flags().Changed("enabled") {
return fmt.Errorf("--enabled is required")
}
enabled, _ := cmd.Flags().GetBool("enabled")
agentCode, _ := cmd.Flags().GetString("agentCode")
selection, err := SetBrowserPolicy(patConfigDir(), agentCode, enabled)
if err != nil {
return err
}
return output.WriteJSON(cmd.OutOrStdout(), selection)
},
}
cmd.Flags().Bool("enabled", false, "PAT 撞墙时是否允许本地打开浏览器")
cmd.Flags().String("agentCode", "", "Agent 唯一标识(可选;不填则写入全局默认策略,不从 env DINGTALK_DWS_AGENTCODE 回退)")
helpers.DeclareLeafMetadata(cmd, helpers.LeafSpec{
Safety: contract.SafetySpec{
Effect: "write", Risk: "medium",
Confirmation: "not_required", Idempotency: "unknown",
},
Contract: helpers.LeafContract{
Identity: contract.ToolIdentitySpec{
ProductID: "pat",
Name: "browser_policy",
CanonicalPath: "pat.browser_policy",
CLIPath: "pat browser-policy",
PrimaryCLIPath: "pat browser-policy",
},
Description: "配置 PAT 授权流程是否允许打开本地浏览器",
Interface: &contract.InterfaceSpec{
Mode: "local",
Availability: "available",
Reason: "命令仅操作本地进程或策略文件,不调用 MCP 接口",
},
Selection: contract.SelectionSpec{
AgentSummary: "配置 PAT 授权流程是否允许打开本地浏览器",
UseWhen: []string{"需要允许或禁止某 Agent 在 PAT 授权时打开浏览器"},
AvoidWhen: []string{"需要授予产品 scope 时用 pat chmod,而不是改浏览器策略"},
Examples: []string{
"dws pat browser-policy --enabled --format json",
"dws pat browser-policy --enabled=false --agentCode <AGENT_CODE> --format json",
},
},
},
})
return cmd
}