You've already forked agentic-coding-workflow
feat(mcp): IssueSystemTokenWithTx for cross-module tx (spec §6.1)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
"github.com/yan1h/agent-coding-workflow/internal/audit"
|
||||||
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
||||||
@@ -20,6 +21,7 @@ import (
|
|||||||
type TokenService interface {
|
type TokenService interface {
|
||||||
IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error)
|
IssueAdmin(ctx context.Context, in IssueAdminInput) (IssueResult, error)
|
||||||
IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (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)
|
Authenticate(ctx context.Context, plaintext string) (*Token, error)
|
||||||
Revoke(ctx context.Context, id, byUserID uuid.UUID) error
|
Revoke(ctx context.Context, id, byUserID uuid.UUID) error
|
||||||
RevokeBySession(ctx context.Context, sessionID 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 不直接管事务边界(由
|
// FK 校验失败。spec §6.1 明确建议同事务提交。本 service 不直接管事务边界(由
|
||||||
// ACP SessionService 在 Create 流程内编排),但调用顺序由调用方保证。
|
// 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)。
|
// 默认 ExpiresIn = 24h;默认 Tools 为内置 PM 工具白名单(spec §6.2)。
|
||||||
// 写一条 audit `mcp.token.create_system`。
|
// 写一条 audit `mcp.token.create_system`。
|
||||||
func (s *tokenService) IssueSystemToken(ctx context.Context, in IssueSystemTokenInput) (IssueResult, error) {
|
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 {
|
if in.ACPSessionID == uuid.Nil {
|
||||||
return IssueResult{}, errs.New(errs.CodeInvalidInput, "acp_session_id is required for system token")
|
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()
|
id := uuid.New()
|
||||||
created, err := s.repo.Create(ctx, &Token{
|
created, err := repo.Create(ctx, &Token{
|
||||||
ID: id,
|
ID: id,
|
||||||
TokenHash: hash,
|
TokenHash: hash,
|
||||||
UserID: in.UserID,
|
UserID: in.UserID,
|
||||||
|
|||||||
@@ -385,3 +385,24 @@ func TestIssueSystemToken_NilUserID(t *testing.T) {
|
|||||||
|
|
||||||
// 编译期保证:类型实现接口
|
// 编译期保证:类型实现接口
|
||||||
var _ = errors.New
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user