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") }