From 76e30432bdb8c0b8ecbae6a663ef0d34a9d65275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E5=AE=9D=E5=9D=A4?= Date: Mon, 29 Jun 2026 17:32:23 +0800 Subject: [PATCH] fix(harness): auto-inject ALLOW rules for plan mode tools When HarnessAgent.Builder.enablePlanMode(true) is called, plan_enter, plan_write, and todo_write now get unconditional ALLOW rules injected into PermissionContextState so PermissionEngine does not prompt ASK under DEFAULT mode. plan_exit deliberately excluded to preserve HITL confirmation. Changes: - Add permissionContext mirror field in HarnessAgent.Builder - Modify permissionContext() setter to store locally instead of forwarding to inner immediately - In build(): merge user context + plan mode allow rules, then set inner.permissionContext() once - Add 6 tests verifying auto-injection behavior Fixes #1910 --- .../harness/agent/HarnessAgent.java | 31 ++- .../HarnessAgentPlanModePermissionTest.java | 197 ++++++++++++++++++ 2 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentPlanModePermissionTest.java diff --git a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java index 2a3b8f7b0..6a5f4824c 100644 --- a/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java +++ b/agentscope-harness/src/main/java/io/agentscope/harness/agent/HarnessAgent.java @@ -31,7 +31,9 @@ import io.agentscope.core.model.ExecutionConfig; import io.agentscope.core.model.GenerateOptions; import io.agentscope.core.model.Model; +import io.agentscope.core.permission.PermissionBehavior; import io.agentscope.core.permission.PermissionContextState; +import io.agentscope.core.permission.PermissionRule; import io.agentscope.core.shutdown.GracefulShutdownMiddleware; import io.agentscope.core.skill.repository.AgentSkillRepository; import io.agentscope.core.state.AgentState; @@ -1091,6 +1093,9 @@ public static class Builder { // JsonFileAgentStateStore rooted at ~/.agentscope/state//, outside any workspace). AgentStateStore stateStoreOverride; + // Permission context — mirrored to enable plan-mode allow-rule injection in build(). + PermissionContextState permissionContext = PermissionContextState.builder().build(); + DistributedStore distributedStore; io.agentscope.harness.agent.bus.MessageBus messageBus; @@ -1459,7 +1464,10 @@ public Builder stopOnReject(boolean stopOnReject) { } public Builder permissionContext(PermissionContextState permissionContext) { - inner.permissionContext(permissionContext); + this.permissionContext = + permissionContext != null + ? permissionContext + : PermissionContextState.builder().build(); return this; } @@ -2279,6 +2287,27 @@ public HarnessAgent build() { return t instanceof ToolBase tb && tb.isReadOnly(); }, planExtraAllowed)); + + // Auto-inject ALLOW rules for plan-control tools so PermissionEngine does not + // prompt ASK in DEFAULT mode. plan_exit deliberately excluded (preserves HITL). + PermissionContextState base = this.permissionContext; + PermissionContextState.Builder pb = + PermissionContextState.builder().mode(base.getMode()); + base.getWorkingDirectories().forEach(pb::addWorkingDirectory); + base.getAllowRules() + .forEach((t, rules) -> rules.forEach(r -> pb.addAllowRule(t, r))); + base.getDenyRules().forEach((t, rules) -> rules.forEach(r -> pb.addDenyRule(t, r))); + base.getAskRules().forEach((t, rules) -> rules.forEach(r -> pb.addAskRule(t, r))); + for (String toolName : + List.of(PlanModeTools.PLAN_ENTER, PlanModeTools.PLAN_WRITE, "todo_write")) { + pb.addAllowRule( + toolName, + new PermissionRule( + toolName, null, PermissionBehavior.ALLOW, "plan_mode")); + } + inner.permissionContext(pb.build()); + } else { + inner.permissionContext(this.permissionContext); } // ---- workspace/tools.json: MCP servers + allow/deny filter ---- diff --git a/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentPlanModePermissionTest.java b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentPlanModePermissionTest.java new file mode 100644 index 000000000..426d35039 --- /dev/null +++ b/agentscope-harness/src/test/java/io/agentscope/harness/agent/HarnessAgentPlanModePermissionTest.java @@ -0,0 +1,197 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.harness.agent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.Model; +import io.agentscope.core.permission.PermissionBehavior; +import io.agentscope.core.permission.PermissionContextState; +import io.agentscope.core.permission.PermissionEngine; +import io.agentscope.core.permission.PermissionMode; +import io.agentscope.core.permission.PermissionRule; +import io.agentscope.harness.agent.filesystem.local.LocalFilesystem; +import io.agentscope.harness.agent.tool.PlanModeTools; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import reactor.core.publisher.Flux; + +/** + * Verifies that {@link HarnessAgent.Builder#enablePlanMode(boolean)} automatically injects + * ALLOW rules for plan-control tools into the {@link PermissionEngine}, so they are not + * prompted ASK under {@link PermissionMode#DEFAULT}. + * + *

