diff --git a/mateclaw-server/src/main/java/vip/mate/approval/grant/controller/ApprovalGrantController.java b/mateclaw-server/src/main/java/vip/mate/approval/grant/controller/ApprovalGrantController.java
index 6074c80b..42ded287 100644
--- a/mateclaw-server/src/main/java/vip/mate/approval/grant/controller/ApprovalGrantController.java
+++ b/mateclaw-server/src/main/java/vip/mate/approval/grant/controller/ApprovalGrantController.java
@@ -11,6 +11,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import vip.mate.approval.grant.entity.ApprovalGrant;
import vip.mate.approval.grant.entity.ApprovalResolutionLog;
+import vip.mate.agent.repository.AgentMapper;
import vip.mate.approval.grant.repository.ApprovalGrantMapper;
import vip.mate.approval.grant.repository.ApprovalResolutionLogMapper;
import vip.mate.approval.grant.service.ApprovalGrantService;
@@ -55,6 +56,7 @@ public class ApprovalGrantController {
private static final long DEFAULT_WORKSPACE_ID = 1L;
private final ApprovalGrantService grantService;
+ private final AgentMapper agentMapper;
private final ApprovalGrantMapper grantMapper;
private final ApprovalResolutionLogMapper resolutionMapper;
private final AuthService authService;
@@ -295,6 +297,7 @@ public class ApprovalGrantController {
}
}
case ApprovalGrant.ScopeType.AGENT -> {
+ requireExistingAgent(body.scopeId);
if (toolNull) {
requireAdminPlusPassword(isAdmin, body.password, actorId);
} else if (!isAdmin) {
@@ -304,6 +307,15 @@ public class ApprovalGrantController {
}
}
case ApprovalGrant.ScopeType.WORKSPACE -> {
+ // WORKSPACE-scope matching requires scope_id == the invocation's
+ // workspaceId AND the grant row's workspace_id (tenant column) to
+ // equal that same workspace — a scopeId pointing anywhere else can
+ // never fire. Reject the dead configuration outright.
+ if (!String.valueOf(workspaceId).equals(body.scopeId)) {
+ throw new MateClawException("err.approval.workspace_scope_mismatch", 400,
+ "WORKSPACE-scope scopeId must equal the current workspace id ("
+ + workspaceId + "); a cross-workspace grant can never match");
+ }
workspaceService.requirePermission(workspaceId, actorId, "admin");
if (toolNull) {
requireAdminPlusPassword(true, body.password, actorId);
@@ -314,6 +326,24 @@ public class ApprovalGrantController {
}
}
+ /**
+ * AGENT-scope scopeId must reference an existing agent — a workspace or
+ * conversation id pasted here compiles into a grant that never matches.
+ */
+ private void requireExistingAgent(String scopeId) {
+ Long agentId;
+ try {
+ agentId = Long.parseLong(scopeId);
+ } catch (NumberFormatException e) {
+ throw new MateClawException("err.approval.agent_not_found", 400,
+ "AGENT-scope scopeId must be a numeric agent id: " + scopeId);
+ }
+ if (agentMapper.selectById(agentId) == null) {
+ throw new MateClawException("err.approval.agent_not_found", 400,
+ "AGENT-scope scopeId does not reference an existing agent: " + scopeId);
+ }
+ }
+
private void requireAdminPlusPassword(boolean isAdmin, String rawPassword, Long actorId) {
if (!isAdmin) {
throw new MateClawException("err.approval.admin_required", 403, "admin role required");
diff --git a/mateclaw-server/src/test/java/vip/mate/approval/grant/controller/ApprovalGrantControllerTest.java b/mateclaw-server/src/test/java/vip/mate/approval/grant/controller/ApprovalGrantControllerTest.java
index 1085efab..fcf3f03b 100644
--- a/mateclaw-server/src/test/java/vip/mate/approval/grant/controller/ApprovalGrantControllerTest.java
+++ b/mateclaw-server/src/test/java/vip/mate/approval/grant/controller/ApprovalGrantControllerTest.java
@@ -10,6 +10,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
+import vip.mate.agent.model.AgentEntity;
+import vip.mate.agent.repository.AgentMapper;
import vip.mate.approval.grant.entity.ApprovalGrant;
import vip.mate.approval.grant.repository.ApprovalGrantMapper;
import vip.mate.approval.grant.repository.ApprovalResolutionLogMapper;
@@ -51,8 +53,10 @@ class ApprovalGrantControllerTest {
private static final long WORKSPACE_ID = 100L;
private static final long MEMBER_ID = 1001L;
private static final long ADMIN_ID = 2002L;
+ private static final long AGENT_ID = 3003L;
@Mock ApprovalGrantService grantService;
+ @Mock AgentMapper agentMapper;
@Mock ApprovalGrantMapper grantMapper;
@Mock ApprovalResolutionLogMapper resolutionMapper;
@Mock AuthService authService;
@@ -79,6 +83,12 @@ class ApprovalGrantControllerTest {
// flag the unused one. Using lenient here keeps setUp shared.
lenient().when(authService.findByUsername("member-user")).thenReturn(member);
lenient().when(authService.findByUsername("admin-user")).thenReturn(admin);
+
+ // AGENT-scope creation now verifies the target agent exists; most tests
+ // use AGENT_ID as a valid reference. Lenient: non-AGENT tests never hit it.
+ AgentEntity agent = new AgentEntity();
+ agent.setId(AGENT_ID);
+ lenient().when(agentMapper.selectById(AGENT_ID)).thenReturn(agent);
}
@Nested
@@ -118,7 +128,7 @@ class ApprovalGrantControllerTest {
@Test
void agent_scope_explicit_tool_requires_admin() {
- ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "agent-1", "read_file");
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", String.valueOf(AGENT_ID), "read_file");
doThrow(new MateClawException("err.workspace.insufficient_permission", 403, "admin required"))
.when(workspaceService).requirePermission(WORKSPACE_ID, MEMBER_ID, "admin");
@@ -128,7 +138,7 @@ class ApprovalGrantControllerTest {
@Test
void agent_scope_null_tool_requires_admin_plus_password() {
- ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "agent-1", null);
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", String.valueOf(AGENT_ID), null);
// admin true; missing password → 403
when(workspaceService.hasPermission(WORKSPACE_ID, ADMIN_ID, "admin")).thenReturn(true);
body.password = null;
@@ -140,7 +150,7 @@ class ApprovalGrantControllerTest {
@Test
void agent_scope_null_tool_admin_with_password_passes() {
- ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "agent-1", null);
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", String.valueOf(AGENT_ID), null);
body.password = "correct-password";
when(workspaceService.hasPermission(WORKSPACE_ID, ADMIN_ID, "admin")).thenReturn(true);
// verifyCurrentUserPassword passes silently when correct.
@@ -186,9 +196,45 @@ class ApprovalGrantControllerTest {
.hasMessageContaining("CRITICAL is not auto-approvable");
}
+ @Test
+ void workspace_scope_id_must_match_current_workspace() {
+ // A WORKSPACE-scope grant whose scopeId is a different workspace can
+ // never match at runtime (tenant column + scope match both fail) —
+ // rejected at creation as a dead configuration.
+ ApprovalGrantController.CreateGrantRequest body = baseBody("WORKSPACE",
+ "2079860736482390017", "read_file");
+
+ assertThatThrownBy(() -> controller.create(body, WORKSPACE_ID, adminAuth))
+ .isInstanceOf(MateClawException.class)
+ .hasMessageContaining("must equal the current workspace id");
+ verify(grantMapper, never()).insert(any(ApprovalGrant.class));
+ }
+
+ @Test
+ void agent_scope_nonexistent_agent_is_rejected() {
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "424242", "read_file");
+ when(agentMapper.selectById(424242L)).thenReturn(null);
+
+ assertThatThrownBy(() -> controller.create(body, WORKSPACE_ID, adminAuth))
+ .isInstanceOf(MateClawException.class)
+ .hasMessageContaining("does not reference an existing agent");
+ verify(grantMapper, never()).insert(any(ApprovalGrant.class));
+ }
+
+ @Test
+ void agent_scope_non_numeric_id_is_rejected() {
+ // e.g. a conversation id or workspace name pasted into the AGENT scope.
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "wecom:xxx", "read_file");
+
+ assertThatThrownBy(() -> controller.create(body, WORKSPACE_ID, adminAuth))
+ .isInstanceOf(MateClawException.class)
+ .hasMessageContaining("must be a numeric agent id");
+ verify(grantMapper, never()).insert(any(ApprovalGrant.class));
+ }
+
@Test
void until_conversation_end_requires_conversation_scope() {
- ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", "agent-1", "read_file");
+ ApprovalGrantController.CreateGrantRequest body = baseBody("AGENT", String.valueOf(AGENT_ID), "read_file");
body.grantKind = "UNTIL_CONVERSATION_END";
assertThatThrownBy(() -> controller.create(body, WORKSPACE_ID, memberAuth))
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index a10dd973..81241841 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -4424,7 +4424,7 @@ export default {
severityMedium: 'auto-approve LOW/MEDIUM findings',
severityHigh: 'auto-approve LOW/MEDIUM/HIGH findings',
scopeIdPickAgent: 'Pick an agent…',
- scopeIdWorkspaceHint: 'Pick the workspace this grant applies to (must be the workspace the conversation actually belongs to).',
+ scopeIdWorkspaceHint: 'Fixed to the current workspace. To create a grant for another workspace, switch workspaces in the console first.',
scopeIdAgentHint: 'Applies to every conversation of the selected agent within this workspace.',
scopeIdUserHint: 'USER-scope grants can only target the currently logged-in user.',
scopeIdConversationHint: 'Conversation id, as shown in the audit log "session" column (e.g. wecom:xxx).',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index 0a01d598..91a6aa31 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -4516,7 +4516,7 @@ export default {
severityMedium: '放行低/中危命中',
severityHigh: '放行低/中/高危命中',
scopeIdPickAgent: '选择智能体…',
- scopeIdWorkspaceHint: '选择策略生效的工作区(必须与会话实际所属工作区一致)。',
+ scopeIdWorkspaceHint: '固定为当前工作区。如需为其他工作区创建策略,请先在控制台切换工作区。',
scopeIdAgentHint: '选择智能体后,该智能体在本工作区的所有会话均适用。',
scopeIdUserHint: '只能为当前登录用户创建 USER 范围策略。',
scopeIdConversationHint: '填写会话 ID,可在审计日志的"会话"列查看(如 wecom:xxx)。',
diff --git a/mateclaw-ui/src/views/Security/AutoApproveGrants/index.vue b/mateclaw-ui/src/views/Security/AutoApproveGrants/index.vue
index c895123d..ca48f12f 100644
--- a/mateclaw-ui/src/views/Security/AutoApproveGrants/index.vue
+++ b/mateclaw-ui/src/views/Security/AutoApproveGrants/index.vue
@@ -207,20 +207,16 @@
-
+ disabled
+ />