magic-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +392 -0
  3. package/cmd/magic-agent/main.go +12 -0
  4. package/cmd/magic-client/main.go +144 -0
  5. package/go.mod +11 -0
  6. package/go.sum +12 -0
  7. package/internal/agent/arkclaw.go +587 -0
  8. package/internal/agent/arkclaw_stream.go +312 -0
  9. package/internal/agent/arkclaw_stream_test.go +428 -0
  10. package/internal/agent/arkclaw_test.go +827 -0
  11. package/internal/agent/ask.go +694 -0
  12. package/internal/agent/ask_test.go +587 -0
  13. package/internal/agent/attachments.go +200 -0
  14. package/internal/agent/attachments_test.go +481 -0
  15. package/internal/agent/capability.go +154 -0
  16. package/internal/agent/capability_test.go +190 -0
  17. package/internal/agent/claude.go +406 -0
  18. package/internal/agent/codebuddy.go +1044 -0
  19. package/internal/agent/codebuddy_account_test.go +128 -0
  20. package/internal/agent/codebuddy_gateway.go +479 -0
  21. package/internal/agent/codebuddy_gateway_stream.go +358 -0
  22. package/internal/agent/codebuddy_gateway_test.go +673 -0
  23. package/internal/agent/codex.go +599 -0
  24. package/internal/agent/codex_home.go +206 -0
  25. package/internal/agent/codex_home_test.go +130 -0
  26. package/internal/agent/codex_stream_test.go +97 -0
  27. package/internal/agent/codex_test.go +280 -0
  28. package/internal/agent/custom_models.go +197 -0
  29. package/internal/agent/custom_models_test.go +283 -0
  30. package/internal/agent/dsh.go +754 -0
  31. package/internal/agent/dsh_sdk.go +679 -0
  32. package/internal/agent/dsh_sdk_test.go +726 -0
  33. package/internal/agent/dsh_test.go +759 -0
  34. package/internal/agent/engine.go +469 -0
  35. package/internal/agent/engine_base.go +497 -0
  36. package/internal/agent/engine_base_test.go +308 -0
  37. package/internal/agent/engine_test.go +858 -0
  38. package/internal/agent/env.go +88 -0
  39. package/internal/agent/env_test.go +106 -0
  40. package/internal/agent/jsonextract.go +128 -0
  41. package/internal/agent/jsonextract_test.go +110 -0
  42. package/internal/agent/keepalive_test.go +216 -0
  43. package/internal/agent/llmengine.go +470 -0
  44. package/internal/agent/llmengine_test.go +551 -0
  45. package/internal/agent/llmhttp.go +437 -0
  46. package/internal/agent/llmhttp_test.go +304 -0
  47. package/internal/agent/llmmodels.go +205 -0
  48. package/internal/agent/login.go +27 -0
  49. package/internal/agent/main_test.go +23 -0
  50. package/internal/agent/models.go +686 -0
  51. package/internal/agent/models_test.go +877 -0
  52. package/internal/agent/openclaw.go +543 -0
  53. package/internal/agent/openclaw_acp.go +949 -0
  54. package/internal/agent/openclaw_acp_test.go +406 -0
  55. package/internal/agent/openclaw_test.go +451 -0
  56. package/internal/agent/output.go +191 -0
  57. package/internal/agent/permission.go +362 -0
  58. package/internal/agent/permission_test.go +526 -0
  59. package/internal/agent/prompt.go +65 -0
  60. package/internal/agent/runcmd.go +259 -0
  61. package/internal/agent/runcmd_test.go +81 -0
  62. package/internal/agent/runcmd_unix.go +56 -0
  63. package/internal/agent/runcmd_windows.go +66 -0
  64. package/internal/agent/runner.go +180 -0
  65. package/internal/agent/runner_test.go +263 -0
  66. package/internal/agent/spawnhook_test.go +96 -0
  67. package/internal/agent/stream.go +882 -0
  68. package/internal/agent/stream_test.go +565 -0
  69. package/internal/agent/streamjson.go +185 -0
  70. package/internal/agent/tags.go +52 -0
  71. package/internal/agent/toolkind.go +96 -0
  72. package/internal/agent/toolkind_test.go +70 -0
  73. package/internal/agent/tools_suffix_test.go +261 -0
  74. package/internal/agent/trae.go +342 -0
  75. package/internal/agent/workspace_test.go +166 -0
  76. package/internal/boundary/boundary_test.go +165 -0
  77. package/internal/cli/ask.go +1113 -0
  78. package/internal/cli/ask_test.go +1088 -0
  79. package/internal/cli/attach_test.go +239 -0
  80. package/internal/cli/contract_test.go +157 -0
  81. package/internal/cli/control.go +149 -0
  82. package/internal/cli/control_test.go +192 -0
  83. package/internal/cli/engines_test.go +205 -0
  84. package/internal/cli/events.go +198 -0
  85. package/internal/cli/events_test.go +166 -0
  86. package/internal/cli/keepalive_test.go +391 -0
  87. package/internal/cli/login.go +78 -0
  88. package/internal/cli/permission_test.go +295 -0
  89. package/internal/cli/prompt_source_test.go +222 -0
  90. package/internal/cli/root.go +752 -0
  91. package/internal/cli/session.go +486 -0
  92. package/internal/cli/session_test.go +202 -0
  93. package/internal/cli/sessionlog.go +325 -0
  94. package/internal/cli/sessionlog_command_test.go +204 -0
  95. package/internal/cli/sessionlog_test.go +262 -0
  96. package/internal/cli/sessionlog_user_test.go +129 -0
  97. package/internal/cli/toolkind_wire_test.go +84 -0
  98. package/internal/cli/workspace_test.go +188 -0
  99. package/internal/client/cors_test.go +136 -0
  100. package/internal/client/lan.go +68 -0
  101. package/internal/client/lan_test.go +53 -0
  102. package/internal/client/pair.go +163 -0
  103. package/internal/client/pair_test.go +109 -0
  104. package/internal/client/profile.go +184 -0
  105. package/internal/client/service.go +888 -0
  106. package/internal/client/service_test.go +654 -0
  107. package/internal/client/session_events_test.go +186 -0
  108. package/internal/client/session_messages_test.go +150 -0
  109. package/internal/config/config.go +439 -0
  110. package/internal/config/config_test.go +448 -0
  111. package/internal/session/session.go +447 -0
  112. package/internal/session/session_append.go +241 -0
  113. package/internal/session/session_append_test.go +161 -0
  114. package/internal/session/session_kill_unix_test.go +90 -0
  115. package/internal/session/session_mark_test.go +95 -0
  116. package/internal/session/session_test.go +237 -0
  117. package/internal/session/sessionlog.go +128 -0
  118. package/internal/session/sessionlog_read_test.go +157 -0
  119. package/npm/dist/darwin-arm64/magic-agent +0 -0
  120. package/npm/dist/darwin-x64/magic-agent +0 -0
  121. package/npm/dist/linux-arm64/magic-agent +0 -0
  122. package/npm/dist/linux-x64/magic-agent +0 -0
  123. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  124. package/npm/install.js +121 -0
  125. package/npm/lib/platform.js +78 -0
  126. package/package.json +36 -0