Regression test for: PlanModeMiddleware whitelist and PermissionEngine not coordinated. + */ +class HarnessAgentPlanModePermissionTest { + + @TempDir Path workspace; + + private static Model stubModel() { + Model model = mock(Model.class); + when(model.getModelName()).thenReturn("stub"); + ChatResponse chunk = + new ChatResponse( + "id", + List.of(TextBlock.builder().text("ok").build()), + null, + Map.of(), + "stop"); + when(model.stream(anyList(), any(), any())).thenReturn(Flux.just(chunk)); + return model; + } + + @Test + void enablePlanMode_planEnterGetsAllowRule() throws Exception { + java.nio.file.Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .enablePlanMode(true) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + List rules = perm.getAllowRules().get(PlanModeTools.PLAN_ENTER); + assertEquals( + 1, + rules == null ? 0 : rules.size(), + "plan_enter should have exactly one ALLOW rule"); + if (rules != null && !rules.isEmpty()) { + assertEquals(PermissionBehavior.ALLOW, rules.get(0).behavior()); + } + } + + @Test + void enablePlanMode_planWriteGetsAllowRule() throws Exception { + java.nio.file.Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .enablePlanMode(true) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + List rules = perm.getAllowRules().get(PlanModeTools.PLAN_WRITE); + assertEquals( + 1, + rules == null ? 0 : rules.size(), + "plan_write should have exactly one ALLOW rule"); + if (rules != null && !rules.isEmpty()) { + assertEquals(PermissionBehavior.ALLOW, rules.get(0).behavior()); + } + } + + @Test + void enablePlanMode_todoWriteGetsAllowRule() throws Exception { + java.nio.file.Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .enablePlanMode(true) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + List rules = perm.getAllowRules().get("todo_write"); + assertEquals( + 1, + rules == null ? 0 : rules.size(), + "todo_write should have exactly one ALLOW rule"); + if (rules != null && !rules.isEmpty()) { + assertEquals(PermissionBehavior.ALLOW, rules.get(0).behavior()); + } + } + + @Test + void enablePlanMode_planExitHasNoAllowRule() throws Exception { + java.nio.file.Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .enablePlanMode(true) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + List rules = perm.getAllowRules().get(PlanModeTools.PLAN_EXIT); + assertEquals( + 0, + rules == null ? 0 : rules.size(), + "plan_exit must not have an ALLOW rule — it relies on default ASK (HITL)"); + } + + @Test + void enablePlanMode_userAllowRulesArePreserved() throws Exception { + java.nio.file.Files.createDirectories(workspace); + PermissionRule userRule = + new PermissionRule("my_tool", null, PermissionBehavior.ALLOW, "user"); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .enablePlanMode(true) + .permissionContext( + PermissionContextState.builder() + .mode(PermissionMode.DEFAULT) + .addAllowRule("my_tool", userRule) + .build()) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + List rules = perm.getAllowRules().get("my_tool"); + assertEquals(1, rules == null ? 0 : rules.size(), "user-configured rule must be preserved"); + assertEquals(PermissionBehavior.ALLOW, rules.get(0).behavior()); + } + + @Test + void disabledPlanMode_noAutoAllowRulesInjected() throws Exception { + java.nio.file.Files.createDirectories(workspace); + HarnessAgent agent = + HarnessAgent.builder() + .name("t") + .model(stubModel()) + .workspace(workspace) + .abstractFilesystem(new LocalFilesystem(workspace)) + .build(); + + PermissionContextState perm = agent.getDelegate().getPermissionContext(); + assertEquals( + 0, + perm.getAllowRules().size(), + "no allow rules should be injected when plan mode is disabled"); + } +}