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
new file mode 100644
index 00000000..5c4f7989
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/approval/grant/controller/ApprovalGrantController.java
@@ -0,0 +1,351 @@
+package vip.mate.approval.grant.controller;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+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.approval.grant.repository.ApprovalGrantMapper;
+import vip.mate.approval.grant.repository.ApprovalResolutionLogMapper;
+import vip.mate.approval.grant.service.ApprovalGrantService;
+import vip.mate.auth.model.UserEntity;
+import vip.mate.auth.service.AuthService;
+import vip.mate.common.result.R;
+import vip.mate.exception.MateClawException;
+import vip.mate.workspace.core.annotation.RequireWorkspaceRole;
+import vip.mate.workspace.core.service.WorkspaceService;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * REST surface for the auto-grant subsystem.
+ *
+ * The header {@code @RequireWorkspaceRole("member")} is the minimum gate; the
+ * §2.4.5 6-cell authorization matrix is enforced inside each handler:
+ *
+ * - {@code CONVERSATION} scope — any workspace member.
+ * - {@code USER} scope — only the actor can create a grant for themselves.
+ * - {@code AGENT} scope with explicit {@code toolName} — agent owner or admin.
+ * - {@code AGENT} scope with {@code toolName=null} — admin only, plus password.
+ * - {@code WORKSPACE} scope with explicit {@code toolName} — admin only.
+ * - {@code WORKSPACE} scope with {@code toolName=null} — admin only, plus password.
+ *
+ *
+ * Snowflake id fields ({@code id} / {@code grantedBy} / {@code revokedBy} /
+ * {@code scopeId}) are serialized as strings by the global Jackson config so the
+ * frontend keeps them as strings end-to-end (see CLAUDE.md precision convention).
+ */
+@Tag(name = "自动批准策略")
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/approval")
+@RequiredArgsConstructor
+public class ApprovalGrantController {
+
+ private static final long DEFAULT_WORKSPACE_ID = 1L;
+
+ private final ApprovalGrantService grantService;
+ private final ApprovalGrantMapper grantMapper;
+ private final ApprovalResolutionLogMapper resolutionMapper;
+ private final AuthService authService;
+ private final WorkspaceService workspaceService;
+
+ // ─── Create ─────────────────────────────────────────────────────────
+
+ @Operation(summary = "创建自动批准策略")
+ @PostMapping("/grants")
+ @RequireWorkspaceRole("member")
+ public R create(@RequestBody CreateGrantRequest body,
+ @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
+ Authentication auth) {
+ Long actorId = resolveUserId(auth);
+ Long ws = workspaceId != null ? workspaceId : DEFAULT_WORKSPACE_ID;
+
+ validate(body);
+ enforceCreationAuthorization(body, actorId, ws);
+
+ ApprovalGrant grant = new ApprovalGrant();
+ grant.setWorkspaceId(ws);
+ grant.setScopeType(body.scopeType);
+ grant.setScopeId(body.scopeId);
+ grant.setToolName(emptyToNull(body.toolName));
+ grant.setRuleId(emptyToNull(body.ruleId));
+ grant.setMaxSeverity(body.maxSeverity);
+ grant.setGrantKind(body.grantKind);
+ grant.setExpireAt(body.expireAt);
+ grant.setGrantedBy(actorId);
+ grant.setGrantedAt(LocalDateTime.now());
+ grant.setRevoked(0);
+ grant.setDeleted(0);
+ grant.setNote(body.note);
+
+ grantMapper.insert(grant);
+ log.info("[APPROVAL] Grant created: id={} scope={}/{} tool={} rule={} ceiling={} kind={} by user={}",
+ grant.getId(), grant.getScopeType(), grant.getScopeId(),
+ grant.getToolName(), grant.getRuleId(), grant.getMaxSeverity(),
+ grant.getGrantKind(), actorId);
+ return R.ok(grant);
+ }
+
+ // ─── List ───────────────────────────────────────────────────────────
+
+ @Operation(summary = "列出当前 workspace 的自动批准策略")
+ @GetMapping("/grants")
+ @RequireWorkspaceRole("member")
+ public R> list(
+ @RequestParam(required = false) String scopeType,
+ @RequestParam(required = false) String toolName,
+ @RequestParam(required = false) Integer revoked,
+ @RequestParam(required = false, defaultValue = "false") boolean mine,
+ @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId,
+ Authentication auth) {
+ Long actorId = resolveUserId(auth);
+ Long ws = workspaceId != null ? workspaceId : DEFAULT_WORKSPACE_ID;
+
+ // mine=false (看全部) 需要 admin;mine=true 任意 member 可以看自己的
+ if (!mine) {
+ workspaceService.requirePermission(ws, actorId, "admin");
+ }
+
+ var wrapper = Wrappers.lambdaQuery()
+ .eq(ApprovalGrant::getWorkspaceId, ws)
+ .eq(ApprovalGrant::getDeleted, 0)
+ .orderByDesc(ApprovalGrant::getGrantedAt);
+ if (scopeType != null && !scopeType.isEmpty()) {
+ wrapper.eq(ApprovalGrant::getScopeType, scopeType);
+ }
+ if (toolName != null && !toolName.isEmpty()) {
+ wrapper.eq(ApprovalGrant::getToolName, toolName);
+ }
+ if (revoked != null) {
+ wrapper.eq(ApprovalGrant::getRevoked, revoked);
+ }
+ if (mine) {
+ wrapper.eq(ApprovalGrant::getGrantedBy, actorId);
+ }
+ return R.ok(grantMapper.selectList(wrapper));
+ }
+
+ // ─── Active summary (chip "(N)") ────────────────────────────────────
+
+ @Operation(summary = "当前 workspace 的活跃策略数量摘要")
+ @GetMapping("/grants/active")
+ @RequireWorkspaceRole("member")
+ public R