From 3a448cee5f4491b8814a7d53ae7270b1b6e7ceac Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Fri, 8 May 2026 12:44:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(mcp):=20IssueSystemTokenWithTx=20for=20cro?= =?UTF-8?q?ss-module=20tx=20(spec=20=C2=A76.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract issueSystemToken helper; pool-bound + tx-bound entries share impl - IssueSystemTokenWithTx accepts pgx.Tx, uses repo.WithTx(tx) for DB writes - Audit Record stays outside tx (audit failure doesn't roll back business path) Co-Authored-By: Claude Opus 4.6 --- internal/mcp/token_service.go | 22 +++++++++++++++------- internal/mcp/token_service_test.go | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/internal/mcp/token_service.go b/internal/mcp/token_service.go index b227ac4..0c71791 100644 --- a/internal/mcp/token_service.go +++ b/internal/mcp/token_service.go @@ -6,6 +6,7 @@ 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" @@ -20,6 +21,7 @@ import ( type TokenService interface { IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) + IssueSystemTokenWithTx(ctx context.Context, tx pgx.Tx, in IssueSystemTokenInput) (IssueResult, error) Authenticate(ctx context.Context, plaintext string) (*Token, error) Revoke(ctx context.Context, id, byUserID uuid.UUID) error RevokeBySession(ctx context.Context, sessionID uuid.UUID) error @@ -137,15 +139,21 @@ func (s *tokenService) IssueAdmin(ctx context.Context, in IssueAdminInput) (Issu // FK 校验失败。spec §6.1 明确建议同事务提交。本 service 不直接管事务边界(由 // ACP SessionService 在 Create 流程内编排),但调用顺序由调用方保证。 // -// TODO(Part 3): 当前 pgRepo 持有 *pgxpool.Pool 并忽略 ctx 中可能存在的 pgx tx, -// 因此严格意义上 IssueSystemToken 与 acp_sessions row 的写入并不在同一事务里 —— -// 调用顺序虽然能避开 FK 错误,但不是真正的原子提交。要兑现 spec §6.1 同事务 -// 保证,Part 3 需要为 Repository 增加 `WithTx(tx pgx.Tx) Repository` 构造, -// 并在 acp.SessionService.Create 中以同一事务执行 acp_sessions 插入与本次签发。 -// // 默认 ExpiresIn = 24h;默认 Tools 为内置 PM 工具白名单(spec §6.2)。 // 写一条 audit `mcp.token.create_system`。 func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) { + return s.issueSystemToken(ctx, s.repo, in) +} + +// IssueSystemTokenWithTx 与 IssueSystemToken 相同,但所有 DB 写操作走传入事务。 +// 调用方(如 acp.SessionService.Create)负责在同一个 tx 里先写入 acp_sessions row, +// 再调本方法签发 token,实现 spec §6.1 同事务原子提交。 +func (s *tokenService) IssueSystemTokenWithTx(ctx context.Context, tx pgx.Tx, in IssueSystemTokenInput) (IssueResult, error) { + return s.issueSystemToken(ctx, s.repo.WithTx(tx), in) +} + +// issueSystemToken 是共享实现。repo 参数决定 DB 写入走 pool 还是 tx。 +func (s *tokenService) issueSystemToken(ctx context.Context, repo Repository, in IssueSystemTokenInput) (IssueResult, error) { if in.ACPSessionID == uuid.Nil { return IssueResult{}, errs.New(errs.CodeInvalidInput, "acp_session_id is required for system token") } @@ -168,7 +176,7 @@ func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemToken } id := uuid.New() - created, err := s.repo.Create(ctx, &Token{ + created, err := repo.Create(ctx, &Token{ ID: id, TokenHash: hash, UserID: in.UserID, diff --git a/internal/mcp/token_service_test.go b/internal/mcp/token_service_test.go index 334e12b..aa01f80 100644 --- a/internal/mcp/token_service_test.go +++ b/internal/mcp/token_service_test.go @@ -385,3 +385,24 @@ func TestIssueSystemToken_NilUserID(t *testing.T) { // 编译期保证:类型实现接口 var _ = errors.New + +func TestIssueSystemTokenWithTx_DelegatesToRepoWithTx(t *testing.T) { + t.Parallel() + repo := newFakeRepo() + au := &fakeAudit{} + svc := mcp.NewTokenService(repo, au, nil) + + uid := uuid.New() + sid := uuid.New() + pid := uuid.New() + res, err := svc.IssueSystemTokenWithTx(context.Background(), nil, mcp.IssueSystemTokenInput{ + UserID: uid, ACPSessionID: sid, ProjectIDs: []uuid.UUID{pid}, + }) + require.NoError(t, err) + assert.NotEmpty(t, res.Plaintext) + assert.Equal(t, []string{"mcp.token.create_system"}, au.actions()) + + got, err := repo.GetByID(context.Background(), res.ID) + require.NoError(t, err) + assert.Equal(t, mcp.IssuerSystem, got.Issuer) +}