feat(acp): Supervisor.Spawn extraEnv parameter

- extraEnv overrides agent_kinds.encrypted_env same-name keys (spec §6.1 #11)
- Prevents admin from hard-coding ACW_MCP_TOKEN in agent_kinds
- SessionService passes nil for now; system token wiring lands in I4
- BuildSpawnEnv exported for testing the override priority
- Fix app.go NewSupervisor call to match updated signature (from I5)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 12:46:50 +08:00
parent 3a448cee5f
commit d44ffc851f
5 changed files with 70 additions and 9 deletions
+36 -4
View File
@@ -27,6 +27,7 @@ import (
"github.com/yan1h/agent-coding-workflow/internal/audit"
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
"github.com/yan1h/agent-coding-workflow/internal/infra/notify"
"github.com/yan1h/agent-coding-workflow/internal/mcp"
"github.com/yan1h/agent-coding-workflow/internal/workspace"
)
@@ -49,9 +50,10 @@ type Supervisor struct {
repo Repository
audit audit.Recorder
notify *notify.Dispatcher
wtSvc workspace.WorktreeService
wsRepo workspace.Repository
crypto *crypto.Encryptor
wtSvc workspace.WorktreeService
wsRepo workspace.Repository
mcpTokens mcp.TokenService // spec §6.3 — onExit revokes system token
crypto *crypto.Encryptor
cfg SupervisorConfig
log *slog.Logger
}
@@ -60,6 +62,7 @@ type Supervisor struct {
// 起 goroutine,无主循环)。Stop 在 ShutdownAll 中实现。
func NewSupervisor(repo Repository, rec audit.Recorder, disp *notify.Dispatcher,
wtSvc workspace.WorktreeService, wsRepo workspace.Repository,
mcpTokens mcp.TokenService,
enc *crypto.Encryptor, cfg SupervisorConfig, log *slog.Logger) *Supervisor {
if log == nil {
log = slog.Default()
@@ -71,6 +74,7 @@ func NewSupervisor(repo Repository, rec audit.Recorder, disp *notify.Dispatcher,
notify: disp,
wtSvc: wtSvc,
wsRepo: wsRepo,
mcpTokens: mcpTokens,
crypto: enc,
cfg: cfg,
log: log,
@@ -143,12 +147,17 @@ func (s *Supervisor) GetRelay(sid uuid.UUID) *Relay {
// Spawn 启动一个子进程并完成 ACP handshake。返回的 *Process 已注册到 procs map;
// handshake 失败时调用 Kill 回滚并返回错误。调用方(SessionService)负责前置的
// worktree 占用与 acp_sessions 行 INSERT;本方法仅负责 OS 层资源与协议层握手。
func (s *Supervisor) Spawn(ctx context.Context, sess *Session, kind *AgentKind) (*Process, error) {
func (s *Supervisor) Spawn(ctx context.Context, sess *Session, kind *AgentKind, extraEnv map[string]string) (*Process, error) {
envMap, err := decryptEnv(s.crypto, kind.EncryptedEnv)
if err != nil {
return nil, err
}
// System override: extraEnv overrides agent_kinds same-name keys (spec §6.1 #11)
for k, v := range extraEnv {
envMap[k] = v
}
// strip env:只保留 PATH + 解密 overrides,避免泄漏 master key、git 凭据等。
osEnv := []string{"PATH=" + os.Getenv("PATH")}
for k, v := range envMap {
@@ -258,6 +267,22 @@ func (s *Supervisor) handshake(ctx context.Context, sess *Session, relay *Relay,
return nil
}
// BuildSpawnEnv exposes env merge logic for testing.
func (s *Supervisor) BuildSpawnEnv(kind *AgentKind, extraEnv map[string]string) ([]string, error) {
envMap, err := decryptEnv(s.crypto, kind.EncryptedEnv)
if err != nil {
return nil, err
}
for k, v := range extraEnv {
envMap[k] = v
}
out := []string{"PATH=" + os.Getenv("PATH")}
for k, v := range envMap {
out = append(out, fmt.Sprintf("%s=%s", k, v))
}
return out, nil
}
// Kill 终止指定 session 的子进程。在 Unix 平台上先发 SIGTERM 等待 grace 时间,
// 仍未退出再 Process.Kill;Windows 没有可靠 SIGTERM 实现,直接 Process.Kill。
// 始终阻塞直到 monitorProcess 关闭 done channel。Kill 是幂等的——目标 session
@@ -416,6 +441,13 @@ func (s *Supervisor) onExit(ctx context.Context, proc *Process, status SessionSt
}
}
_ = waitErr // 已通过 KilledByUs / ExitError 编码到 status / exitCode
// Revoke session's system MCP tokens (spec §6.3)
if s.mcpTokens != nil {
if err := s.mcpTokens.RevokeBySession(ctx, proc.SessionID); err != nil {
s.log.Error("acp.on_exit.mcp_revoke",
"session_id", proc.SessionID, "err", err.Error())
}
}
// 最后关 relay:广播 session_terminated 给 WS subscribers。
proc.Relay.Close(string(status), &exitCode)