From 7c84620d768bb2e583fc6428f70586143b5d14a0 Mon Sep 17 00:00:00 2001 From: maguowei Date: Tue, 6 Oct 2026 15:43:02 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(profile-editor):=20=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=8E=E6=B2=99=E7=AE=B1=E6=8E=A8=E8=8D=90=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=20dotfiles=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AD=90=E8=BF=9B=E7=A8=8B=E5=87=AD=E6=8D=AE=E6=B8=85=E7=90=86?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 权限预设:allow 收敛为 WebSearch,ask 精简并补齐 publish,deny 补齐 rm 变体、 refspec 强推、凭据导出命令与 gh/gcloud/git-credentials 路径 - 沙箱预设:新增 credentials(envVars/files 按 name/path 去重追加), excludedCommands 由 git * 细化为联网类 git 子命令及 gh/aws/gcloud/kubectl 等 - 常用选项新增 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB,默认关闭,同步用户手册 Co-Authored-By: Claude Opus 5.5 --- docs/user-manual.md | 2 +- docs/user-manual.zh-CN.md | 2 +- .../__tests__/ProfileEditor.test.tsx | 33 ++++- .../profile-editor/permission-presets.ts | 140 +++++------------- .../profile-editor/sandbox-presets.ts | 108 +++++++++++++- .../profile-editor/settings-form-registry.ts | 16 ++ 6 files changed, 188 insertions(+), 113 deletions(-) diff --git a/docs/user-manual.md b/docs/user-manual.md index be7483bd..de847709 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -115,7 +115,7 @@ The configuration editor on the right is divided into several sections. - **Basic information**: name (required), description, optional provider (selecting one automatically fills in the connection endpoint and model mapping). - **Authentication**: the authentication key is written to `env.ANTHROPIC_AUTH_TOKEN`; the API endpoint is written to `env.ANTHROPIC_BASE_URL`, and when it is not set, the model test uses the official Anthropic endpoint. - **Models and behavior**: the default model (an editable dropdown whose candidates come from the current provider's models, or you can type a custom model), the effort level (`auto`/`low`/`medium`/`high`/`xhigh`/`max`), the Opus / Sonnet / Haiku default models (also editable dropdowns), the Subagent model, the reply language, and the output style. -- **Common options**: override common Claude Code switches such as deep thinking, thinking summaries, Fast Mode, disable Hooks, disable AI attribution, LSP tools, Tool Search, the new Init, no flicker, and Agent Teams. See the editor for the full list. +- **Common options**: override common Claude Code switches such as deep thinking, thinking summaries, Fast Mode, disable Hooks, disable AI attribution, LSP tools, Tool Search, the new Init, no flicker, subprocess credential scrubbing, and Agent Teams. See the editor for the full list. - **Environment variables**: maintain `env` keys other than authentication and common options. Duplicate keys, invalid JSON, or an unsaved row edit will block saving. - **Permissions**: default mode, disable `bypassPermissions`, allow / deny / ask rules, additional directories, and recommended rule presets. - **Sandbox**: can be enabled or disabled, with recommended presets to add; complex configurations can switch to JSON mode. diff --git a/docs/user-manual.zh-CN.md b/docs/user-manual.zh-CN.md index 2b98243d..ece38baa 100644 --- a/docs/user-manual.zh-CN.md +++ b/docs/user-manual.zh-CN.md @@ -115,7 +115,7 @@ Skills 对应 `~/.claude/skills//SKILL.md`。启用的 Skill 保存在 `~/.c - **基础信息**:名称(必填)、描述、可选供应商(选择后自动带入连接地址与模型映射)。 - **认证**:认证密钥写入 `env.ANTHROPIC_AUTH_TOKEN`;API 地址写入 `env.ANTHROPIC_BASE_URL`,未设置时模型测试使用 Anthropic 官方地址。 - **模型与行为**:默认模型(可输入下拉框,候选取自当前供应商的模型,也可手工输入自定义模型)、努力级别(`auto`/`low`/`medium`/`high`/`xhigh`/`max`)、Opus / Sonnet / Haiku 默认模型(同为可输入下拉框)、Subagent 模型、回复语言、输出风格。 -- **常用选项**:覆盖深度思考、Thinking 摘要、Fast Mode、禁用 Hooks、禁用 AI 署名、LSP 工具、Tool Search、新版 Init、无闪烁、Agent Teams 等 Claude Code 常用开关。具体可在编辑器内查看。 +- **常用选项**:覆盖深度思考、Thinking 摘要、Fast Mode、禁用 Hooks、禁用 AI 署名、LSP 工具、Tool Search、新版 Init、无闪烁、子进程凭据清理、Agent Teams 等 Claude Code 常用开关。具体可在编辑器内查看。 - **环境变量**:维护除认证和常用选项外的 `env` 键。重复键、非法 JSON 或未保存的行编辑会阻止保存。 - **权限**:默认模式、禁用 `bypassPermissions`、允许 / 拒绝 / 询问规则、附加目录、推荐规则预设。 - **Sandbox**:可启用或关闭,可添加推荐预设;复杂配置可切换 JSON 模式。 diff --git a/src/components/__tests__/ProfileEditor.test.tsx b/src/components/__tests__/ProfileEditor.test.tsx index 6df22ad3..50e78c68 100644 --- a/src/components/__tests__/ProfileEditor.test.tsx +++ b/src/components/__tests__/ProfileEditor.test.tsx @@ -12,6 +12,7 @@ import { OFFICIAL_MARKETPLACE_ID, OFFICIAL_MARKETPLACE_REPO, } from "../profile-editor/marketplace-presets"; +import { RECOMMENDED_SANDBOX_PRESET } from "../profile-editor/sandbox-presets"; import { ThemeProvider } from "../theme-provider"; const { invokeMock, showToastMock, fetchMock, openDialogMock, openUrlMock } = vi.hoisted(() => ({ @@ -683,12 +684,12 @@ describe("ProfileEditor", () => { "aria-expanded", "false", ); - expect(within(commonSection).getByText("已启用 0/15")).toBeInTheDocument(); + expect(within(commonSection).getByText("已启用 0/16")).toBeInTheDocument(); expect(within(commonSection).queryByRole("button", { name: "控件" })).not.toBeInTheDocument(); expect(within(commonSection).queryByRole("button", { name: "JSON" })).not.toBeInTheDocument(); const commonHeader = within(commonSection) - .getByText("已启用 0/15") + .getByText("已启用 0/16") .closest('[data-slot="settings-section-header"]'); expect(commonHeader).toHaveClass("cursor-pointer"); fireEvent.click(commonHeader as HTMLElement); @@ -698,7 +699,7 @@ describe("ProfileEditor", () => { expect( within(commonSection).queryByRole("combobox", { name: "输出风格" }), ).not.toBeInTheDocument(); - expect(within(commonSection).getAllByRole("switch")).toHaveLength(15); + expect(within(commonSection).getAllByRole("switch")).toHaveLength(16); expect(within(commonSection).getByText("默认启用深度思考")).toBeInTheDocument(); expect(within(commonSection).getByText("显示 Thinking 摘要")).toBeInTheDocument(); expect(within(commonSection).getByText("接受计划时显示清理上下文")).toBeInTheDocument(); @@ -2605,7 +2606,7 @@ describe("ProfileEditor", () => { fireEvent.click(within(permissionsSection).getByRole("button", { name: "展开 询问规则" })); fireEvent.click(within(permissionsSection).getByRole("button", { name: "展开 拒绝规则" })); - expect(within(permissionsSection).getByLabelText("允许规则 1")).toHaveValue("Bash(pwd)"); + expect(within(permissionsSection).getByLabelText("允许规则 1")).toHaveValue("WebSearch"); expect(within(permissionsSection).getByLabelText("询问规则 1")).toHaveValue("Bash(rm *)"); expect(within(permissionsSection).getByLabelText("拒绝规则 1")).toHaveValue("Bash(sudo *)"); @@ -2623,10 +2624,11 @@ describe("ProfileEditor", () => { disableAutoMode: "disable", additionalDirectories: ["~/projects/shared"], }); - expect(savedPermissions?.allow).toContain("Bash(go test *)"); + expect(savedPermissions?.allow).toEqual(["WebSearch"]); expect(savedPermissions?.allow).not.toContain("Bash(old-allow *)"); - expect(savedPermissions?.ask).toContain("Bash(curl *)"); + expect(savedPermissions?.ask).toContain("Bash(pnpm publish *)"); expect(savedPermissions?.deny).toContain("Bash(git reset --hard*)"); + expect(savedPermissions?.deny).toContain("Bash(printenv*)"); expect(savedPermissions?.deny).not.toContain("Read(**/config.yaml)"); }, 10_000); @@ -3018,6 +3020,9 @@ describe("ProfileEditor", () => { filesystem: { allowWrite: ["/tmp/build"], }, + credentials: { + envVars: [{ name: "GH_TOKEN", mode: "deny" }], + }, excludedCommands: ["pnpm *"], network: { allowedDomains: ["example.com"], @@ -3048,7 +3053,17 @@ describe("ProfileEditor", () => { allowWrite: ["/tmp/build"], }, }); - expect(savedSandbox?.excludedCommands).toEqual(["pnpm *", "docker *", "git *"]); + expect(savedSandbox?.excludedCommands).toEqual([ + "pnpm *", + ...RECOMMENDED_SANDBOX_PRESET.excludedCommands, + ]); + const savedCredentials = savedSandbox?.credentials as Record | undefined; + const savedEnvVars = savedCredentials?.envVars as Array<{ name: string }> | undefined; + // 已存在的 GH_TOKEN 不重复追加 + expect(savedEnvVars?.[0]).toEqual({ name: "GH_TOKEN", mode: "deny" }); + expect(savedEnvVars?.filter((entry) => entry.name === "GH_TOKEN")).toHaveLength(1); + expect(savedEnvVars).toHaveLength(RECOMMENDED_SANDBOX_PRESET.credentials.envVars.length); + expect(savedCredentials?.files).toEqual(RECOMMENDED_SANDBOX_PRESET.credentials.files); const savedNetwork = savedSandbox?.network as Record | undefined; expect(savedNetwork).toMatchObject({ allowedDomains: ["example.com"], @@ -4287,7 +4302,7 @@ describe("ProfileEditor", () => { renderEditor({ profile: null, onSave }); const commonSection = getSection("常用选项"); - expect(within(commonSection).getByText("已启用 8/15")).toBeInTheDocument(); + expect(within(commonSection).getByText("已启用 8/16")).toBeInTheDocument(); toggleAccordionSection("常用选项"); for (const label of [ "默认启用深度思考", @@ -4312,6 +4327,7 @@ describe("ProfileEditor", () => { "禁用所有 Hooks", "尊重 .gitignore", "禁用自动更新", + "子进程凭据清理", "显式启用 Tool Search", "启用 Agent Teams", ]) { @@ -4350,6 +4366,7 @@ describe("ProfileEditor", () => { expect(saved.settings.env).not.toHaveProperty("DISABLE_AUTOUPDATER"); expect(saved.settings.env).not.toHaveProperty("ENABLE_TOOL_SEARCH"); expect(saved.settings.env).not.toHaveProperty("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"); + expect(saved.settings.env).not.toHaveProperty("CLAUDE_CODE_SUBPROCESS_ENV_SCRUB"); }); function opencodeGoProvider(): Provider { diff --git a/src/components/profile-editor/permission-presets.ts b/src/components/profile-editor/permission-presets.ts index 76a97e8d..23029a9f 100644 --- a/src/components/profile-editor/permission-presets.ts +++ b/src/components/profile-editor/permission-presets.ts @@ -1,68 +1,10 @@ export const RECOMMENDED_PERMISSION_RULES = { - allow: [ - "Bash(pwd)", - "Bash(date *)", - "Bash(whoami)", - "Bash(id)", - "Bash(which *)", - "Bash(file *)", - "Bash(df *)", - "Bash(ps *)", - "Bash(pgrep *)", - "Bash(lsof *)", - "Bash(* --version)", - "Bash(* --help *)", - "Bash(rg *)", - "Bash(sed *)", - "Bash(awk *)", - "Bash(fd *)", - "Bash(jq *)", - "Bash(yq *)", - "Bash(sort *)", - "Bash(uniq *)", - "Bash(cut *)", - "Bash(tr *)", - "Bash(echo *)", - "Bash(printf *)", - "Bash(touch *)", - "Bash(mkdir *)", - "Bash(go test *)", - "Bash(go vet *)", - "Bash(go list *)", - "Bash(cargo check *)", - "Bash(cargo test *)", - "Bash(cargo clippy *)", - "Bash(pnpm test *)", - "Bash(pnpm run test *)", - "Bash(pnpm run lint *)", - "Bash(pnpm run typecheck *)", - "Bash(pnpm run check *)", - "Bash(pnpm run build *)", - "Bash(bun test *)", - "Bash(bun run test *)", - "Bash(bun run lint *)", - "Bash(bun run typecheck *)", - "Bash(bun run check *)", - "Bash(bun run build *)", - "Bash(make test *)", - "Bash(make check *)", - "Bash(make lint *)", - "Bash(make build *)", - "WebSearch", - "WebFetch(domain:*)", - ], + allow: ["WebSearch"], ask: [ "Bash(rm *)", - "Bash(chmod *)", - "Bash(chown *)", "Bash(kill *)", "Bash(killall *)", "Bash(pkill *)", - "Bash(env)", - "Bash(printenv *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(open *)", "Bash(gh *)", "Bash(git commit *)", "Bash(git pull *)", @@ -74,61 +16,22 @@ export const RECOMMENDED_PERMISSION_RULES = { "Bash(git rebase *)", "Bash(git push *)", "Bash(git stash *)", - "Bash(find * -exec*)", - "Bash(sed -i*)", - "Bash(go run *)", - "Bash(go get *)", - "Bash(go install *)", - "Bash(go generate *)", - "Bash(go mod tidy *)", - "Bash(cargo run *)", - "Bash(cargo install *)", - "Bash(cargo update *)", "Bash(cargo publish *)", - "Bash(rustup *)", - "Bash(npm *)", - "Bash(yarn *)", - "Bash(npx *)", - "Bash(corepack *)", - "Bash(curl *)", - "Bash(wget *)", - "Bash(pip *)", - "Bash(python -m pip *)", - "Bash(python *)", - "Bash(python3 *)", - "Bash(uv *)", - "Bash(pnpm install *)", - "Bash(pnpm add *)", - "Bash(pnpm remove *)", - "Bash(pnpm update *)", - "Bash(pnpm dlx *)", - "Bash(pnpm exec *)", - "Bash(pnpm approve-builds *)", - "Bash(bun install *)", - "Bash(bun add *)", - "Bash(bun remove *)", - "Bash(bunx *)", + "Bash(npm publish *)", + "Bash(pnpm publish *)", "Bash(brew *)", "Bash(docker *)", "Bash(docker-compose *)", - "Bash(rsync *)", "Bash(make install *)", "Bash(make deploy *)", "Bash(make release *)", - "Bash(nc *)", - "Bash(ncat *)", - "Bash(telnet *)", - "Bash(socat *)", - "Bash(nmap *)", - "Bash(ssh *)", - "Bash(scp *)", - "Bash(sftp *)", "Bash(kubectl *)", "Bash(helm *)", "Bash(aws *)", "Bash(gcloud *)", "Bash(az *)", - "Bash(osascript *)", + "Bash(ssh *)", + "Bash(scp *)", "Bash(launchctl *)", "Bash(diskutil *)", ], @@ -141,6 +44,15 @@ export const RECOMMENDED_PERMISSION_RULES = { "Bash(rm -r -f *)", "Bash(rm -f -r *)", "Bash(rm -R *)", + "Bash(rm -Rf *)", + "Bash(rm -fR *)", + "Bash(rm --recursive *)", + "Bash(rm * --recursive*)", + "Bash(/bin/rm -rf *)", + "Bash(/bin/rm -fr *)", + "Bash(/bin/rm -R*)", + "Bash(command rm -rf *)", + "Bash(command rm -fr *)", "Bash(dd *)", "Bash(mkfs *)", "Bash(shred *)", @@ -159,6 +71,7 @@ export const RECOMMENDED_PERMISSION_RULES = { "Bash(git push * -f *)", "Bash(git push --mirror *)", "Bash(git push * --mirror *)", + "Bash(git push * +*)", "Bash(git reset --hard*)", "Bash(git reset * --hard*)", "Bash(git clean -fd*)", @@ -167,6 +80,23 @@ export const RECOMMENDED_PERMISSION_RULES = { "Bash(git clean -dfx*)", "Bash(find * -delete*)", "Bash(find * -exec rm*)", + "Bash(env)", + "Bash(printenv*)", + "Bash(gh auth token*)", + "Bash(gh auth status --show-token*)", + "Bash(gh auth status -t*)", + "Bash(aws configure get *)", + "Bash(aws configure export-credentials*)", + "Bash(aws sts get-session-token*)", + "Bash(gcloud auth print-access-token*)", + "Bash(gcloud auth print-identity-token*)", + "Bash(gcloud auth application-default print-access-token*)", + "Bash(kubectl config view --raw*)", + "Bash(kubectl config view * --raw*)", + "Bash(git credential *)", + "Bash(security find-generic-password*)", + "Bash(security find-internet-password*)", + "Bash(npm token *)", "Read(**/.env)", "Read(**/.env.*)", "Read(**/.envrc)", @@ -222,6 +152,12 @@ export const RECOMMENDED_PERMISSION_RULES = { "Edit(~/.kube/**)", "Read(~/.docker/**)", "Edit(~/.docker/**)", + "Read(~/.config/gh/**)", + "Edit(~/.config/gh/**)", + "Read(~/.config/gcloud/**)", + "Edit(~/.config/gcloud/**)", + "Read(~/.git-credentials)", + "Edit(~/.git-credentials)", "Read(//etc/**)", "Edit(//etc/**)", "Read(//root/**)", diff --git a/src/components/profile-editor/sandbox-presets.ts b/src/components/profile-editor/sandbox-presets.ts index 78ce5b94..20e75fdb 100644 --- a/src/components/profile-editor/sandbox-presets.ts +++ b/src/components/profile-editor/sandbox-presets.ts @@ -3,7 +3,51 @@ import { readObject } from "./editor-utils"; export const RECOMMENDED_SANDBOX_PRESET = { enabled: true, autoAllowBashIfSandboxed: true, - excludedCommands: ["docker *", "git *"], + credentials: { + envVars: [ + { name: "AWS_ACCESS_KEY_ID", mode: "deny" }, + { name: "AWS_SECRET_ACCESS_KEY", mode: "deny" }, + { name: "AWS_SESSION_TOKEN", mode: "deny" }, + { name: "CLOUDSDK_PROXY_PASSWORD", mode: "deny" }, + { name: "GH_TOKEN", mode: "deny" }, + { name: "GITHUB_TOKEN", mode: "deny" }, + { name: "GITLAB_TOKEN", mode: "deny" }, + { name: "NPM_TOKEN", mode: "deny" }, + ], + files: [ + { path: "~/.ssh", mode: "deny" }, + { path: "~/.aws", mode: "deny" }, + { path: "~/.gnupg", mode: "deny" }, + { path: "~/.kube", mode: "deny" }, + { path: "~/.docker/config.json", mode: "deny" }, + { path: "~/.config/gh", mode: "deny" }, + { path: "~/.config/gcloud", mode: "deny" }, + { path: "~/.git-credentials", mode: "deny" }, + { path: "~/.netrc", mode: "deny" }, + { path: "~/.npmrc", mode: "deny" }, + { path: "~/.pypirc", mode: "deny" }, + ], + }, + // 仅排除需要联网、凭据或会写 .git/hooks 的命令,本地 git 操作仍留在沙箱内 + excludedCommands: [ + "git commit *", + "git push *", + "git pull *", + "git fetch *", + "git clone *", + "git ls-remote *", + "git remote *", + "git submodule *", + "git subtree *", + "docker *", + "gh *", + "aws *", + "gcloud *", + "kubectl *", + "helm *", + "ssh *", + "scp *", + ], network: { allowLocalBinding: true, allowUnixSockets: ["/var/run/docker.sock"], @@ -37,6 +81,62 @@ function appendMissingStrings(value: unknown, additions: readonly string[]) { }; } +// 按标识字段(name / path)去重追加;schema 只允许 mode: "deny",已存在的同名条目无需覆盖 +function appendMissingEntries( + value: unknown, + additions: readonly Record[], + identityKey: string, +) { + const currentItems = Array.isArray(value) ? [...value] : []; + const currentIds = new Set( + currentItems.flatMap((item) => { + const id = readObject(item)[identityKey]; + return typeof id === "string" ? [id] : []; + }), + ); + let changed = !Array.isArray(value); + + for (const addition of additions) { + const id = addition[identityKey]; + if (currentIds.has(id)) { + continue; + } + currentItems.push({ ...addition }); + currentIds.add(id); + changed = true; + } + + return { + value: currentItems, + changed, + }; +} + +function mergeRecommendedCredentials(value: unknown) { + const nextCredentials: Record = { ...readObject(value) }; + const envVars = appendMissingEntries( + nextCredentials.envVars, + RECOMMENDED_SANDBOX_PRESET.credentials.envVars, + "name", + ); + const files = appendMissingEntries( + nextCredentials.files, + RECOMMENDED_SANDBOX_PRESET.credentials.files, + "path", + ); + if (envVars.changed) { + nextCredentials.envVars = envVars.value; + } + if (files.changed) { + nextCredentials.files = files.value; + } + + return { + value: nextCredentials, + changed: envVars.changed || files.changed, + }; +} + export function mergeRecommendedSandboxPreset(value: unknown): SandboxPresetMergeResult { const sandboxObject = readObject(value); const nextValue: Record = { ...sandboxObject }; @@ -52,6 +152,12 @@ export function mergeRecommendedSandboxPreset(value: unknown): SandboxPresetMerg changed = true; } + const credentials = mergeRecommendedCredentials(nextValue.credentials); + if (credentials.changed) { + nextValue.credentials = credentials.value; + changed = true; + } + const excludedCommands = appendMissingStrings( nextValue.excludedCommands, RECOMMENDED_SANDBOX_PRESET.excludedCommands, diff --git a/src/components/profile-editor/settings-form-registry.ts b/src/components/profile-editor/settings-form-registry.ts index 05a5b9de..9bdc2d0d 100644 --- a/src/components/profile-editor/settings-form-registry.ts +++ b/src/components/profile-editor/settings-form-registry.ts @@ -450,6 +450,22 @@ export const PROFILE_SETTINGS_FORM_REGISTRY: SettingsFieldDefinition[] = [ en: "Disable auto-updates, feedback, error reporting, telemetry, and all other nonessential traffic. Use Disable auto-updates when only updates should be disabled.", }, }, + { + key: "subprocessEnvScrub", + section: "common", + kind: "checkbox", + storage: "env-only", + envKey: "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB", + enabledValue: "1", + label: { + zh: "子进程凭据清理", + en: "Scrub subprocess credentials", + }, + description: { + zh: "从 Bash、hooks 和 stdio MCP 子进程环境中清理 Anthropic、AWS、NPM、数据库等凭据;GitHub Token 和代理设置保留。依赖被清理凭据的脚本、发布操作或 MCP 服务可能认证失败。", + en: "Scrub Anthropic, AWS, NPM, database, and other credentials from Bash, hook, and stdio MCP subprocess environments. GitHub tokens and proxy settings are preserved. Scripts, publishing commands, or MCP servers that need scrubbed credentials may fail to authenticate.", + }, + }, { key: "disableAutoUpdater", section: "common", From 802c4b3b51478fe139202c83d7c0bda8c6b03d5d Mon Sep 17 00:00:00 2001 From: maguowei Date: Tue, 6 Oct 2026 16:02:55 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix(profile-editor):=20=E6=94=BE=E5=BC=80?= =?UTF-8?q?=20/etc=20=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=B9=B6=E5=B0=86=20git=20=E6=95=B4=E4=BD=93=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E5=87=BA=E6=B2=99=E7=AE=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 推荐权限预设不再整体拒绝 Read(//etc/**),仅拒绝 shadow、gshadow 与 SSH 主机私钥, 避免沙箱内 git status 无法读取 /etc/gitconfig;推荐沙箱预设改为 git * 统一在沙箱外执行。 Co-Authored-By: Claude Opus 5.5 --- src/components/__tests__/ProfileEditor.test.tsx | 11 +++++++++++ src/components/profile-editor/permission-presets.ts | 5 ++++- src/components/profile-editor/sandbox-presets.ts | 12 ++---------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/components/__tests__/ProfileEditor.test.tsx b/src/components/__tests__/ProfileEditor.test.tsx index 50e78c68..959a2f2d 100644 --- a/src/components/__tests__/ProfileEditor.test.tsx +++ b/src/components/__tests__/ProfileEditor.test.tsx @@ -2629,6 +2629,16 @@ describe("ProfileEditor", () => { expect(savedPermissions?.ask).toContain("Bash(pnpm publish *)"); expect(savedPermissions?.deny).toContain("Bash(git reset --hard*)"); expect(savedPermissions?.deny).toContain("Bash(printenv*)"); + // 系统配置需可读,避免沙箱内 git status 无法探测 /etc/gitconfig + expect(savedPermissions?.deny).not.toContain("Read(//etc/**)"); + expect(savedPermissions?.deny).toEqual( + expect.arrayContaining([ + "Edit(//etc/**)", + "Read(//etc/shadow)", + "Read(//etc/gshadow)", + "Read(//etc/ssh/ssh_host_*_key)", + ]), + ); expect(savedPermissions?.deny).not.toContain("Read(**/config.yaml)"); }, 10_000); @@ -3057,6 +3067,7 @@ describe("ProfileEditor", () => { "pnpm *", ...RECOMMENDED_SANDBOX_PRESET.excludedCommands, ]); + expect(savedSandbox?.excludedCommands).toContain("git *"); const savedCredentials = savedSandbox?.credentials as Record | undefined; const savedEnvVars = savedCredentials?.envVars as Array<{ name: string }> | undefined; // 已存在的 GH_TOKEN 不重复追加 diff --git a/src/components/profile-editor/permission-presets.ts b/src/components/profile-editor/permission-presets.ts index 23029a9f..9124349f 100644 --- a/src/components/profile-editor/permission-presets.ts +++ b/src/components/profile-editor/permission-presets.ts @@ -158,7 +158,10 @@ export const RECOMMENDED_PERMISSION_RULES = { "Edit(~/.config/gcloud/**)", "Read(~/.git-credentials)", "Edit(~/.git-credentials)", - "Read(//etc/**)", + // 系统配置需可读(如 /etc/gitconfig),只阻止读取系统密码和 SSH 主机私钥 + "Read(//etc/shadow)", + "Read(//etc/gshadow)", + "Read(//etc/ssh/ssh_host_*_key)", "Edit(//etc/**)", "Read(//root/**)", "Edit(//root/**)", diff --git a/src/components/profile-editor/sandbox-presets.ts b/src/components/profile-editor/sandbox-presets.ts index 20e75fdb..08b5a480 100644 --- a/src/components/profile-editor/sandbox-presets.ts +++ b/src/components/profile-editor/sandbox-presets.ts @@ -28,17 +28,9 @@ export const RECOMMENDED_SANDBOX_PRESET = { { path: "~/.pypirc", mode: "deny" }, ], }, - // 仅排除需要联网、凭据或会写 .git/hooks 的命令,本地 git 操作仍留在沙箱内 + // Git 统一在沙箱外执行,以兼容系统配置、凭据和 hooks excludedCommands: [ - "git commit *", - "git push *", - "git pull *", - "git fetch *", - "git clone *", - "git ls-remote *", - "git remote *", - "git submodule *", - "git subtree *", + "git *", "docker *", "gh *", "aws *", From bc4c354d8deaee115b796fce9c8c87a1d648e097 Mon Sep 17 00:00:00 2001 From: maguowei Date: Tue, 6 Oct 2026 19:59:46 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(profile-editor):=20=E6=9D=83=E9=99=90?= =?UTF-8?q?=E4=B8=8E=E6=B2=99=E7=AE=B1=E6=8E=A8=E8=8D=90=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E5=AE=8C=E5=85=A8=E9=95=9C=E5=83=8F=20dotfiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 权限预设:allow 清空为 [],ask 收敛到 11 条、deny 收敛到 34 条, 与 dotfiles 的 permissions 段逐条一致,移除项目自造的加固项 - 沙箱预设:credentials 补 ANTHROPIC_API_KEY、OPENAI_API_KEY, 移除 CLOUDSDK_PROXY_PASSWORD 与 ~/.npmrc - 沙箱预设:excludedCommands 由 git * 细化为 6 个联网类 git 子命令, 不再排除 aws/gcloud/kubectl/helm/ssh/scp - 沙箱预设:network 新增 allowedDomains,不再注入 allowUnixSockets - 同步 ProfileEditor 测试断言与 config-system 规则说明 Co-Authored-By: Claude Code --- .claude/rules/config-system.md | 1 + .../__tests__/ProfileEditor.test.tsx | 48 +++--- .../profile-editor/permission-presets.ts | 155 +++--------------- .../profile-editor/sandbox-presets.ts | 52 +++--- 4 files changed, 81 insertions(+), 175 deletions(-) diff --git a/.claude/rules/config-system.md b/.claude/rules/config-system.md index a429a3b9..14662148 100644 --- a/.claude/rules/config-system.md +++ b/.claude/rules/config-system.md @@ -75,6 +75,7 @@ paths: ## 权限与状态行 - 权限编辑器只管理 `defaultMode`、`disableBypassPermissionsMode`、`allow`、`deny`、`ask`、`additionalDirectories`;写回时保留其它顶层字段,例如 `disableAutoMode`。 +- 权限(`permission-presets.ts`)与沙箱(`sandbox-presets.ts`)的推荐预设逐条镜像维护者 dotfiles 的 `claude/settings.json` 对应段落,不额外追加项目自造的加固项;改任意一侧时同步另一侧和 `ProfileEditor.test.tsx` 断言。 - 修复权限 dirty 问题时优先做局部语义比较,不要扩大到全局 dirty 系统。 - 状态行默认脚本按平台分发:非 Windows 用 `src-tauri/resources/statusline/default.sh`(Bash,依赖 jq),Windows 用 `src-tauri/resources/statusline/default.ps1`(PowerShell,免 jq)。安装走后端 `install_status_line_preset`:Windows 写入 `~/.claude/statusline.ps1` 并把 `command` 设为绝对正斜杠**且加引号**的 `powershell -NoProfile -ExecutionPolicy Bypass -File "..."`(用户名含空格时不加引号会截断参数);两份脚本功能需保持对齐。 - 两份脚本源文件都**不带 BOM**;Windows 落盘时由 `config.rs::expected_status_line_script()` 前置 UTF-8 BOM。Windows PowerShell 5.1 读取无 BOM 的 `.ps1` 时按系统代码页(简中 CP936)解码,UTF-8 中文注释错位后残留的悬空 lead byte 会吞掉行尾换行,使下一行代码并入注释并触发 `ParserError`,状态行整行无输出。给源文件加 BOM 会变成双 BOM,Bash 脚本加 BOM 会让 shebang 失效——两者都不要做。 diff --git a/src/components/__tests__/ProfileEditor.test.tsx b/src/components/__tests__/ProfileEditor.test.tsx index 959a2f2d..6d664283 100644 --- a/src/components/__tests__/ProfileEditor.test.tsx +++ b/src/components/__tests__/ProfileEditor.test.tsx @@ -12,6 +12,7 @@ import { OFFICIAL_MARKETPLACE_ID, OFFICIAL_MARKETPLACE_REPO, } from "../profile-editor/marketplace-presets"; +import { RECOMMENDED_PERMISSION_RULES } from "../profile-editor/permission-presets"; import { RECOMMENDED_SANDBOX_PRESET } from "../profile-editor/sandbox-presets"; import { ThemeProvider } from "../theme-provider"; @@ -2602,13 +2603,17 @@ describe("ProfileEditor", () => { expect(within(permissionsSection).queryByLabelText("询问规则 1")).not.toBeInTheDocument(); expect(within(permissionsSection).queryByLabelText("拒绝规则 1")).not.toBeInTheDocument(); - fireEvent.click(within(permissionsSection).getByRole("button", { name: "展开 允许规则" })); + // 预设的 allow 为空,列表不再显示展开开关 + expect( + within(permissionsSection).queryByRole("button", { name: "展开 允许规则" }), + ).not.toBeInTheDocument(); fireEvent.click(within(permissionsSection).getByRole("button", { name: "展开 询问规则" })); fireEvent.click(within(permissionsSection).getByRole("button", { name: "展开 拒绝规则" })); - expect(within(permissionsSection).getByLabelText("允许规则 1")).toHaveValue("WebSearch"); - expect(within(permissionsSection).getByLabelText("询问规则 1")).toHaveValue("Bash(rm *)"); - expect(within(permissionsSection).getByLabelText("拒绝规则 1")).toHaveValue("Bash(sudo *)"); + expect(within(permissionsSection).getByLabelText("询问规则 1")).toHaveValue("Bash(git push *)"); + expect(within(permissionsSection).getByLabelText("拒绝规则 1")).toHaveValue( + "Bash(gh auth token*)", + ); await act(async () => { fireEvent.click(screen.getByRole("button", { name: "保存" })); @@ -2624,21 +2629,18 @@ describe("ProfileEditor", () => { disableAutoMode: "disable", additionalDirectories: ["~/projects/shared"], }); - expect(savedPermissions?.allow).toEqual(["WebSearch"]); - expect(savedPermissions?.allow).not.toContain("Bash(old-allow *)"); - expect(savedPermissions?.ask).toContain("Bash(pnpm publish *)"); - expect(savedPermissions?.deny).toContain("Bash(git reset --hard*)"); - expect(savedPermissions?.deny).toContain("Bash(printenv*)"); - // 系统配置需可读,避免沙箱内 git status 无法探测 /etc/gitconfig + // 预设与 dotfiles 镜像:allow 为空,空列表不写回 allow 键 + expect(savedPermissions?.allow).toBeUndefined(); + expect(savedPermissions?.ask).toEqual([...RECOMMENDED_PERMISSION_RULES.ask]); + expect(savedPermissions?.deny).toEqual([...RECOMMENDED_PERMISSION_RULES.deny]); + expect(savedPermissions?.ask).toContain("Bash(git push *)"); + expect(savedPermissions?.ask).not.toContain("Bash(rm *)"); + expect(savedPermissions?.deny).toContain("Bash(gh auth token*)"); + expect(savedPermissions?.deny).not.toContain("Bash(sudo *)"); + expect(savedPermissions?.deny).not.toContain("Bash(git reset --hard*)"); + // 系统配置需可读(如 /etc/gitconfig),只禁写 + expect(savedPermissions?.deny).toContain("Edit(//etc/**)"); expect(savedPermissions?.deny).not.toContain("Read(//etc/**)"); - expect(savedPermissions?.deny).toEqual( - expect.arrayContaining([ - "Edit(//etc/**)", - "Read(//etc/shadow)", - "Read(//etc/gshadow)", - "Read(//etc/ssh/ssh_host_*_key)", - ]), - ); expect(savedPermissions?.deny).not.toContain("Read(**/config.yaml)"); }, 10_000); @@ -3067,7 +3069,7 @@ describe("ProfileEditor", () => { "pnpm *", ...RECOMMENDED_SANDBOX_PRESET.excludedCommands, ]); - expect(savedSandbox?.excludedCommands).toContain("git *"); + expect(savedSandbox?.excludedCommands).toContain("git push *"); const savedCredentials = savedSandbox?.credentials as Record | undefined; const savedEnvVars = savedCredentials?.envVars as Array<{ name: string }> | undefined; // 已存在的 GH_TOKEN 不重复追加 @@ -3077,10 +3079,14 @@ describe("ProfileEditor", () => { expect(savedCredentials?.files).toEqual(RECOMMENDED_SANDBOX_PRESET.credentials.files); const savedNetwork = savedSandbox?.network as Record | undefined; expect(savedNetwork).toMatchObject({ - allowedDomains: ["example.com"], allowLocalBinding: true, }); - expect(savedNetwork?.allowUnixSockets).toEqual(["/tmp/app.sock", "/var/run/docker.sock"]); + expect(savedNetwork?.allowedDomains).toEqual([ + "example.com", + ...RECOMMENDED_SANDBOX_PRESET.network.allowedDomains, + ]); + // 预设不再接管 allowUnixSockets,用户已有配置保持不变 + expect(savedNetwork?.allowUnixSockets).toEqual(["/tmp/app.sock"]); }); it("keeps delegate visible for existing permissions default mode without exposing it as a normal option", () => { diff --git a/src/components/profile-editor/permission-presets.ts b/src/components/profile-editor/permission-presets.ts index 9124349f..d2eab8f4 100644 --- a/src/components/profile-editor/permission-presets.ts +++ b/src/components/profile-editor/permission-presets.ts @@ -1,126 +1,22 @@ +// 本预设逐条镜像维护者 dotfiles 的 claude/settings.json 的 permissions 段; +// 调整预设时需同步该文件,避免应用出来的规则与 dotfiles 漂移。 export const RECOMMENDED_PERMISSION_RULES = { - allow: ["WebSearch"], + allow: [], ask: [ - "Bash(rm *)", - "Bash(kill *)", - "Bash(killall *)", - "Bash(pkill *)", - "Bash(gh *)", - "Bash(git commit *)", - "Bash(git pull *)", - "Bash(git merge *)", - "Bash(git restore *)", - "Bash(git checkout *)", - "Bash(git clean *)", - "Bash(git reset *)", - "Bash(git rebase *)", "Bash(git push *)", - "Bash(git stash *)", - "Bash(cargo publish *)", + "Bash(gh pr merge *)", + "Bash(gh release *)", + "Bash(gh repo delete *)", + "Bash(gh secret *)", + "Bash(gh workflow run *)", "Bash(npm publish *)", - "Bash(pnpm publish *)", - "Bash(brew *)", - "Bash(docker *)", - "Bash(docker-compose *)", - "Bash(make install *)", - "Bash(make deploy *)", - "Bash(make release *)", + "Bash(cargo publish *)", "Bash(kubectl *)", - "Bash(helm *)", - "Bash(aws *)", - "Bash(gcloud *)", - "Bash(az *)", - "Bash(ssh *)", - "Bash(scp *)", - "Bash(launchctl *)", - "Bash(diskutil *)", + "Read(**/.env)", + "Read(**/.env.*)", ], deny: [ - "Bash(sudo *)", - "Bash(su *)", - "Bash(doas *)", - "Bash(rm -rf *)", - "Bash(rm -fr *)", - "Bash(rm -r -f *)", - "Bash(rm -f -r *)", - "Bash(rm -R *)", - "Bash(rm -Rf *)", - "Bash(rm -fR *)", - "Bash(rm --recursive *)", - "Bash(rm * --recursive*)", - "Bash(/bin/rm -rf *)", - "Bash(/bin/rm -fr *)", - "Bash(/bin/rm -R*)", - "Bash(command rm -rf *)", - "Bash(command rm -fr *)", - "Bash(dd *)", - "Bash(mkfs *)", - "Bash(shred *)", - "Bash(diskutil erase*)", - "Bash(diskutil partition*)", - "Bash(chmod *777*)", - "Bash(chmod -R 777*)", - "Bash(shutdown *)", - "Bash(reboot *)", - "Bash(halt *)", - "Bash(poweroff *)", - "Bash(csrutil *)", - "Bash(git push --force*)", - "Bash(git push * --force*)", - "Bash(git push -f *)", - "Bash(git push * -f *)", - "Bash(git push --mirror *)", - "Bash(git push * --mirror *)", - "Bash(git push * +*)", - "Bash(git reset --hard*)", - "Bash(git reset * --hard*)", - "Bash(git clean -fd*)", - "Bash(git clean * -fd*)", - "Bash(git clean -fdx*)", - "Bash(git clean -dfx*)", - "Bash(find * -delete*)", - "Bash(find * -exec rm*)", - "Bash(env)", - "Bash(printenv*)", "Bash(gh auth token*)", - "Bash(gh auth status --show-token*)", - "Bash(gh auth status -t*)", - "Bash(aws configure get *)", - "Bash(aws configure export-credentials*)", - "Bash(aws sts get-session-token*)", - "Bash(gcloud auth print-access-token*)", - "Bash(gcloud auth print-identity-token*)", - "Bash(gcloud auth application-default print-access-token*)", - "Bash(kubectl config view --raw*)", - "Bash(kubectl config view * --raw*)", - "Bash(git credential *)", - "Bash(security find-generic-password*)", - "Bash(security find-internet-password*)", - "Bash(npm token *)", - "Read(**/.env)", - "Read(**/.env.*)", - "Read(**/.envrc)", - "Read(**/*.env)", - "Read(**/secrets/**)", - "Edit(**/secrets/**)", - "Read(**/.secrets/**)", - "Edit(**/.secrets/**)", - "Read(**/*secret*.json)", - "Edit(**/*secret*.json)", - "Read(**/*secret*.yaml)", - "Edit(**/*secret*.yaml)", - "Read(**/*secret*.yml)", - "Edit(**/*secret*.yml)", - "Read(**/*secret*.toml)", - "Edit(**/*secret*.toml)", - "Read(**/*credentials*.json)", - "Edit(**/*credentials*.json)", - "Read(**/*credentials*.yaml)", - "Edit(**/*credentials*.yaml)", - "Read(**/*credentials*.yml)", - "Edit(**/*credentials*.yml)", - "Read(**/*credentials*.toml)", - "Edit(**/*credentials*.toml)", "Read(**/*.pem)", "Edit(**/*.pem)", "Read(**/*.key)", @@ -133,38 +29,27 @@ export const RECOMMENDED_PERMISSION_RULES = { "Edit(**/id_rsa*)", "Read(**/id_ed25519*)", "Edit(**/id_ed25519*)", - "Read(**/.netrc)", - "Edit(**/.netrc)", - "Read(**/.npmrc)", - "Edit(**/.npmrc)", - "Read(**/.pypirc)", - "Edit(**/.pypirc)", - "Read(**/.docker/config.json)", - "Edit(**/.docker/config.json)", - "Edit(**/.git/**)", "Read(~/.ssh/**)", "Edit(~/.ssh/**)", "Read(~/.aws/**)", "Edit(~/.aws/**)", "Read(~/.gnupg/**)", "Edit(~/.gnupg/**)", - "Read(~/.kube/**)", - "Edit(~/.kube/**)", - "Read(~/.docker/**)", - "Edit(~/.docker/**)", "Read(~/.config/gh/**)", "Edit(~/.config/gh/**)", "Read(~/.config/gcloud/**)", "Edit(~/.config/gcloud/**)", + "Read(~/.kube/**)", + "Edit(~/.kube/**)", + "Read(~/.docker/config.json)", + "Edit(~/.docker/config.json)", "Read(~/.git-credentials)", "Edit(~/.git-credentials)", - // 系统配置需可读(如 /etc/gitconfig),只阻止读取系统密码和 SSH 主机私钥 - "Read(//etc/shadow)", - "Read(//etc/gshadow)", - "Read(//etc/ssh/ssh_host_*_key)", + "Read(~/.netrc)", + "Edit(~/.netrc)", + "Read(~/.pypirc)", + "Edit(~/.pypirc)", + // 系统配置需可读(如 /etc/gitconfig),只阻止写入 "Edit(//etc/**)", - "Read(//root/**)", - "Edit(//root/**)", - "Edit(//var/**)", ], } as const; diff --git a/src/components/profile-editor/sandbox-presets.ts b/src/components/profile-editor/sandbox-presets.ts index 08b5a480..20df10db 100644 --- a/src/components/profile-editor/sandbox-presets.ts +++ b/src/components/profile-editor/sandbox-presets.ts @@ -1,5 +1,7 @@ import { readObject } from "./editor-utils"; +// 本预设逐条镜像维护者 dotfiles 的 claude/settings.json 的 sandbox 段; +// 调整预设时需同步该文件,避免应用出来的配置与 dotfiles 漂移。 export const RECOMMENDED_SANDBOX_PRESET = { enabled: true, autoAllowBashIfSandboxed: true, @@ -8,41 +10,53 @@ export const RECOMMENDED_SANDBOX_PRESET = { { name: "AWS_ACCESS_KEY_ID", mode: "deny" }, { name: "AWS_SECRET_ACCESS_KEY", mode: "deny" }, { name: "AWS_SESSION_TOKEN", mode: "deny" }, - { name: "CLOUDSDK_PROXY_PASSWORD", mode: "deny" }, { name: "GH_TOKEN", mode: "deny" }, { name: "GITHUB_TOKEN", mode: "deny" }, { name: "GITLAB_TOKEN", mode: "deny" }, { name: "NPM_TOKEN", mode: "deny" }, + { name: "ANTHROPIC_API_KEY", mode: "deny" }, + { name: "OPENAI_API_KEY", mode: "deny" }, ], files: [ { path: "~/.ssh", mode: "deny" }, { path: "~/.aws", mode: "deny" }, { path: "~/.gnupg", mode: "deny" }, - { path: "~/.kube", mode: "deny" }, - { path: "~/.docker/config.json", mode: "deny" }, { path: "~/.config/gh", mode: "deny" }, { path: "~/.config/gcloud", mode: "deny" }, + { path: "~/.kube", mode: "deny" }, + { path: "~/.docker/config.json", mode: "deny" }, { path: "~/.git-credentials", mode: "deny" }, { path: "~/.netrc", mode: "deny" }, - { path: "~/.npmrc", mode: "deny" }, { path: "~/.pypirc", mode: "deny" }, ], }, - // Git 统一在沙箱外执行,以兼容系统配置、凭据和 hooks + // 只有联网类 git 子命令在沙箱外执行,以兼容凭据、hooks 与远端访问; + // 其余 git 子命令仍在沙箱内运行 excludedCommands: [ - "git *", - "docker *", + "git push *", + "git pull *", + "git fetch *", + "git clone *", + "git ls-remote *", + "git submodule *", "gh *", - "aws *", - "gcloud *", - "kubectl *", - "helm *", - "ssh *", - "scp *", + "docker *", ], network: { allowLocalBinding: true, - allowUnixSockets: ["/var/run/docker.sock"], + allowedDomains: [ + "github.com", + "*.githubusercontent.com", + "ghcr.io", + "registry.npmjs.org", + "pypi.org", + "files.pythonhosted.org", + "proxy.golang.org", + "sum.golang.org", + "crates.io", + "*.crates.io", + "formulae.brew.sh", + ], }, } as const; @@ -168,12 +182,12 @@ export function mergeRecommendedSandboxPreset(value: unknown): SandboxPresetMerg networkChanged = true; } - const allowUnixSockets = appendMissingStrings( - nextNetwork.allowUnixSockets, - RECOMMENDED_SANDBOX_PRESET.network.allowUnixSockets, + const allowedDomains = appendMissingStrings( + nextNetwork.allowedDomains, + RECOMMENDED_SANDBOX_PRESET.network.allowedDomains, ); - if (allowUnixSockets.changed) { - nextNetwork.allowUnixSockets = allowUnixSockets.value; + if (allowedDomains.changed) { + nextNetwork.allowedDomains = allowedDomains.value; networkChanged = true; } From 6f66856429a1d57635769618ac536f1b1082acd9 Mon Sep 17 00:00:00 2001 From: maguowei Date: Tue, 6 Oct 2026 20:12:24 +0800 Subject: [PATCH 4/7] =?UTF-8?q?refactor(permissions):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=B7=B2=E5=A4=B1=E6=95=88=E7=9A=84=E5=AE=BD=E6=9D=BE=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 权限预设镜像 dotfiles 后 ask 只剩 11 条,与宽松模式的 43 条硬编码清单 零交集,开关触发时无可移动规则,已成空转 - 删除开关 UI、说明 Tooltip、清单常量与 helper 函数、中英 i18n 及测试用例 Co-Authored-By: Claude Code --- .../__tests__/ProfileEditor.test.tsx | 76 ---------- .../profile-editor/PermissionsEditor.tsx | 142 +----------------- src/i18n.ts | 8 - 3 files changed, 7 insertions(+), 219 deletions(-) diff --git a/src/components/__tests__/ProfileEditor.test.tsx b/src/components/__tests__/ProfileEditor.test.tsx index 6d664283..f84abb1d 100644 --- a/src/components/__tests__/ProfileEditor.test.tsx +++ b/src/components/__tests__/ProfileEditor.test.tsx @@ -2776,82 +2776,6 @@ describe("ProfileEditor", () => { expect(savedPermissions?.deny).toEqual(["Bash(git reset --hard*)"]); }); - it("toggles loose mode by moving configured ask rules into allow and back", async () => { - const onSave = vi.fn(); - renderEditor({ - onSave, - profile: { - ...PROFILE_FIXTURE, - settings: { - permissions: { - defaultMode: "dontAsk", - disableBypassPermissionsMode: "disable", - allow: ["Bash(pwd)"], - ask: ["Bash(kill *)", "Bash(env)", "Bash(custom *)"], - additionalDirectories: ["~/projects/shared"], - }, - }, - }, - }); - - const permissionsSection = getSection("权限"); - await act(async () => { - toggleAccordionSection("权限"); - await Promise.resolve(); - }); - const looseModeSwitch = within(permissionsSection).getByRole("switch", { - name: "宽松模式", - }); - expect( - within(permissionsSection).getByRole("button", { name: "宽松模式说明" }), - ).toHaveAttribute( - "data-tooltip", - "启用后会把宽松规则从询问规则移动到允许规则;关闭后会把这些规则移回询问规则。只影响当前编辑草稿,保存后生效。", - ); - expect(looseModeSwitch).toHaveAttribute("aria-checked", "false"); - - await act(async () => { - fireEvent.click(looseModeSwitch); - await Promise.resolve(); - }); - expect(looseModeSwitch).toHaveAttribute("aria-checked", "true"); - expect( - within(permissionsSection).getByRole("button", { name: "收起 允许规则" }), - ).toBeInTheDocument(); - - expect(within(permissionsSection).getByLabelText("允许规则 1")).toHaveValue("Bash(pwd)"); - expect(within(permissionsSection).getByLabelText("允许规则 2")).toHaveValue("Bash(kill *)"); - expect(within(permissionsSection).getByLabelText("允许规则 3")).toHaveValue("Bash(env)"); - expect(within(permissionsSection).getByLabelText("询问规则 1")).toHaveValue("Bash(custom *)"); - - await act(async () => { - fireEvent.click(looseModeSwitch); - await Promise.resolve(); - }); - expect(looseModeSwitch).toHaveAttribute("aria-checked", "false"); - - expect(within(permissionsSection).getByLabelText("允许规则 1")).toHaveValue("Bash(pwd)"); - expect(within(permissionsSection).getByLabelText("询问规则 1")).toHaveValue("Bash(custom *)"); - expect(within(permissionsSection).getByLabelText("询问规则 2")).toHaveValue("Bash(kill *)"); - expect(within(permissionsSection).getByLabelText("询问规则 3")).toHaveValue("Bash(env)"); - - await act(async () => { - fireEvent.click(screen.getByRole("button", { name: "保存" })); - }); - - expect(onSave).toHaveBeenCalledTimes(1); - const savedPermissions = onSave.mock.calls[0]?.[0]?.settings.permissions as - | Record - | undefined; - expect(savedPermissions).toMatchObject({ - defaultMode: "dontAsk", - disableBypassPermissionsMode: "disable", - additionalDirectories: ["~/projects/shared"], - }); - expect(savedPermissions?.allow).toEqual(["Bash(pwd)"]); - expect(savedPermissions?.ask).toEqual(["Bash(custom *)", "Bash(kill *)", "Bash(env)"]); - }); - it("selects an additional directory from the add action and preserves cancel as no-op", async () => { const onSave = vi.fn(); openDialogMock diff --git a/src/components/profile-editor/PermissionsEditor.tsx b/src/components/profile-editor/PermissionsEditor.tsx index 12627807..dc209353 100644 --- a/src/components/profile-editor/PermissionsEditor.tsx +++ b/src/components/profile-editor/PermissionsEditor.tsx @@ -1,5 +1,5 @@ import { open } from "@tauri-apps/plugin-dialog"; -import { ArrowRightLeft, FolderOpen, Info } from "lucide-react"; +import { ArrowRightLeft, FolderOpen } from "lucide-react"; import { type Dispatch, type SetStateAction, useEffect, useMemo, useRef, useState } from "react"; import { showOperationError } from "@/lib/user-facing-error"; import { useToast } from "../../hooks/useToast"; @@ -14,7 +14,6 @@ import { SelectTrigger, SelectValue, } from "../ui/select"; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; import { buildStringListError, createRowId, @@ -47,52 +46,6 @@ const MANAGED_PERMISSION_KEYS = [ "additionalDirectories", ] as const; const PERMISSION_LIST_KEYS = ["allow", "deny", "ask", "additionalDirectories"] as const; -const LOOSE_MODE_PERMISSION_RULES = [ - "Bash(kill *)", - "Bash(env)", - "Bash(printenv *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(open *)", - "Bash(git commit *)", - "Bash(git pull *)", - "Bash(git checkout *)", - "Bash(git stash *)", - "Bash(sed -i*)", - "Bash(find * -exec*)", - "Bash(go run *)", - "Bash(go get *)", - "Bash(go install *)", - "Bash(go generate *)", - "Bash(go mod tidy *)", - "Bash(cargo run *)", - "Bash(cargo install *)", - "Bash(cargo update *)", - "Bash(rustup *)", - "Bash(npm *)", - "Bash(yarn *)", - "Bash(npx *)", - "Bash(curl *)", - "Bash(wget *)", - "Bash(pip *)", - "Bash(python -m pip *)", - "Bash(python *)", - "Bash(python3 *)", - "Bash(uv *)", - "Bash(pnpm install *)", - "Bash(pnpm add *)", - "Bash(pnpm remove *)", - "Bash(pnpm update *)", - "Bash(pnpm dlx *)", - "Bash(pnpm exec *)", - "Bash(pnpm approve-builds *)", - "Bash(bun install *)", - "Bash(bun add *)", - "Bash(bun remove *)", - "Bash(bunx *)", - "Bash(make install *)", -] as const; -const LOOSE_MODE_PERMISSION_RULE_SET = new Set(LOOSE_MODE_PERMISSION_RULES); type PermissionListKey = (typeof PERMISSION_LIST_KEYS)[number]; @@ -214,32 +167,6 @@ function setPermissionListFromRows( } } -function hasLooseModeRule(rows: StringRow[]): boolean { - return rows.some((row) => LOOSE_MODE_PERMISSION_RULE_SET.has(row.value.trim())); -} - -function isLooseModeEnabled(allowRows: StringRow[], askRows: StringRow[]): boolean { - return hasLooseModeRule(allowRows) && !hasLooseModeRule(askRows); -} - -function appendMissingRows(targetRows: StringRow[], rowsToAppend: StringRow[]): StringRow[] { - const existingValues = new Set(targetRows.map((row) => row.value.trim())); - const nextRows = [...targetRows]; - - for (const row of rowsToAppend) { - const normalizedValue = row.value.trim(); - if (!existingValues.has(normalizedValue)) { - existingValues.add(normalizedValue); - nextRows.push({ - id: createRowId("permission"), - value: row.value, - }); - } - } - - return nextRows; -} - function buildPermissionsValue( permissionObject: Record, draft: PermissionsDraftValue, @@ -397,7 +324,6 @@ function PermissionsEditor({ value, onChange, onError }: PermissionsEditorProps) const [recommendedDialogOpen, setRecommendedDialogOpen] = useState(false); const [clearRulesDialog, setClearRulesDialog] = useState(null); const skipStructuredSyncRef = useRef(false); - const looseModeEnabled = isLooseModeEnabled(allowRows, askRows); useEffect(() => { skipStructuredSyncRef.current = true; @@ -607,32 +533,6 @@ function PermissionsEditor({ value, onChange, onError }: PermissionsEditorProps) }); } - function handleToggleLooseMode() { - const movingRows = looseModeEnabled - ? allowRows.filter((row) => LOOSE_MODE_PERMISSION_RULE_SET.has(row.value.trim())) - : askRows.filter((row) => LOOSE_MODE_PERMISSION_RULE_SET.has(row.value.trim())); - - if (movingRows.length === 0) { - return; - } - - setAllowExpanded(true); - setAskExpanded(true); - - if (looseModeEnabled) { - setAllowRows((current) => - current.filter((row) => !LOOSE_MODE_PERMISSION_RULE_SET.has(row.value.trim())), - ); - setAskRows((current) => appendMissingRows(current, movingRows)); - return; - } - - setAskRows((current) => - current.filter((row) => !LOOSE_MODE_PERMISSION_RULE_SET.has(row.value.trim())), - ); - setAllowRows((current) => appendMissingRows(current, movingRows)); - } - function handleConfirmClearRules() { if (clearRulesDialog === "allow") { setAllowRows([]); @@ -663,43 +563,15 @@ function PermissionsEditor({ value, onChange, onError }: PermissionsEditorProps)
-
- - {t("profileEditor.permissions.disableBypass")} - - setDisableBypass(!disableBypass)} - /> -
+ + {t("profileEditor.permissions.disableBypass")} + setDisableBypass(!disableBypass)} /> - - - - - - - {t("profileEditor.permissions.looseModeHelp")} - - -