@@ -0,0 +1,362 @@
1
+ package agent
2
+
3
+ // permission.go - 四档权限模型(跨引擎统一语义)+ Claude Code 参数映射。
4
+ //
5
+ // 用户需求(2026-09-18):「按照4档 使用claudecode的参数映射一下实现集成」。
6
+ // 四档来自对五家 agent CLI(Claude Code / Codex / CodeBuddy / TRAE / OpenClaw)
7
+ // 权限机制的横向对比:它们的全部用户可选模式都能收敛到同一条轴 ——
8
+ // 「哪些动作自动放行,放行不了时由谁裁决」:
9
+ //
10
+ // 第 1 档 手动审批 manual 沙箱开启,只读放行,其余逐项由「用户」确认
11
+ // 第 2 档 自动接受编辑 accept-edits 沙箱开启,工作区内编辑放行,命令仍由「用户」确认
12
+ // 第 3 档 自动审批 auto 沙箱开启,沙箱内放行,越界交「LLM Guardian」判定
13
+ // 第 4 档 完全访问 full 沙箱关闭,命令直接在宿主机执行,无审批
14
+ //
15
+ // ── Claude Code 参数映射(本文件的核心) ───────────────────────────────────────
16
+ //
17
+ // 档位 --permission-mode settings.sandbox
18
+ // manual default enabled=true, autoAllowBashIfSandboxed=false
19
+ // accept-edits acceptEdits enabled=true, autoAllowBashIfSandboxed=false
20
+ // auto auto enabled=true, autoAllowBashIfSandboxed=true
21
+ // full bypassPermissions enabled=false
22
+ //
23
+ // 三个关键实现决定(都来自官方文档,不是猜的):
24
+ //
25
+ // 1. 档位走 `--permission-mode`,**不走** settings 的 `permissions.defaultMode`。
26
+ // 官方明确:`auto` 与 `bypassPermissions` 写在项目级 `.claude/settings.json`
27
+ // 或本地级 `.claude/settings.local.json` 里**不生效**(会被忽略,会话回落到
28
+ // Manual / 内建默认)。`--permission-mode` 是唯一在任意作用域都可靠的入口,
29
+ // 且它 overrides settings 里的 defaultMode。
30
+ //
31
+ // 2. 沙箱**只能**走 `--settings`。claude 没有 `--sandbox` 命令行参数
32
+ // (官方 CLI reference 的 flags 表全文无此参数),启用方式只有会话内 `/sandbox`、
33
+ // settings 文件的 `sandbox.enabled`、或 `--settings` 内联 JSON。
34
+ // 而 `--settings` 官方语义是「一个文件路径**或**一段内联 JSON」,不是可重复 flag ——
35
+ // 所以 MaxTokens 的 env 注入、sandbox、autoMode、permissions 必须**合并进同一份 JSON**。
36
+ // 这正是 agentSettingsPayload 存在的理由(早期 maxTokensSettings 只装 env)。
37
+ //
38
+ // 3. `autoAllowBashIfSandboxed` 是第 1/2 档与第 3 档的分水岭。该键默认值是 true,
39
+ // 所以第 1/2 档必须**显式写 false**,否则沙箱内的 Bash 会被自动放行,
40
+ // 第 2 档就退化成了「编辑与命令都不问」。
41
+ //
42
+ // 另外:第 1~3 档会写 `failIfUnavailable: true` 与 `allowUnsandboxedCommands: false`。
43
+ // 前者让沙箱起不来时**直接报错退出**,而不是静默降级成不沙箱运行 ——
44
+ // 静默降级是安全特性里最坏的结果(用户以为被保护着,其实没有)。
45
+ // 后者关闭 `dangerouslyDisableSandbox` 逃逸舱口(官方称 Strict sandbox mode);
46
+ // 需要放行的命令请用 PermissionOptions.ExcludedCommands 显式列出,而不是放宽全局。
47
+
48
+ import (
49
+ "encoding/json"
50
+ "sort"
51
+ "strings"
52
+ )
53
+
54
+ // PermissionTier 是四档权限模型的档位标识(跨引擎统一语义)。
55
+ type PermissionTier string
56
+
57
+ const (
58
+ // PermissionManual 第 1 档 · 手动审批:沙箱开启,只读放行,其余逐项由用户确认。
59
+ PermissionManual PermissionTier = "manual"
60
+ // PermissionAcceptEdits 第 2 档 · 自动接受编辑:沙箱开启,编辑放行,命令仍需确认。
61
+ PermissionAcceptEdits PermissionTier = "accept-edits"
62
+ // PermissionAuto 第 3 档 · 自动审批:沙箱开启,越界由 LLM Guardian 判定。
63
+ PermissionAuto PermissionTier = "auto"
64
+ // PermissionFull 第 4 档 · 完全访问:沙箱关闭,无审批。
65
+ PermissionFull PermissionTier = "full"
66
+ )
67
+
68
+ // DefaultPermissionTier 默认档位。
69
+ //
70
+ // 取 full 是为了**保持既有行为**:改造前 claude / codebuddy 在 --tools 非 off 时
71
+ // 恒传 `--dangerously-skip-permissions` / `-y`,语义就是第 4 档。默认改成别的档位
72
+ // 会是一次静默的行为变更(现有调用方的 agent 会突然开始弹审批 / 被沙箱拦),
73
+ // 因此这里不改变默认值,只把「可选」变成「可选且显式」。
74
+ // 需要更安全默认值的调用方请显式传 --permission auto(推荐)或 manual。
75
+ const DefaultPermissionTier = PermissionFull
76
+
77
+ // PermissionOptions 是四档模型之上的可选项(都是「加密」而非「放宽」)。
78
+ //
79
+ // 零值即最严格:严格沙箱、无额外放行、无自定义受信边界。
80
+ type PermissionOptions struct {
81
+ // ExcludedCommands 始终在沙箱外执行的命令(claude 的 sandbox.excludedCommands)。
82
+ // 用于 docker / watchman 这类与沙箱不兼容的工具,比整体放宽沙箱精确得多。
83
+ ExcludedCommands []string
84
+
85
+ // AllowedDomains 沙箱网络白名单(sandbox.network.allowedDomains)。
86
+ // 留空表示不改动 claude 自身的网络策略。
87
+ AllowedDomains []string
88
+
89
+ // AutoModeEnvironment 第 3 档分类器的「受信边界」描述(自然语言,不是正则)。
90
+ // 官方要求写成「像给新工程师介绍基础设施」那样,例如
91
+ // "Source control: github.example.com/acme-corp"。
92
+ // 非空时会以 ["$defaults", ...] 的形式注入 autoMode.environment,
93
+ // 即保留内建默认条目再追加自定义条目。
94
+ // 只在第 3 档生效(其余档位没有分类器)。
95
+ AutoModeEnvironment []string
96
+
97
+ // Deny 追加的 deny 规则(permissions.deny)。deny 在所有档位(含第 4 档)
98
+ // 都先于 allow / ask 求值,且不可被白名单例外覆盖 —— 这是唯一能穿透
99
+ // 「完全访问」的硬红线,建议放 rm -rf 之类不可逆操作。
100
+ Deny []string
101
+
102
+ // Ask 追加的 ask 规则(permissions.ask)。命中即强制人工审批,
103
+ // 在第 3 档下**分类器也无法自动放行** —— 这就是官方推荐的「人为检查点」,
104
+ // 例如 Bash(git push *) / Bash(gh pr create *)。
105
+ Ask []string
106
+
107
+ // AllowUnsandboxedCommands 是否保留「逃逸舱口」(sandbox.allowUnsandboxedCommands)。
108
+ // 默认 false = 严格沙箱:模型无法用 dangerouslyDisableSandbox 把命令挪到沙箱外。
109
+ // 第 4 档无沙箱,本项不生效。
110
+ AllowUnsandboxedCommands bool
111
+ }
112
+
113
+ // permissionOrDefault nil / 空串档位回落默认档(保持既有行为)。
114
+ func permissionOrDefault(t PermissionTier) PermissionTier {
115
+ if t == "" {
116
+ return DefaultPermissionTier
117
+ }
118
+ return t
119
+ }
120
+
121
+ // ParsePermissionTier 解析 --permission 取值,返回档位与是否合法。
122
+ //
123
+ // 接受四档规范名与常见别名(含数字档位),大小写 / 连字符 / 下划线不敏感:
124
+ //
125
+ // manual manual | default | ask | 1
126
+ // accept-edits accept-edits | acceptedits | edits | 2
127
+ // auto auto | guardian | llm | classifier | 3
128
+ // full full | bypass | bypasspermissions | yolo | dangerous | 4
129
+ //
130
+ // 注意 "default" 归到 manual:这与 claude 自己的命名一致(claude 的
131
+ // `--permission-mode default` 就是 UI 上那个 Manual 档),避免同一个词在两处
132
+ // 表示不同含义。
133
+ func ParsePermissionTier(s string) (PermissionTier, bool) {
134
+ key := strings.ToLower(strings.TrimSpace(s))
135
+ key = strings.NewReplacer("-", "", "_", "", " ", "").Replace(key)
136
+ switch key {
137
+ case "manual", "default", "ask", "1":
138
+ return PermissionManual, true
139
+ case "acceptedits", "acceptedit", "edits", "edit", "2":
140
+ return PermissionAcceptEdits, true
141
+ case "auto", "guardian", "llm", "classifier", "3":
142
+ return PermissionAuto, true
143
+ case "full", "bypass", "bypasspermissions", "yolo", "dangerous", "4":
144
+ return PermissionFull, true
145
+ }
146
+ return "", false
147
+ }
148
+
149
+ // PermissionTiers 返回四档规范名(按档位从严到宽),供 --help / 报错文案使用。
150
+ func PermissionTiers() []string {
151
+ return []string{
152
+ string(PermissionManual),
153
+ string(PermissionAcceptEdits),
154
+ string(PermissionAuto),
155
+ string(PermissionFull),
156
+ }
157
+ }
158
+
159
+ // PermissionSupportOf 返回某引擎对四档模型的落地方式(机器可读,进 --engines 输出):
160
+ //
161
+ // "flag:--permission-mode" claude / codebuddy / codebuddy-ai:CLI 有原生 --permission-mode,
162
+ // 沙箱与 autoMode 经 --settings 注入(同族 flag 面)
163
+ // "none" 其余引擎暂未接线(trae 的档位在 yaml 配置里、codex 是
164
+ // 「沙箱 × 审批」两轴、llm 无工具语义、openclaw 走
165
+ // exec.mode 五档、arkclaw 由网关侧决定;dsh 只有
166
+ // read-only / workspace-write 两个配置层预设,没有
167
+ // 「沙箱关闭 / 无审批」那一档,四档无法一一落地;
168
+ // codebuddy-gateway 的档位由**网关进程**的 --permission-mode
169
+ // 决定,且网关会把远程任务的权限强制切到 bypassPermissions
170
+ // —— per-call 传四档没有落地通道)
171
+ // —— 传 --permission 会**明确报错**,
172
+ // 而不是静默忽略一个安全设置
173
+ func PermissionSupportOf(engine string) string {
174
+ // 具名 agent(如 MagicAI)先归到它协议的家族名,再查表 —— 见 CapabilityFamilyOf。
175
+ engine = CapabilityFamilyOf(engine)
176
+ switch engine {
177
+ case "claude", "codebuddy", "codebuddy-ai":
178
+ return "flag:--permission-mode"
179
+ default:
180
+ return "none"
181
+ }
182
+ }
183
+
184
+ // PermissionSupported 报告某引擎是否已接线四档模型。
185
+ func PermissionSupported(engine string) bool {
186
+ return PermissionSupportOf(engine) != "none"
187
+ }
188
+
189
+ // claudePermissionMode 把档位映射到 claude / codebuddy `--permission-mode` 的取值。
190
+ //
191
+ // 取值必须是 CLI 认的字面量(claude 官方:`default` / `acceptEdits` / `plan` /
192
+ // `auto` / `dontAsk` / `bypassPermissions`;codebuddy 同族,另支持 `manual`
193
+ // 作为 default 的别名)。这里一律用不带别名的规范值,兼容面最宽。
194
+ func claudePermissionMode(t PermissionTier) string {
195
+ switch permissionOrDefault(t) {
196
+ case PermissionManual:
197
+ return "default"
198
+ case PermissionAcceptEdits:
199
+ return "acceptEdits"
200
+ case PermissionAuto:
201
+ return "auto"
202
+ case PermissionFull:
203
+ return "bypassPermissions"
204
+ }
205
+ return "default"
206
+ }
207
+
208
+ // sandboxSettings 构造 settings.sandbox 子对象。第 4 档返回显式关闭。
209
+ //
210
+ // engine 用于区分两家文档确认过的键集:CodeBuddy 文档未列 `failIfUnavailable`,
211
+ // 未知键的行为没有保证,故只在 claude 上注入。
212
+ func sandboxSettings(tier PermissionTier, opts PermissionOptions, engine string) map[string]any {
213
+ if permissionOrDefault(tier) == PermissionFull {
214
+ // 第 4 档:显式关闭沙箱。
215
+ // 不只是「保持与改造前一致」—— 显式写出来才真正保证档位语义:
216
+ // 若用户的 ~/.claude/settings.json 里开着 sandbox.enabled=true,
217
+ // 不写这一项会让第 4 档继承到沙箱,与「沙箱关闭」的档位承诺矛盾。
218
+ // --settings 属于 flag 层,优先级高于 user settings,故能覆盖它。
219
+ return map[string]any{"enabled": false}
220
+ }
221
+ sb := map[string]any{
222
+ "enabled": true,
223
+ // 分水岭:只有第 3 档让沙箱内的 Bash 免确认。
224
+ // 该键默认值是 true,第 1/2 档必须显式写 false,否则第 2 档会退化成
225
+ // 「编辑与命令都不问」。
226
+ "autoAllowBashIfSandboxed": tier == PermissionAuto,
227
+ // 默认关闭逃逸舱口(Strict sandbox mode)。
228
+ "allowUnsandboxedCommands": opts.AllowUnsandboxedCommands,
229
+ }
230
+ if len(opts.ExcludedCommands) > 0 {
231
+ sb["excludedCommands"] = normalizeList(opts.ExcludedCommands)
232
+ }
233
+ if len(opts.AllowedDomains) > 0 {
234
+ sb["network"] = map[string]any{
235
+ "allowedDomains": normalizeList(opts.AllowedDomains),
236
+ }
237
+ }
238
+ if engine == "claude" {
239
+ // 沙箱起不来就报错退出,不静默降级成不沙箱运行。
240
+ sb["failIfUnavailable"] = true
241
+ }
242
+ return sb
243
+ }
244
+
245
+ // autoModeSettings 构造第 3 档的 settings.autoMode 子对象;非第 3 档或无自定义
246
+ // 边界时返回 nil。
247
+ //
248
+ // 官方限制:分类器只从 `~/.claude/settings.json`、托管设置、`--settings` 读 autoMode,
249
+ // **不读**项目内 `.claude/settings.json` 与 `.claude/settings.local.json`
250
+ // (防止仓库注入自己的 allow 规则)。我们走 `--settings`,属于被读取的那一档。
251
+ func autoModeSettings(tier PermissionTier, opts PermissionOptions) map[string]any {
252
+ if permissionOrDefault(tier) != PermissionAuto {
253
+ return nil
254
+ }
255
+ env := normalizeList(opts.AutoModeEnvironment)
256
+ if len(env) == 0 {
257
+ return nil
258
+ }
259
+ // "$defaults" 是官方占位符:把内建默认条目插到这个位置,
260
+ // 我们的自定义条目追加在后。不写 "$defaults" 等于**完整替换**内建规则,
261
+ // 会丢掉内建安全规则,故必须带上。
262
+ return map[string]any{"environment": append([]string{"$defaults"}, env...)}
263
+ }
264
+
265
+ // permissionRules 构造 settings.permissions 子对象;无规则时返回 nil。
266
+ func permissionRules(opts PermissionOptions) map[string]any {
267
+ deny, ask := normalizeList(opts.Deny), normalizeList(opts.Ask)
268
+ if len(deny) == 0 && len(ask) == 0 {
269
+ return nil
270
+ }
271
+ p := map[string]any{}
272
+ if len(deny) > 0 {
273
+ p["deny"] = deny
274
+ }
275
+ if len(ask) > 0 {
276
+ p["ask"] = ask
277
+ }
278
+ return p
279
+ }
280
+
281
+ // agentSettingsPayload 组装 claude / codebuddy `--settings` 的**单份** JSON 载荷。
282
+ //
283
+ // 为什么必须合并:`--settings` 官方语义是「一个文件路径或一段内联 JSON」,
284
+ // 不是可重复 flag。MaxTokens 的 env 注入、sandbox、autoMode、permissions
285
+ // 若分多次传,第二次会覆盖第一次(或行为未定义)。所以这里一次性组装。
286
+ //
287
+ // 工具关闭(--tools off)时只注入 env:没有工具调用,沙箱与权限规则都无意义,
288
+ // 而 `failIfUnavailable: true` 反而可能让一次纯 chat 因为沙箱起不来而失败。
289
+ //
290
+ // 返回 (payload, true) 表示需要传 --settings;("", false) 表示本次无需注入。
291
+ func agentSettingsPayload(req Request, engine string) (string, bool) {
292
+ tier := permissionOrDefault(req.Permission)
293
+ opts := req.PermissionOptions
294
+ settings := map[string]any{}
295
+
296
+ if env := maxTokensEnv(req.MaxTokens); env != nil {
297
+ settings["env"] = env
298
+ }
299
+
300
+ // 沙箱 / autoMode / 权限规则只在真会调用工具时注入。
301
+ if !toolsIsOff(req) {
302
+ settings["sandbox"] = sandboxSettings(tier, opts, engine)
303
+ if am := autoModeSettings(tier, opts); am != nil {
304
+ settings["autoMode"] = am
305
+ }
306
+ if rules := permissionRules(opts); rules != nil {
307
+ settings["permissions"] = rules
308
+ }
309
+ }
310
+
311
+ if len(settings) == 0 {
312
+ return "", false
313
+ }
314
+ data, err := json.Marshal(settings)
315
+ if err != nil {
316
+ return "", false
317
+ }
318
+ return string(data), true
319
+ }
320
+
321
+ // normalizeList 去空白、去空项、去重并保持稳定顺序。
322
+ // 让「同一组选项」永远产出同一份 JSON —— 否则参数断言类测试会随机翻车。
323
+ func normalizeList(in []string) []string {
324
+ if len(in) == 0 {
325
+ return nil
326
+ }
327
+ seen := make(map[string]bool, len(in))
328
+ out := make([]string, 0, len(in))
329
+ for _, raw := range in {
330
+ for _, part := range strings.Split(raw, ",") {
331
+ v := strings.TrimSpace(part)
332
+ if v == "" || seen[v] {
333
+ continue
334
+ }
335
+ seen[v] = true
336
+ out = append(out, v)
337
+ }
338
+ }
339
+ return out
340
+ }
341
+
342
+ // permissionSummary 一行式描述某档位的行为,供 --help 与 -v 输出复用。
343
+ func permissionSummary(t PermissionTier) string {
344
+ switch permissionOrDefault(t) {
345
+ case PermissionManual:
346
+ return "沙箱开启,只读放行,其余逐项确认(claude --permission-mode default)"
347
+ case PermissionAcceptEdits:
348
+ return "沙箱开启,编辑放行,命令逐条确认(claude --permission-mode acceptEdits)"
349
+ case PermissionAuto:
350
+ return "沙箱开启,越界由 LLM Guardian 判定(claude --permission-mode auto)"
351
+ case PermissionFull:
352
+ return "沙箱关闭,无审批(claude --permission-mode bypassPermissions)"
353
+ }
354
+ return ""
355
+ }
356
+
357
+ // SortedPermissionTiers 排序后的档位名(给 --engines / 报错文案用,保证稳定输出)。
358
+ func SortedPermissionTiers() []string {
359
+ out := PermissionTiers()
360
+ sort.Strings(out)
361
+ return out
362
+ }