You've already forked agentic-coding-workflow
feat(mcp): TokenService skeleton + IssueAdmin
This commit is contained in:
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user