Files
agentic-coding-workflow/internal/acp/domain.go
T
2026-06-12 11:44:19 +08:00

281 lines
9.8 KiB
Go

// Package acp 实现 ACP(Agent Client Protocol)Client 模块。
// 模块结构(spec §13 决策 #12):单 package + 多文件,与现有 chat/jobs/workspace 一致。
//
// 三个聚合根:
// - AgentKind:admin 注册的 agent 类型(claude_code/gemini/...),含加密 env
// - Session:用户启动的一个 ACP 会话,绑定 workspace + agent kind + (issue/req?)
// - Event:JSON-RPC 双向消息流,每条入库
package acp
import (
"context"
"time"
"github.com/google/uuid"
)
// SessionStatus 是 acp_sessions.status 枚举。
type SessionStatus string
const (
SessionStarting SessionStatus = "starting"
SessionRunning SessionStatus = "running"
SessionCrashed SessionStatus = "crashed"
SessionExited SessionStatus = "exited"
)
// IsActive 判定 session 是否仍占资源(worktree、subprocess、限流计数)。
func (s SessionStatus) IsActive() bool {
return s == SessionStarting || s == SessionRunning
}
// EventDirection 标识事件流向。in = client→agent,out = agent→client。
type EventDirection string
const (
DirectionIn EventDirection = "in"
DirectionOut EventDirection = "out"
)
// RPCKind 标识 JSON-RPC 消息类型。
type RPCKind string
const (
RPCRequest RPCKind = "request"
RPCResponse RPCKind = "response"
RPCNotification RPCKind = "notification"
RPCError RPCKind = "error"
)
// ClientType 标识 agent CLI 的客户端类型,决定 spawn 时注入哪个配置目录
// 重定向变量(CLAUDE_CONFIG_DIR / CODEX_HOME / GEMINI_CLI_HOME)以及前端
// 渲染哪套配置表单。generic 仅注入 HOME。
type ClientType string
const (
ClientClaudeCode ClientType = "claude_code"
ClientCodex ClientType = "codex"
ClientGemini ClientType = "gemini"
ClientGeneric ClientType = "generic"
)
// Valid 判定 client_type 是否为已知枚举值。
func (t ClientType) Valid() bool {
switch t {
case ClientClaudeCode, ClientCodex, ClientGemini, ClientGeneric:
return true
}
return false
}
// ACW 自家 MCP 注入约定:session 创建时签发的 system token 与 MCP 公网地址
// 经 extraEnv 注入子进程,同时在 session/new 握手时作为 http MCP server 下发。
const (
EnvMCPToken = "ACW_MCP_TOKEN"
EnvMCPURL = "ACW_MCP_URL"
// AcwMcpServerName 是自家 MCP 在 mcpServers 里的保留名,第三方条目不可占用。
AcwMcpServerName = "acw"
)
// McpServerType 是第三方 MCP server 的传输类型。stdio 是 ACP 基线(所有 agent
// 必须支持);http / sse 需 agent 在 initialize 中声明 mcpCapabilities 才下发。
type McpServerType string
const (
McpStdio McpServerType = "stdio"
McpHTTP McpServerType = "http"
McpSSE McpServerType = "sse"
)
// McpServerSpec 是 admin 在 AgentKind 上配置的一条 MCP server。session/new
// 握手时转换为 ACP wire 格式随 mcpServers 下发。Env / Headers 可能含密钥,
// 整个列表加密落库(encrypted_mcp_servers);明文仅返回给 admin 编辑。
type McpServerSpec struct {
Name string `json:"name"`
Type McpServerType `json:"type"`
Command string `json:"command,omitempty"` // stdio
Args []string `json:"args,omitempty"` // stdio
Env map[string]string `json:"env,omitempty"` // stdio
URL string `json:"url,omitempty"` // http / sse
Headers map[string]string `json:"headers,omitempty"` // http / sse
}
// AgentKind 是 admin 注册的 agent 类型记录。EncryptedEnv 是 AES-GCM 密文,
// 仅 supervisor spawn 时解密为 map 注入子进程。其他场景不解密、不出 API。
// EncryptedMCPServers 同为密文,但明文经 API 返回给 admin 供编辑。
type AgentKind struct {
ID uuid.UUID
Name string
DisplayName string
Description string
BinaryPath string
Args []string
EncryptedEnv []byte
Enabled bool
ToolAllowlist []string // 命中的工具调用自动放行;含 "*" 表示放行全部可识别的工具(无法识别名称的调用仍走人工审批)
ClientType ClientType
EncryptedMCPServers []byte
CreatedBy uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
}
// ConfigFile 是 AgentKind 维度的 agent CLI 配置文件(如 .claude/settings.json、
// .codex/config.toml)。RelPath 相对受管 home 目录;EncryptedContent 是 AES-GCM
// 密文(内容可能含 auth token),spawn 前解密物化到受管目录。
type ConfigFile struct {
ID uuid.UUID
AgentKindID uuid.UUID
RelPath string
EncryptedContent []byte
UpdatedBy uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
}
// Session 是一次 ACP 会话。AgentSessionID 是 agent 侧的 session 标识(来自
// session/new response),与本表 ID 不同,仅用于 ACP 协议 method 内填充。
type Session struct {
ID uuid.UUID
WorkspaceID uuid.UUID
ProjectID uuid.UUID
AgentKindID uuid.UUID
UserID uuid.UUID
IssueID *uuid.UUID
RequirementID *uuid.UUID
AgentSessionID *string
Branch string
CwdPath string
IsMainWorktree bool
Status SessionStatus
PID *int32
ExitCode *int32
LastError *string
StartedAt time.Time
EndedAt *time.Time
}
// Event 是一条入库的 JSON-RPC 消息。Payload 是 JSON-RPC envelope 完整 JSON
// 字节(不解析为结构体,便于无损 replay);超过 64KB 的会按 §3.5 规则裁剪。
type Event struct {
ID int64
SessionID uuid.UUID
Direction EventDirection
RPCKind RPCKind
Method *string
Payload []byte
PayloadSize int32
Truncated bool
CreatedAt time.Time
}
// Caller 表示发起请求的用户上下文,与 PM 模块的 Caller 同语义。
type Caller struct {
UserID uuid.UUID
IsAdmin bool
}
// AgentKindService 暴露 AgentKind 的应用服务方法。env 走"完整 map 替换"
// 语义:每次 PATCH 传完整 env,nil 表示不修改;非 nil 整体加密重写。
type AgentKindService interface {
Create(ctx context.Context, c Caller, in CreateAgentKindInput) (*AgentKind, error)
List(ctx context.Context, c Caller) ([]*AgentKind, error)
Get(ctx context.Context, c Caller, id uuid.UUID) (*AgentKind, error)
Update(ctx context.Context, c Caller, id uuid.UUID, in UpdateAgentKindInput) (*AgentKind, error)
Delete(ctx context.Context, c Caller, id uuid.UUID) error
// 配置文件管理(admin only)。与 env 不同:内容明文返回给 admin 供编辑,
// 存储层加密。content 是明文。
ListConfigFiles(ctx context.Context, c Caller, kindID uuid.UUID) ([]*ConfigFileContent, error)
UpsertConfigFile(ctx context.Context, c Caller, kindID uuid.UUID, relPath, content string) (*ConfigFileContent, error)
DeleteConfigFile(ctx context.Context, c Caller, kindID uuid.UUID, relPath string) error
}
// ConfigFileContent 是解密后的配置文件视图(service → handler)。
type ConfigFileContent struct {
RelPath string
Content string
UpdatedAt time.Time
}
// SessionService 暴露 Session 聚合根的应用服务方法。
type SessionService interface {
Create(ctx context.Context, c Caller, in CreateSessionInput) (*Session, error)
Get(ctx context.Context, c Caller, id uuid.UUID) (*Session, error)
List(ctx context.Context, c Caller, f SessionListFilter) ([]*Session, error)
Terminate(ctx context.Context, c Caller, id uuid.UUID) error
}
// SessionListFilter 控制 sessions 列表的过滤维度。
// All 仅 admin 可用(查看全部用户);RequirementID nil 表示不按需求过滤。
type SessionListFilter struct {
All bool
RequirementID *uuid.UUID
}
// CreateSessionInput 是创建 session 的入参。
// branch / issue_number / requirement_number 三选一或全空(spec §8.1)。
type CreateSessionInput struct {
WorkspaceID uuid.UUID
AgentKindID uuid.UUID
Branch *string
IssueNumber *int
RequirementNumber *int
InitialPrompt *string
}
// CreateAgentKindInput 携带创建必需字段。Args / Env 可为 nil。
// ClientType 为空时默认 generic。
type CreateAgentKindInput struct {
Name string
DisplayName string
Description string
BinaryPath string
Args []string
Env map[string]string
Enabled bool
ToolAllowlist []string
ClientType ClientType
MCPServers []McpServerSpec
}
// UpdateAgentKindInput 是 patch 输入。Name 不可改(创建后稳定,引用约束)。
// Env 三态:nil = 不修改;空 map = 清空全部 env;非空 = 整体替换。
type UpdateAgentKindInput struct {
DisplayName *string
Description *string
BinaryPath *string
Args []string // nil = 不改;非 nil = 替换
Env map[string]string // nil = 不改;非 nil(含空)= 替换
Enabled *bool
ToolAllowlist []string // nil = 不改;非 nil(含空)= 替换
ClientType *ClientType // nil = 不改
MCPServers []McpServerSpec // nil = 不改;非 nil(含空)= 替换
}
// PermissionStatus 是 acp_permission_requests.status 枚举。
type PermissionStatus string
const (
PermissionPending PermissionStatus = "pending"
PermissionApproved PermissionStatus = "approved"
PermissionDenied PermissionStatus = "denied"
PermissionAuto PermissionStatus = "auto"
PermissionExpired PermissionStatus = "expired"
)
// PermissionRequest 是一条代理发起的工具调用授权请求记录。
type PermissionRequest struct {
ID uuid.UUID
SessionID uuid.UUID
AgentRequestID string
ToolName string
ToolCall []byte // 原始 toolCall JSON
Options []byte // 原始 options JSON
Status PermissionStatus
ChosenOptionID *string
DecidedBy *uuid.UUID
DecidedAt *time.Time
CreatedAt time.Time
}