From d1573bde092262a96d653c2494fbf5bbe61c5a19 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 8 May 2026 07:57:17 +0800 Subject: [PATCH] feat(mcp): TokenService skeleton + IssueAdmin --- internal/mcp/token_service.go | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 internal/mcp/token_service.go diff --git a/internal/mcp/token_service.go b/internal/mcp/token_service.go new file mode 100644 index 0000000..7171524 --- /dev/null +++ b/internal/mcp/token_service.go @@ -0,0 +1,135 @@ +package mcp + +import ( + "context" + "strings" + "time" + + "github.com/google/uuid" + + "github.com/yan1h/agent-coding-workflow/internal/audit" + "github.com/yan1h/agent-coding-workflow/internal/infra/errs" +) + +// TokenService 是 MCP token 的业务编排层。三类入口: +// - IssueAdmin:admin 后台手动签发,必须给 expires_at +// - IssueSystemToken:ACP supervisor 起 session 时调,绑定 acp_session_id +// - Authenticate:HTTP 中间件调,验 plaintext + 返回 user_id + scope +// +// 撤销与列表通过 Repository 直接调即可,不在 service 上加薄壳。 +type TokenService interface { + IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error) + IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) + Authenticate(ctx context.Context, plaintext string) (*MCPToken, error) + Revoke(ctx context.Context, id, byUserID uuid.UUID) error + RevokeBySession(ctx context.Context, sessionID uuid.UUID) error +} + +// IssueAdminInput 是 admin 签发时的入参。UserID 缺省 = 当前 admin 自己(由调用方填)。 +// Name 必填;ExpiresAt 必填(spec §10 决策 #14:防长期泄露)。 +type IssueAdminInput struct { + UserID uuid.UUID + Name string + Scope Scope + ExpiresAt time.Time + CreatedBy uuid.UUID // 当前 admin 用户 ID +} + +// IssueSystemTokenInput 是 ACP session 启动时的入参。 +type IssueSystemTokenInput struct { + UserID uuid.UUID + ACPSessionID uuid.UUID + ProjectIDs []uuid.UUID + Tools []string // 缺省 nil = 系统默认(不含 chat) + ExpiresIn time.Duration // 默认 24h +} + +// IssueResult 是签发返回。Plaintext 仅此一次返回;调用方须立即转交给客户端。 +type IssueResult struct { + ID uuid.UUID + Plaintext string + ExpiresAt *time.Time +} + +// defaultSystemTools 是 system token 的默认工具白名单(spec §6.2)。 +// 不含 list_recent_messages:agent 不该看用户私人对话历史。 +var defaultSystemTools = []string{ + "list_projects", "list_workspaces", "list_requirements", "list_issues", + "get_issue", "get_requirement", + "create_issue", "update_issue", "close_issue", "reopen_issue", "assign_issue", + "create_requirement", "update_requirement", "close_requirement", "reopen_requirement", "set_requirement_phase", +} + +// NewTokenService 构造 service。 +func NewTokenService(repo Repository, audit audit.Recorder) TokenService { + return &tokenService{repo: repo, audit: audit} +} + +type tokenService struct { + repo Repository + audit audit.Recorder +} + +// IssueAdmin 签发一个长期 admin token。Name + ExpiresAt 必填;scope 可空(= 全权)。 +// 写一条 audit `mcp.token.create`。 +func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error) { + if strings.TrimSpace(in.Name) == "" { + return IssueResult{}, errs.New(errs.CodeMcpTokenNameRequired, "name is required") + } + if in.ExpiresAt.IsZero() { + return IssueResult{}, errs.New(errs.CodeMcpTokenExpiresRequired, "expires_at is required for admin token") + } + + plain, hash, err := NewToken() + if err != nil { + return IssueResult{}, errs.Wrap(err, errs.CodeInternal, "generate token plaintext") + } + + id := uuid.New() + created, err := s.repo.Create(ctx, &MCPToken{ + ID: id, + TokenHash: hash, + UserID: in.UserID, + Name: in.Name, + Issuer: IssuerAdmin, + Scope: in.Scope, + ExpiresAt: &in.ExpiresAt, + CreatedBy: in.CreatedBy, + }) + if err != nil { + return IssueResult{}, err + } + + _ = s.audit.Record(ctx, audit.Entry{ + UserID: &in.CreatedBy, + Action: "mcp.token.create", + TargetType: "mcp_token", + TargetID: created.ID.String(), + Metadata: map[string]any{ + "token_id": created.ID.String(), + "user_id": in.UserID.String(), + "name": in.Name, + "scope": in.Scope, + "expires_at": in.ExpiresAt, + }, + }) + + return IssueResult{ + ID: created.ID, + Plaintext: plain, + ExpiresAt: created.ExpiresAt, + }, nil +} + +func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) { + panic("not implemented: IssueSystemToken") +} +func (s *tokenService) Authenticate(ctx context.Context, plaintext string) (*MCPToken, error) { + panic("not implemented: Authenticate") +} +func (s *tokenService) Revoke(ctx context.Context, id, byUserID uuid.UUID) error { + panic("not implemented: Revoke") +} +func (s *tokenService) RevokeBySession(ctx context.Context, sessionID uuid.UUID) error { + panic("not implemented: RevokeBySession") +}