feat(acp): SessionService.Create issues system MCP token in tx

- InTx wraps InsertSession + IssueSystemTokenWithTx (spec §6.1 atomic guarantee)
- Tx failure -> releaseOnFailure (worktree rollback)
- Spawn called with extraEnv = ACW_MCP_TOKEN + ACW_MCP_URL
- Spawn failure -> RevokeBySession (idempotent) + releaseOnFailure
- app.go SessionService construction updated with mcpTokens + config
- Unit tests: token issuance happy path + rollback on token failure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 13:01:16 +08:00
parent 146dca7738
commit 18f6c8bd2e
5 changed files with 365 additions and 17 deletions
+49 -17
View File
@@ -10,17 +10,21 @@ import (
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/yan1h/agent-coding-workflow/internal/audit"
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
"github.com/yan1h/agent-coding-workflow/internal/mcp"
"github.com/yan1h/agent-coding-workflow/internal/project"
"github.com/yan1h/agent-coding-workflow/internal/workspace"
)
// SessionServiceConfig 是 service 的限流配置(从 AcpConfig 派生)。
// SessionServiceConfig 是 service 的限流 + MCP 配置(从 AcpConfig / MCPConfig 派生)。
type SessionServiceConfig struct {
MaxActiveGlobal int
MaxActivePerUser int
SystemTokenTTL time.Duration
MCPPublicURL string
}
// ProjectAccess 是 acp 模块对 project 模块的窄接口;adapter 在 app.go 实现。
@@ -31,24 +35,25 @@ type ProjectAccess interface {
}
type sessionService struct {
repo Repository
wsSvc workspace.WorkspaceService
wtSvc workspace.WorktreeService
wsRepo workspace.Repository
pa ProjectAccess
sup *Supervisor
audit audit.Recorder
cfg SessionServiceConfig
repo Repository
wsSvc workspace.WorkspaceService
wtSvc workspace.WorktreeService
wsRepo workspace.Repository
pa ProjectAccess
sup *Supervisor
audit audit.Recorder
cfg SessionServiceConfig
mcpTokens mcp.TokenService // spec §6.1 — system token issuance
}
// NewSessionService 构造 SessionService。
func NewSessionService(repo Repository, wsSvc workspace.WorkspaceService,
wtSvc workspace.WorktreeService, wsRepo workspace.Repository,
pa ProjectAccess, sup *Supervisor, rec audit.Recorder,
cfg SessionServiceConfig) SessionService {
cfg SessionServiceConfig, mcpTokens mcp.TokenService) SessionService {
return &sessionService{
repo: repo, wsSvc: wsSvc, wtSvc: wtSvc, wsRepo: wsRepo,
pa: pa, sup: sup, audit: rec, cfg: cfg,
pa: pa, sup: sup, audit: rec, cfg: cfg, mcpTokens: mcpTokens,
}
}
@@ -192,7 +197,7 @@ func (s *sessionService) Create(ctx context.Context, c Caller, in CreateSessionI
}
}
// 7. INSERT
// 7. INSERT acp_sessions + system MCP token(spec §6.1 atomic guarantee)
sess := &Session{
ID: sessionID, WorkspaceID: ws.ID, ProjectID: ws.ProjectID,
AgentKindID: kind.ID, UserID: c.UserID,
@@ -200,10 +205,32 @@ func (s *sessionService) Create(ctx context.Context, c Caller, in CreateSessionI
Branch: branch, CwdPath: cwdPath, IsMainWorktree: isMain,
Status: SessionStarting,
}
out, err := s.repo.InsertSession(ctx, sess)
if err != nil {
var (
out *Session
tokenRes mcp.IssueResult
)
txErr := s.repo.InTx(ctx, func(txCtx context.Context, tx pgx.Tx) error {
inserted, err := s.repo.WithTx(tx).InsertSession(txCtx, sess)
if err != nil {
return err
}
out = inserted
res, err := s.mcpTokens.IssueSystemTokenWithTx(txCtx, tx, mcp.IssueSystemTokenInput{
UserID: c.UserID,
ACPSessionID: inserted.ID,
ProjectIDs: []uuid.UUID{ws.ProjectID},
ExpiresIn: s.cfg.SystemTokenTTL,
})
if err != nil {
return err
}
tokenRes = res
return nil
})
if txErr != nil {
s.releaseOnFailure(ctx, sess, sessCaller, worktreeID)
return nil, err
return nil, txErr
}
// 8. audit
@@ -215,11 +242,16 @@ func (s *sessionService) Create(ctx context.Context, c Caller, in CreateSessionI
"requirement_id": reqID,
})
// 9. async spawn(失败时回滚 worktree)
// 9. async spawn(失败时回滚 worktree + revoke MCP token
extraEnv := map[string]string{
"ACW_MCP_TOKEN": tokenRes.Plaintext,
"ACW_MCP_URL": s.cfg.MCPPublicURL,
}
go func() {
spawnCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if _, err := s.sup.Spawn(spawnCtx, out, kind, nil); err != nil {
if _, err := s.sup.Spawn(spawnCtx, out, kind, extraEnv); err != nil {
_ = s.mcpTokens.RevokeBySession(spawnCtx, out.ID)
s.releaseOnFailure(spawnCtx, out, sessCaller, worktreeID)
}
}()