You've already forked agentic-coding-workflow
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:
@@ -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)
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in New Issue
Block a user