diff --git a/README.md b/README.md index 14969070..430cae08 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ --- +> **Other personal AI agents are built for one person. MateClaw is the one your IT department can actually sign off on.** +> +> Multi-user workspaces. Approval-gated sensitive actions. Full audit trail. Spring Boot Actuator health monitoring. Per-channel error isolation so one chat platform's outage doesn't take down the rest. One JAR on your own machine, zero data egress. + Most AI tools die when their vendor has a bad day. Most forget you the moment the tab closes. Most give you a chatbox and call it a product. **MateClaw is the whole widget.** One deployment. Reasoning, knowledge, memory, tools, channels — built together, not bolted on. And when your primary model goes down, the next one picks up mid-sentence. diff --git a/README_zh.md b/README_zh.md index 8932c73d..85c95ea7 100644 --- a/README_zh.md +++ b/README_zh.md @@ -28,6 +28,10 @@ --- +> **别的 AI 助手是给一个人用的。MateClaw 是公司允许部署的那一个。** +> +> 多用户工作空间。敏感操作走审批。完整审计日志。Spring Boot Actuator 健康监控。单个渠道挂掉不影响其他渠道的错误隔离。一个 JAR 包跑在自己机器上,数据不出门。 + 大多数 AI 工具一到厂商抽风那天就两手一摊。关一次标签页就忘了你是谁。给你一个聊天框,就敢叫产品。 **MateClaw 是完整的一整套。** 一次部署——推理、知识、记忆、工具、多渠道入口,从第一天就一起设计,不是事后拼接。主模型挂了,下一家接着把这句话说完。 diff --git a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java index 825f1e2b..1526041b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/controller/AgentController.java @@ -4,6 +4,7 @@ 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 org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import vip.mate.channel.web.Utf8SseEmitter; @@ -11,9 +12,12 @@ import vip.mate.agent.AgentService; import vip.mate.agent.AgentState; import vip.mate.agent.model.AgentEntity; import vip.mate.audit.service.AuditEventService; +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.io.IOException; import java.util.List; @@ -34,6 +38,8 @@ public class AgentController { private final AgentService agentService; private final AuditEventService auditEventService; + private final AuthService authService; + private final WorkspaceService workspaceService; private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); @Operation(summary = "获取Agent列表") @@ -61,9 +67,12 @@ public class AgentController { @RequireWorkspaceRole("member") public R create( @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, - @RequestBody AgentEntity agent) { + @RequestBody AgentEntity agent, + Authentication auth) { // 始终注入 workspace_id,无 header 时使用默认 agent.setWorkspaceId(workspaceId != null ? workspaceId : 1L); + // RFC-077 §4.4: 记录创建者,让 member 后续可删除自建 Agent + agent.setCreatorUserId(resolveUserId(auth)); AgentEntity created = agentService.createAgent(agent); auditEventService.record("CREATE", "AGENT", String.valueOf(created.getId()), created.getName(), null); return R.ok(created); @@ -85,11 +94,24 @@ public class AgentController { @Operation(summary = "删除Agent") @DeleteMapping("/{id}") - @RequireWorkspaceRole("admin") + @RequireWorkspaceRole("member") public R delete(@PathVariable Long id, - @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) { + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, + Authentication auth) { AgentEntity agent = agentService.getAgent(id); verifyResourceWorkspace(agent.getWorkspaceId(), workspaceId); + + // RFC-077 §4.4: 三选一鉴权 — 系统 admin / workspace admin+ / 创建者本人 + Long userId = resolveUserId(auth); + boolean systemAdmin = isSystemAdmin(auth); + boolean workspaceAdmin = !systemAdmin + && workspaceService.hasPermission(agent.getWorkspaceId(), userId, "admin"); + boolean isCreator = userId.equals(agent.getCreatorUserId()); + if (!systemAdmin && !workspaceAdmin && !isCreator) { + throw new MateClawException("err.agent.delete_forbidden", 403, + "Only the creator or a workspace admin can delete this Agent"); + } + agentService.deleteAgent(id); auditEventService.record("DELETE", "AGENT", String.valueOf(id), agent.getName(), null); return R.ok(); @@ -185,4 +207,23 @@ public class AgentController { throw new MateClawException("err.common.wrong_workspace", "资源不属于当前工作区"); } } + + private Long resolveUserId(Authentication auth) { + if (auth == null) { + throw new MateClawException("err.auth.unauthenticated", 401, "Not authenticated"); + } + UserEntity user = authService.findByUsername(auth.getName()); + if (user == null) { + throw new MateClawException("err.auth.user_not_found", 401, "User not found: " + auth.getName()); + } + return user.getId(); + } + + private boolean isSystemAdmin(Authentication auth) { + if (auth == null) { + return false; + } + UserEntity user = authService.findByUsername(auth.getName()); + return user != null && "admin".equalsIgnoreCase(user.getRole()); + } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/controller/TemplateController.java b/mateclaw-server/src/main/java/vip/mate/agent/controller/TemplateController.java index 63a0a4a1..c3920a0d 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/controller/TemplateController.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/controller/TemplateController.java @@ -3,11 +3,16 @@ package vip.mate.agent.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; import vip.mate.agent.model.AgentEntity; import vip.mate.agent.model.TemplateDTO; import vip.mate.agent.service.TemplateService; +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 java.util.List; @@ -23,6 +28,7 @@ import java.util.List; public class TemplateController { private final TemplateService templateService; + private final AuthService authService; @Operation(summary = "获取模板列表") @GetMapping @@ -32,7 +38,24 @@ public class TemplateController { @Operation(summary = "应用模板创建Agent") @PostMapping("/{id}/apply") - public R apply(@PathVariable String id) { - return R.ok(templateService.applyTemplate(id)); + @RequireWorkspaceRole("member") + public R apply( + @PathVariable String id, + @RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId, + Authentication auth) { + long wsId = workspaceId != null ? workspaceId : 1L; + Long userId = resolveUserId(auth); + return R.ok(templateService.applyTemplate(id, wsId, userId)); + } + + private Long resolveUserId(Authentication auth) { + if (auth == null) { + throw new MateClawException("err.auth.unauthenticated", 401, "Not authenticated"); + } + UserEntity user = authService.findByUsername(auth.getName()); + if (user == null) { + throw new MateClawException("err.auth.user_not_found", 401, "User not found: " + auth.getName()); + } + return user.getId(); } } diff --git a/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java b/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java index 664d2d13..87b7bf9c 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/model/AgentEntity.java @@ -52,6 +52,9 @@ public class AgentEntity { /** 所属工作区 ID(默认 1 = default) */ private Long workspaceId; + /** Creator user ID — backfilled on create; lets members delete their own Agents without admin role */ + private Long creatorUserId; + /** 默认思考深度:off / low / medium / high / max,null 表示跟随模型默认 */ private String defaultThinkingLevel; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java index 14359e03..8df27a68 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/service/TemplateService.java @@ -65,17 +65,19 @@ public class TemplateService { /** * 应用模板创建 Agent 及其工作区文件 * - * @param templateId 模板 ID + * @param templateId 模板 ID + * @param workspaceId 目标工作区 ID(来自 X-Workspace-Id header) + * @param creatorUserId 当前用户 ID(用于 RFC-077 创建者归属) * @return 创建的 AgentEntity */ @Transactional - public AgentEntity applyTemplate(String templateId) { + public AgentEntity applyTemplate(String templateId, Long workspaceId, Long creatorUserId) { TemplateDTO template = listTemplates().stream() .filter(t -> t.getId().equals(templateId)) .findFirst() .orElseThrow(() -> new MateClawException("err.agent.template_not_found", "模板不存在: " + templateId)); - // 1. 创建 Agent + // 1. 创建 Agent — RFC-077: 显式注入 workspaceId/creatorUserId,避免 DB 默认值兜底成 1(issue #26 Bug A) AgentEntity agent = new AgentEntity(); agent.setName(template.getName()); agent.setDescription(template.getDescription()); @@ -83,6 +85,8 @@ public class TemplateService { agent.setIcon(template.getIcon()); agent.setTags(template.getTags()); agent.setMaxIterations(template.getMaxIterations()); + agent.setWorkspaceId(workspaceId); + agent.setCreatorUserId(creatorUserId); AgentEntity created = agentService.createAgent(agent); // 2. 创建工作区文件 diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V61__agent_creator_user_id.sql b/mateclaw-server/src/main/resources/db/migration/h2/V61__agent_creator_user_id.sql new file mode 100644 index 00000000..8d15b911 --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/h2/V61__agent_creator_user_id.sql @@ -0,0 +1,6 @@ +-- RFC-077 §4.1: track which user created an Agent, so members can delete +-- their own Agents without needing workspace admin role (issue #26 Bug B). + +ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS creator_user_id BIGINT; + +CREATE INDEX IF NOT EXISTS idx_agent_creator_user ON mate_agent(creator_user_id); diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V61__agent_creator_user_id.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V61__agent_creator_user_id.sql new file mode 100644 index 00000000..3913056a --- /dev/null +++ b/mateclaw-server/src/main/resources/db/migration/mysql/V61__agent_creator_user_id.sql @@ -0,0 +1,10 @@ +-- RFC-077 §4.1: track which user created an Agent, so members can delete +-- their own Agents without needing workspace admin role (issue #26 Bug B). + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND COLUMN_NAME = 'creator_user_id'); +SET @s := IF(@c = 0, 'ALTER TABLE mate_agent ADD COLUMN creator_user_id BIGINT', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @c := (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'mate_agent' AND INDEX_NAME = 'idx_agent_creator_user'); +SET @s := IF(@c = 0, 'CREATE INDEX idx_agent_creator_user ON mate_agent(creator_user_id)', 'SELECT 1'); +PREPARE stmt FROM @s; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/mateclaw-server/src/main/resources/messages.properties b/mateclaw-server/src/main/resources/messages.properties index d1b09190..d89eced8 100644 --- a/mateclaw-server/src/main/resources/messages.properties +++ b/mateclaw-server/src/main/resources/messages.properties @@ -185,6 +185,7 @@ err.agent.dashscope_key_missing=DashScope API Key \u672a\u914d\u7f6e err.agent.anthropic_not_configured=Anthropic Provider \u672a\u5b8c\u6210\u914d\u7f6e err.agent.anthropic_key_invalid=Anthropic API Key \u672a\u914d\u7f6e\u6216\u65e0\u6548 err.agent.template_not_found=\u6a21\u677f\u4e0d\u5b58\u5728 +err.agent.delete_forbidden=\u53ea\u6709\u521b\u5efa\u8005\u6216\u5de5\u4f5c\u533a\u7ba1\u7406\u5458\u53ef\u5220\u9664\u6b64 Agent err.common.wrong_workspace=\u8d44\u6e90\u4e0d\u5c5e\u4e8e\u5f53\u524d\u5de5\u4f5c\u533a # llm err.llm.model_config_not_found=\u6a21\u578b\u914d\u7f6e\u4e0d\u5b58\u5728 diff --git a/mateclaw-server/src/main/resources/messages_en.properties b/mateclaw-server/src/main/resources/messages_en.properties index c253edf9..164a00cb 100644 --- a/mateclaw-server/src/main/resources/messages_en.properties +++ b/mateclaw-server/src/main/resources/messages_en.properties @@ -197,6 +197,7 @@ err.agent.dashscope_key_missing=DashScope API Key not configured err.agent.anthropic_not_configured=Anthropic Provider not configured err.agent.anthropic_key_invalid=Anthropic API Key not configured or invalid err.agent.template_not_found=Template not found +err.agent.delete_forbidden=Only the creator or a workspace admin can delete this Agent err.common.wrong_workspace=Resource does not belong to current workspace # llm err.llm.model_config_not_found=Model config not found