diff --git a/docs/runbooks/tool-approval-gates.md b/docs/runbooks/tool-approval-gates.md index 9199ea0ab..bf465aea5 100644 --- a/docs/runbooks/tool-approval-gates.md +++ b/docs/runbooks/tool-approval-gates.md @@ -65,8 +65,13 @@ The operator can change this in the generated config. ### Existing installations -Existing configs without `ApprovalPolicy` are unaffected — all tools remain in -`Auto` mode. Add the `ApprovalPolicy` section manually or rerun `netclaw init`. +Personal `shell_execute` calls without an exact override use the fail-closed +`Approval` mode. This rule also applies when the Personal `ApprovalPolicy` is +absent or its `DefaultMode` is `Auto`. + +Rerun `netclaw init` or add the `Approval` override to make this behavior +explicit. Set an exact `shell_execute` override to `Auto` only when shell +commands must run without approval. ### Headless mode diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/.openspec.yaml b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/.openspec.yaml new file mode 100644 index 000000000..e08b5f89a --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-03 diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/design.md b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/design.md new file mode 100644 index 000000000..a1e35a1ef --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/design.md @@ -0,0 +1,59 @@ +## Context + +The Personal init path writes `ShellMode = HostAllowed` and an explicit `shell_execute = Approval` override. `ToolAccessPolicy` also selects `Approval` when a Personal shell call has no exact override. + +`ToolAudienceProfilesDoctorCheck` instead treats a missing policy as `Auto`. The main specification also says that runtime defaults do not place shell in `Approval` mode. + +The daemon actor boundary and approval persistence do not change. The change only aligns diagnostics and the contract with the existing authorization decision. + +## Goals / Non-Goals + +**Goals:** + +- Make the doctor report the effective Personal shell mode. +- State the clean-install and fail-closed fallback rules. +- Preserve explicit `Auto`, `Approval`, and `Deny` shell overrides. +- Add focused regression proof for doctor output. + +**Non-Goals:** + +- Change shell authorization or approval persistence. +- Add the approval disposition matrix. +- Change shell parsing or PR #1733. +- Change Team or Public shell access. + +## Decisions + +### Use the exact shell override as the doctor signal + +The doctor will warn only when the Personal profile explicitly sets `shell_execute` to `Auto`. A missing policy or missing exact override resolves to `Approval` in the runtime. + +This approach uses `ToolApprovalConfig.TryGetExplicitMode`. It does not add a second general approval resolver. + +Alternative: move the runtime resolver into a new shared service. This change rejects that option because the contract correction needs no new runtime abstraction. + +### Keep the clean install explicit + +The init wizard will continue to write `shell_execute = Approval`. Operators can inspect the normal security posture without knowledge of the runtime backstop. + +The fallback remains necessary for old or partial configuration. It prevents a missing field from enabling host shell without approval. + +### Keep actor and persistence behavior unchanged + +The session actor, approval actor, and `tool-approvals.json` format do not change. Existing approvals remain valid. + +## Risks / Trade-offs + +- **Risk:** A future runtime change could diverge from the doctor again. **Mitigation:** Tests cover missing policy, missing override, and explicit override cases. +- **Risk:** Operators can misread a missing warning as an explicit configuration endorsement. **Mitigation:** The specification distinguishes the generated override from the runtime backstop. +- **Risk:** A doctor message change can affect documentation or scripts. **Mitigation:** The new message remains a warning and names the explicit `Auto` override. + +## Migration Plan + +No configuration migration is required. Existing Personal configurations retain their current runtime behavior. + +Rollback restores the old diagnostic and specification text. It does not change stored approvals or daemon state. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/proposal.md b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/proposal.md new file mode 100644 index 000000000..5402bd67d --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/proposal.md @@ -0,0 +1,35 @@ +## Why + +A clean Personal install writes an explicit `shell_execute = Approval` override. The runtime also applies this fail-closed result when the override is absent, but the specification and doctor describe `Auto` instead. + +This conflict makes diagnostics incorrect and obscures the deployed security contract before PR #1733 changes shell parsing. This change aligns the contract before the approval matrix work begins. + +## What Changes + +- State that Personal shell execution requires approval unless an explicit shell override selects `Auto` or `Deny`. +- Keep the clean Personal install as the normal configuration source. +- Keep the runtime fallback as a safety backstop for legacy or partial configuration. +- Fix the doctor check so it reports the effective runtime mode. +- Add focused doctor regression tests. +- Keep the approval matrix and PR #1733 changes out of scope. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `tool-approval-gates`: Align the Personal shell fallback requirement with the shipped runtime and generated configuration. +- `netclaw-cli`: Make the doctor warn for an explicit Personal shell `Auto` override, not for the fail-closed fallback. + +## Impact + +- **PRDs:** PRD-002 gateway security and PRD-004 CLI onboarding and configuration. +- **Code:** `ToolAudienceProfilesDoctorCheck` effective-mode diagnostics. +- **Tests:** Focused doctor tests for missing and explicit Personal shell modes. +- **Documentation:** Correct the approval runbook for existing configurations. +- **Security:** The change preserves fail-closed shell behavior. It adds no new grant or bypass. +- **Operations:** Operators stop receiving a false missing-gate warning for a configuration that the runtime gates. +- **MVP scope:** The change corrects the existing contract. It adds no new capability. diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/netclaw-cli/spec.md b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/netclaw-cli/spec.md new file mode 100644 index 000000000..829552728 --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/netclaw-cli/spec.md @@ -0,0 +1,41 @@ +## MODIFIED Requirements + +### Requirement: Doctor checks for approval configuration + +`netclaw doctor` SHALL validate approval configuration consistency. It SHALL +warn when the Personal audience enables host shell access with an exact +`shell_execute = Auto` override. It SHALL NOT warn when the Personal approval +policy is absent. It SHALL NOT warn when the exact shell override is absent. +The runtime resolves both states to `Approval`. The doctor SHALL warn when +`tool-approvals.json` contains patterns for audiences or tools that are no +longer configured. + +#### Scenario: Doctor accepts the missing-policy fail-closed fallback + +- **GIVEN** the Personal audience has host shell access enabled +- **AND** the Personal profile has no `ApprovalPolicy` +- **WHEN** `netclaw doctor` runs +- **THEN** it emits no warning that shell lacks an approval gate + +#### Scenario: Doctor accepts a policy without an exact shell override + +- **GIVEN** the Personal audience has host shell access enabled +- **AND** `ApprovalPolicy.ToolOverrides` does not contain `shell_execute` +- **WHEN** `netclaw doctor` runs +- **THEN** it emits no warning that shell lacks an approval gate + +#### Scenario: Doctor warns about an explicit Personal shell Auto override + +- **GIVEN** the Personal audience has host shell access enabled +- **AND** `ApprovalPolicy.ToolOverrides.shell_execute` is `Auto` +- **WHEN** `netclaw doctor` runs +- **THEN** it emits a warning that Personal host shell explicitly runs without approval +- **AND** the warning recommends changing the exact override to `Approval` + +#### Scenario: Doctor warns about stale approval patterns + +- **GIVEN** `tool-approvals.json` has patterns for `team.shell_execute` +- **AND** the Team audience has shell mode Off +- **WHEN** `netclaw doctor` runs +- **THEN** it emits an info advisory: "Persistent approvals exist for + team.shell_execute but shell is disabled for Team audience." diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/tool-approval-gates/spec.md b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/tool-approval-gates/spec.md new file mode 100644 index 000000000..430183658 --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/specs/tool-approval-gates/spec.md @@ -0,0 +1,70 @@ +## MODIFIED Requirements + +### Requirement: Tool approval configuration per audience + +The system SHALL support per-audience tool approval configuration via +`ToolApprovalConfig` on `ToolAudienceProfile`. Each audience profile SHALL +independently specify a `DefaultMode` (Auto, Approval, Deny) and per-tool +overrides in `ToolOverrides`. The default `DefaultMode` SHALL be `Auto` for +tools without a stricter invocation-specific rule. + +The init-generated Personal config SHALL explicitly write +`ApprovalPolicy.ToolOverrides.shell_execute = Approval` as the normal +shell-safe configuration. For a Personal shell invocation, an exact +`shell_execute` override SHALL select `Auto`, `Approval`, or `Deny`. The runtime +SHALL select `Approval` when that exact override is absent. This rule SHALL +apply when `ApprovalPolicy` is absent. It SHALL also apply when `DefaultMode` +is `Auto`. This fallback SHALL prevent a missing field from enabling host shell +without approval. + +#### Scenario: Shell requires approval in init-generated Personal config + +- **GIVEN** a Personal audience session whose generated config explicitly sets + `ApprovalPolicy.ToolOverrides.shell_execute` to `Approval` +- **WHEN** the agent invokes `shell_execute` +- **THEN** `ToolAccessPolicy` marks the call as approval-gated +- **AND** `DispatchingToolExecutor` consults `IToolApprovalService` before execution +- **AND** if the command pattern is not approved, an approval prompt is emitted + +#### Scenario: Missing Personal approval policy fails closed for shell + +- **GIVEN** a Personal audience session with `ShellMode` set to `HostAllowed` +- **AND** the Personal profile has no `ApprovalPolicy` +- **WHEN** the agent invokes `shell_execute` +- **THEN** the runtime resolves the invocation to `Approval` +- **AND** the missing policy does not enable automatic shell execution + +#### Scenario: Personal policy without an exact shell override fails closed + +- **GIVEN** a Personal approval policy whose `DefaultMode` is `Auto` +- **AND** `ToolOverrides` has no exact `shell_execute` entry +- **WHEN** the agent invokes `shell_execute` +- **THEN** the runtime resolves the invocation to `Approval` + +#### Scenario: Explicit Personal shell Auto override executes without approval + +- **GIVEN** a Personal approval policy with an exact `shell_execute = Auto` override +- **WHEN** the agent invokes a command that passes earlier security gates +- **THEN** the tool executes without an approval prompt + +#### Scenario: Tool in Auto mode executes without approval + +- **GIVEN** a tool whose effective approval mode is `Auto` for the session's audience +- **WHEN** the agent invokes the tool +- **THEN** the tool executes immediately without an approval prompt + +#### Scenario: Tool in Deny mode is always blocked + +- **GIVEN** a tool whose effective approval mode is `Deny` for the session's audience +- **WHEN** the agent invokes the tool +- **THEN** the tool is denied with reason `tool_denied_by_approval_policy` +- **AND** no approval prompt is offered + +#### Scenario: Per-audience independence + +- **GIVEN** Personal sets `shell_execute` to `Approval` and Team sets it to `Deny` +- **WHEN** a Personal session invokes `shell_execute` +- **THEN** `ToolAccessPolicy` marks the call as approval-gated +- **AND** `DispatchingToolExecutor` may prompt if `IToolApprovalService` reports unapproved patterns +- **AND** when a Team session invokes `shell_execute` +- **THEN** the system denies immediately without prompting diff --git a/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/tasks.md b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/tasks.md new file mode 100644 index 000000000..528e85ae1 --- /dev/null +++ b/openspec/changes/archive/2026-08-03-align-personal-shell-approval-fallback/tasks.md @@ -0,0 +1,15 @@ +## 1. Doctor Contract + +- [x] 1.1 Make the Personal shell doctor warning follow the runtime fallback and exact override precedence. +- [x] 1.2 Add focused doctor tests for missing policy, missing override, explicit `Approval`, and explicit `Auto`. + +## 2. Documentation and Specification + +- [x] 2.1 Correct the approval runbook for clean and existing Personal configurations. +- [x] 2.2 Sync the `tool-approval-gates` and `netclaw-cli` delta specifications. + +## 3. Verification + +- [x] 3.1 Run the focused doctor test class. +- [x] 3.2 Run Slopwatch, the file-header check, and `git diff --check`. +- [x] 3.3 Verify the implementation against the OpenSpec change and archive it. diff --git a/openspec/specs/netclaw-cli/spec.md b/openspec/specs/netclaw-cli/spec.md index 802ea9659..1320351a0 100644 --- a/openspec/specs/netclaw-cli/spec.md +++ b/openspec/specs/netclaw-cli/spec.md @@ -52,20 +52,34 @@ rather than relying on runtime-only implicit defaults. ### Requirement: Doctor checks for approval configuration `netclaw doctor` SHALL validate approval configuration consistency. It SHALL -warn when the Personal audience enables host shell access without an explicit -`shell_execute` approval gate in `ApprovalPolicy.ToolOverrides`. It SHALL warn -when `tool-approvals.json` contains patterns for audiences or tools that are no +warn when the Personal audience enables host shell access with an exact +`shell_execute = Auto` override. It SHALL NOT warn when the Personal approval +policy is absent. It SHALL NOT warn when the exact shell override is absent. +The runtime resolves both states to `Approval`. The doctor SHALL warn when +`tool-approvals.json` contains patterns for audiences or tools that are no longer configured. -#### Scenario: Doctor warns about Personal host shell without explicit approval gate +#### Scenario: Doctor accepts the missing-policy fail-closed fallback + +- **GIVEN** the Personal audience has host shell access enabled +- **AND** the Personal profile has no `ApprovalPolicy` +- **WHEN** `netclaw doctor` runs +- **THEN** it emits no warning that shell lacks an approval gate + +#### Scenario: Doctor accepts a policy without an exact shell override - **GIVEN** the Personal audience has host shell access enabled - **AND** `ApprovalPolicy.ToolOverrides` does not contain `shell_execute` - **WHEN** `netclaw doctor` runs -- **THEN** it emits a warning that Personal host shell is enabled without an - explicit `shell_execute` approval gate -- **AND** the warning recommends running `netclaw init` again or setting - `Tools.AudienceProfiles.Personal.ApprovalPolicy.ToolOverrides.shell_execute = "approval"` +- **THEN** it emits no warning that shell lacks an approval gate + +#### Scenario: Doctor warns about an explicit Personal shell Auto override + +- **GIVEN** the Personal audience has host shell access enabled +- **AND** `ApprovalPolicy.ToolOverrides.shell_execute` is `Auto` +- **WHEN** `netclaw doctor` runs +- **THEN** it emits a warning that Personal host shell explicitly runs without approval +- **AND** the warning recommends changing the exact override to `Approval` #### Scenario: Doctor warns about stale approval patterns @@ -291,6 +305,7 @@ SHALL remain a superset of the previous shape: existing `verb` and - **WHEN** the approvals list page renders - **THEN** each row shows the grant's relative creation time alongside its scope label + ### Requirement: CLI derives local control-plane endpoint from daemon bind config When no explicit daemon endpoint override exists, the CLI SHALL derive a usable local control-plane endpoint from `Daemon.Host` and `Daemon.Port` in daemon configuration instead of always falling back to `http://127.0.0.1:5199`. diff --git a/openspec/specs/tool-approval-gates/spec.md b/openspec/specs/tool-approval-gates/spec.md index 5e30628a9..53bff78b9 100644 --- a/openspec/specs/tool-approval-gates/spec.md +++ b/openspec/specs/tool-approval-gates/spec.md @@ -14,12 +14,17 @@ directory grants never widen access to protected paths. The system SHALL support per-audience tool approval configuration via `ToolApprovalConfig` on `ToolAudienceProfile`. Each audience profile SHALL independently specify a `DefaultMode` (Auto, Approval, Deny) and per-tool -overrides in `ToolOverrides`. The default `DefaultMode` SHALL be `Auto` (no -approval required). Runtime audience defaults SHALL NOT implicitly place -`shell_execute` in `Approval` mode. Instead, the init-generated Personal config -SHALL explicitly write -`ApprovalPolicy.ToolOverrides.shell_execute = Approval` as the recommended -shell-safe default. +overrides in `ToolOverrides`. The default `DefaultMode` SHALL be `Auto` for +tools without a stricter invocation-specific rule. + +The init-generated Personal config SHALL explicitly write +`ApprovalPolicy.ToolOverrides.shell_execute = Approval` as the normal +shell-safe configuration. For a Personal shell invocation, an exact +`shell_execute` override SHALL select `Auto`, `Approval`, or `Deny`. The runtime +SHALL select `Approval` when that exact override is absent. This rule SHALL +apply when `ApprovalPolicy` is absent. It SHALL also apply when `DefaultMode` +is `Auto`. This fallback SHALL prevent a missing field from enabling host shell +without approval. #### Scenario: Shell requires approval in init-generated Personal config @@ -30,15 +35,36 @@ shell-safe default. - **AND** `DispatchingToolExecutor` consults `IToolApprovalService` before execution - **AND** if the command pattern is not approved, an approval prompt is emitted +#### Scenario: Missing Personal approval policy fails closed for shell + +- **GIVEN** a Personal audience session with `ShellMode` set to `HostAllowed` +- **AND** the Personal profile has no `ApprovalPolicy` +- **WHEN** the agent invokes `shell_execute` +- **THEN** the runtime resolves the invocation to `Approval` +- **AND** the missing policy does not enable automatic shell execution + +#### Scenario: Personal policy without an exact shell override fails closed + +- **GIVEN** a Personal approval policy whose `DefaultMode` is `Auto` +- **AND** `ToolOverrides` has no exact `shell_execute` entry +- **WHEN** the agent invokes `shell_execute` +- **THEN** the runtime resolves the invocation to `Approval` + +#### Scenario: Explicit Personal shell Auto override executes without approval + +- **GIVEN** a Personal approval policy with an exact `shell_execute = Auto` override +- **WHEN** the agent invokes a command that passes earlier security gates +- **THEN** the tool executes without an approval prompt + #### Scenario: Tool in Auto mode executes without approval -- **GIVEN** a tool whose approval mode is `Auto` for the session's audience +- **GIVEN** a tool whose effective approval mode is `Auto` for the session's audience - **WHEN** the agent invokes the tool - **THEN** the tool executes immediately without an approval prompt #### Scenario: Tool in Deny mode is always blocked -- **GIVEN** a tool whose approval mode is `Deny` for the session's audience +- **GIVEN** a tool whose effective approval mode is `Deny` for the session's audience - **WHEN** the agent invokes the tool - **THEN** the tool is denied with reason `tool_denied_by_approval_policy` - **AND** no approval prompt is offered diff --git a/src/Netclaw.Cli.Tests/Doctor/ToolAudienceProfilesDoctorCheckTests.cs b/src/Netclaw.Cli.Tests/Doctor/ToolAudienceProfilesDoctorCheckTests.cs index 125db8009..2cb74a442 100644 --- a/src/Netclaw.Cli.Tests/Doctor/ToolAudienceProfilesDoctorCheckTests.cs +++ b/src/Netclaw.Cli.Tests/Doctor/ToolAudienceProfilesDoctorCheckTests.cs @@ -89,9 +89,6 @@ public async Task TeamFilesystemAll_IsError() [Fact] public async Task UnrestrictedPersonalProfile_Explicit_NoUnrestrictedWarning() { - // When Personal profile is explicitly written, unrestricted access is intentional. - // Doctor should not warn about the unrestricted profile itself, but may still - // warn about missing shell_execute approval gate. WriteConfig( """ { @@ -114,10 +111,8 @@ public async Task UnrestrictedPersonalProfile_Explicit_NoUnrestrictedWarning() var check = new ToolAudienceProfilesDoctorCheck(_paths); var result = await check.RunAsync(TestContext.Current.CancellationToken); - // Should not warn about unrestricted profile when it's explicit Assert.DoesNotContain("Personal profile allows all tools", result.Message); - // May still warn about missing shell approval gate (different message) - Assert.Contains("without an explicit shell_execute approval gate", result.Message); + Assert.DoesNotContain("explicitly sets shell_execute to Auto", result.Message); } [Fact] @@ -223,11 +218,8 @@ public async Task McpServerWithToolGrants_NoSupplyChainWarning() } [Fact] - public async Task RecommendedProfiles_WarnsAboutShellGateOnly() + public async Task RecommendedProfiles_UseFailClosedShellFallback() { - // CreateProfiles() writes all three profiles explicitly, so Personal is - // explicit. The unrestricted warning is suppressed, but the shell - // approval gate warning still fires (no ApprovalPolicy on Personal). var toolConfig = new ToolConfig { ShellMode = ShellExecutionMode.HostAllowed, @@ -243,15 +235,12 @@ public async Task RecommendedProfiles_WarnsAboutShellGateOnly() var check = new ToolAudienceProfilesDoctorCheck(_paths); var result = await check.RunAsync(TestContext.Current.CancellationToken); - Assert.Equal(DoctorSeverity.Warning, result.Severity); - // Unrestricted warning suppressed for explicit Personal Assert.DoesNotContain("Personal profile allows all tools", result.Message); - // Shell approval gate warning still fires - Assert.Contains("without an explicit shell_execute approval gate", result.Message); + Assert.DoesNotContain("explicitly sets shell_execute to Auto", result.Message); } [Fact] - public async Task PersonalShellWithoutExplicitApprovalPolicy_Warns() + public async Task PersonalShellWithoutApprovalPolicy_DoesNotWarnAboutAutoMode() { WriteConfig( """ @@ -275,12 +264,11 @@ public async Task PersonalShellWithoutExplicitApprovalPolicy_Warns() var check = new ToolAudienceProfilesDoctorCheck(_paths); var result = await check.RunAsync(TestContext.Current.CancellationToken); - Assert.Equal(DoctorSeverity.Warning, result.Severity); - Assert.Contains("without an explicit shell_execute approval gate", result.Message); + Assert.DoesNotContain("explicitly sets shell_execute to Auto", result.Message); } [Fact] - public async Task PersonalShellWithExplicitApprovalPolicy_DoesNotWarnAboutMissingGate() + public async Task PersonalShellWithExplicitApproval_DoesNotWarnAboutAutoMode() { WriteConfig( """ @@ -309,7 +297,72 @@ public async Task PersonalShellWithExplicitApprovalPolicy_DoesNotWarnAboutMissin var check = new ToolAudienceProfilesDoctorCheck(_paths); var result = await check.RunAsync(TestContext.Current.CancellationToken); - Assert.DoesNotContain("without an explicit shell_execute approval gate", result.Message); + Assert.DoesNotContain("explicitly sets shell_execute to Auto", result.Message); + } + + [Fact] + public async Task PersonalShellWithoutExplicitOverride_DoesNotWarnAboutAutoMode() + { + WriteConfig( + """ + { + "configVersion": 1, + "Tools": { + "ShellMode": "HostAllowed", + "AudienceProfiles": { + "Personal": { + "ToolsMode": "All", + "McpServersMode": "All", + "ApprovalPolicy": { + "DefaultMode": "Auto" + }, + "ReadFiles": { "Mode": "All" }, + "WriteFiles": { "Mode": "All" }, + "AttachFiles": { "Mode": "All" } + } + } + } + } + """); + + var check = new ToolAudienceProfilesDoctorCheck(_paths); + var result = await check.RunAsync(TestContext.Current.CancellationToken); + + Assert.DoesNotContain("explicitly sets shell_execute to Auto", result.Message); + } + + [Fact] + public async Task PersonalShellWithExplicitAuto_Warns() + { + WriteConfig( + """ + { + "configVersion": 1, + "Tools": { + "ShellMode": "HostAllowed", + "AudienceProfiles": { + "Personal": { + "ToolsMode": "All", + "McpServersMode": "All", + "ApprovalPolicy": { + "ToolOverrides": { + "shell_execute": "Auto" + } + }, + "ReadFiles": { "Mode": "All" }, + "WriteFiles": { "Mode": "All" }, + "AttachFiles": { "Mode": "All" } + } + } + } + } + """); + + var check = new ToolAudienceProfilesDoctorCheck(_paths); + var result = await check.RunAsync(TestContext.Current.CancellationToken); + + Assert.Equal(DoctorSeverity.Warning, result.Severity); + Assert.Contains("explicitly sets shell_execute to Auto", result.Message); } // ── MCP server missing Personal approval-default warning ── diff --git a/src/Netclaw.Cli/Doctor/ToolAudienceProfilesDoctorCheck.cs b/src/Netclaw.Cli/Doctor/ToolAudienceProfilesDoctorCheck.cs index 66bc02ba8..113127723 100644 --- a/src/Netclaw.Cli/Doctor/ToolAudienceProfilesDoctorCheck.cs +++ b/src/Netclaw.Cli/Doctor/ToolAudienceProfilesDoctorCheck.cs @@ -102,7 +102,7 @@ public Task RunAsync(CancellationToken cancellationToken = de warnings.Add("Personal profile also enables host shell, which has a high blast radius."); } - CheckMissingPersonalShellApproval(toolConfig, warnings); + CheckExplicitPersonalShellAuto(toolConfig, warnings); // Advisory: approval mode configured but shell is off CheckApprovalMismatch(toolConfig, warnings); @@ -283,7 +283,7 @@ private static void CheckApprovalMismatch(ToolConfig toolConfig, List wa } } - private static void CheckMissingPersonalShellApproval(ToolConfig toolConfig, List warnings) + private static void CheckExplicitPersonalShellAuto(ToolConfig toolConfig, List warnings) { if (toolConfig.ShellMode != ShellExecutionMode.HostAllowed) return; @@ -292,13 +292,15 @@ private static void CheckMissingPersonalShellApproval(ToolConfig toolConfig, Lis if (!PersonalProfileAllowsShell(personal)) return; - var approvalMode = personal.ApprovalPolicy?.GetEffectiveMode(ShellTool.ToolName) ?? ToolApprovalMode.Auto; - if (approvalMode is ToolApprovalMode.Approval or ToolApprovalMode.Deny) + if (personal.ApprovalPolicy is null + || !personal.ApprovalPolicy.TryGetExplicitMode(ShellTool.ToolName, out var approvalMode) + || approvalMode != ToolApprovalMode.Auto) return; warnings.Add( - "Personal profile enables host shell without an explicit shell_execute approval gate. " + - "Run `netclaw init` again or set Tools.AudienceProfiles.Personal.ApprovalPolicy.ToolOverrides.shell_execute to Approval."); + "Personal profile explicitly sets shell_execute to Auto while host shell is enabled. " + + "Commands that pass earlier security gates run without approval. " + + "Set Tools.AudienceProfiles.Personal.ApprovalPolicy.ToolOverrides.shell_execute to Approval."); } private static bool PersonalProfileAllowsShell(ToolAudienceProfile profile)