You've already forked agentic-coding-workflow
feat(workspace): session-scoped Caller.Holder + ReleaseByHolder for ACP integration
This commit is contained in:
@@ -112,14 +112,21 @@ type FetchTarget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Caller 表示发起请求的用户上下文。Service 据此判定鉴权与 holder 字符串。
|
// Caller 表示发起请求的用户上下文。Service 据此判定鉴权与 holder 字符串。
|
||||||
|
// SessionID 非 nil 时,Holder() 返回 "session:<uuid>"(ACP 模块用),
|
||||||
|
// 否则返回 "user:<uuid>"。
|
||||||
type Caller struct {
|
type Caller struct {
|
||||||
UserID uuid.UUID
|
UserID uuid.UUID
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
|
SessionID *uuid.UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holder 把 Caller 折成 acquire_holder 字符串。Plan #4 的 ACP supervisor 会用
|
// Holder 把 Caller 折成 acquire_holder 字符串。
|
||||||
// "session:<sid>" 形式调相同接口。
|
// - SessionID 非 nil → "session:<sid>"(ACP supervisor)
|
||||||
|
// - 否则 → "user:<uid>"(普通 HTTP 调用)
|
||||||
func (c Caller) Holder() string {
|
func (c Caller) Holder() string {
|
||||||
|
if c.SessionID != nil {
|
||||||
|
return "session:" + c.SessionID.String()
|
||||||
|
}
|
||||||
return "user:" + c.UserID.String()
|
return "user:" + c.UserID.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +154,10 @@ type WorktreeService interface {
|
|||||||
Delete(ctx context.Context, c Caller, wtID uuid.UUID) error
|
Delete(ctx context.Context, c Caller, wtID uuid.UUID) error
|
||||||
Acquire(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error)
|
Acquire(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error)
|
||||||
Release(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error)
|
Release(ctx context.Context, c Caller, wtID uuid.UUID) (*Worktree, error)
|
||||||
|
// ReleaseByHolder 按 holder 字符串释放所有该 holder 持有的 worktree。
|
||||||
|
// 由 ACP startup reaper / supervisor.onExit 在不知道 wtID 时使用。
|
||||||
|
// 不写 audit(caller 自己写 — audit 上下文是 caller 知道的)。
|
||||||
|
ReleaseByHolder(ctx context.Context, holder string) ([]*Worktree, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GitOpsService 暴露 status/commit/push 操作。target 既可以是 Workspace(main_path)
|
// GitOpsService 暴露 status/commit/push 操作。target 既可以是 Workspace(main_path)
|
||||||
|
|||||||
@@ -81,3 +81,13 @@ DELETE FROM workspace_worktrees WHERE id = $1;
|
|||||||
UPDATE workspace_worktrees
|
UPDATE workspace_worktrees
|
||||||
SET status = 'idle'
|
SET status = 'idle'
|
||||||
WHERE id = $1 AND status = 'pruning';
|
WHERE id = $1 AND status = 'pruning';
|
||||||
|
|
||||||
|
-- name: ReleaseWorktreesByHolder :many
|
||||||
|
UPDATE workspace_worktrees
|
||||||
|
SET status = 'idle',
|
||||||
|
active_holder = NULL,
|
||||||
|
acquired_at = NULL,
|
||||||
|
last_used_at = now(),
|
||||||
|
prune_warning_at = NULL
|
||||||
|
WHERE active_holder = $1 AND status = 'active'
|
||||||
|
RETURNING *;
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ type Repository interface {
|
|||||||
TryStartPrune(ctx context.Context, id uuid.UUID) (*Worktree, error)
|
TryStartPrune(ctx context.Context, id uuid.UUID) (*Worktree, error)
|
||||||
FinishPruneSuccess(ctx context.Context, id uuid.UUID) error
|
FinishPruneSuccess(ctx context.Context, id uuid.UUID) error
|
||||||
FinishPruneRollback(ctx context.Context, id uuid.UUID) error
|
FinishPruneRollback(ctx context.Context, id uuid.UUID) error
|
||||||
|
// ReleaseWorktreesByHolder 一次性释放某 holder 持有的所有活跃 worktree。
|
||||||
|
// 用于 ACP 启动 reaper / supervisor.onExit 不知道具体 wtID 时的清理。
|
||||||
|
ReleaseWorktreesByHolder(ctx context.Context, holder string) ([]*Worktree, error)
|
||||||
|
|
||||||
// Workspace fetch (jobs runner)
|
// Workspace fetch (jobs runner)
|
||||||
ListFetchTargets(ctx context.Context) ([]FetchTarget, error)
|
ListFetchTargets(ctx context.Context) ([]FetchTarget, error)
|
||||||
@@ -403,6 +406,18 @@ func (r *pgRepo) FinishPruneRollback(ctx context.Context, id uuid.UUID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *pgRepo) ReleaseWorktreesByHolder(ctx context.Context, holder string) ([]*Worktree, error) {
|
||||||
|
rows, err := r.q.ReleaseWorktreesByHolder(ctx, &holder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.Wrap(err, errs.CodeInternal, "release worktrees by holder")
|
||||||
|
}
|
||||||
|
out := make([]*Worktree, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, rowToWorktree(row))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Credential =====
|
// ===== Credential =====
|
||||||
|
|
||||||
func rowToCredential(r workspacesqlc.GitCredential) *Credential {
|
func rowToCredential(r workspacesqlc.GitCredential) *Credential {
|
||||||
|
|||||||
@@ -282,6 +282,48 @@ func (q *Queries) MarkPruneWarning(ctx context.Context, id pgtype.UUID) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const releaseWorktreesByHolder = `-- name: ReleaseWorktreesByHolder :many
|
||||||
|
UPDATE workspace_worktrees
|
||||||
|
SET status = 'idle',
|
||||||
|
active_holder = NULL,
|
||||||
|
acquired_at = NULL,
|
||||||
|
last_used_at = now(),
|
||||||
|
prune_warning_at = NULL
|
||||||
|
WHERE active_holder = $1 AND status = 'active'
|
||||||
|
RETURNING id, workspace_id, branch, path, status, active_holder, acquired_at, last_used_at, created_at, prune_warning_at
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ReleaseWorktreesByHolder(ctx context.Context, activeHolder *string) ([]WorkspaceWorktree, error) {
|
||||||
|
rows, err := q.db.Query(ctx, releaseWorktreesByHolder, activeHolder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []WorkspaceWorktree
|
||||||
|
for rows.Next() {
|
||||||
|
var i WorkspaceWorktree
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.WorkspaceID,
|
||||||
|
&i.Branch,
|
||||||
|
&i.Path,
|
||||||
|
&i.Status,
|
||||||
|
&i.ActiveHolder,
|
||||||
|
&i.AcquiredAt,
|
||||||
|
&i.LastUsedAt,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.PruneWarningAt,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const setWorktreeActive = `-- name: SetWorktreeActive :one
|
const setWorktreeActive = `-- name: SetWorktreeActive :one
|
||||||
UPDATE workspace_worktrees
|
UPDATE workspace_worktrees
|
||||||
SET status = 'active',
|
SET status = 'active',
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ func (f *fakeRepo) TryStartPrune(_ context.Context, _ uuid.UUID) (*Worktree, err
|
|||||||
}
|
}
|
||||||
func (f *fakeRepo) FinishPruneSuccess(_ context.Context, _ uuid.UUID) error { return nil }
|
func (f *fakeRepo) FinishPruneSuccess(_ context.Context, _ uuid.UUID) error { return nil }
|
||||||
func (f *fakeRepo) FinishPruneRollback(_ context.Context, _ uuid.UUID) error { return nil }
|
func (f *fakeRepo) FinishPruneRollback(_ context.Context, _ uuid.UUID) error { return nil }
|
||||||
|
func (f *fakeRepo) ReleaseWorktreesByHolder(_ context.Context, _ string) ([]*Worktree, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
func (f *fakeRepo) UpsertCredential(_ context.Context, c *Credential) (*Credential, error) {
|
func (f *fakeRepo) UpsertCredential(_ context.Context, c *Credential) (*Credential, error) {
|
||||||
cp := *c
|
cp := *c
|
||||||
f.creds[c.WorkspaceID] = &cp
|
f.creds[c.WorkspaceID] = &cp
|
||||||
|
|||||||
@@ -184,6 +184,12 @@ func (s *worktreeService) Release(ctx context.Context, c Caller, wtID uuid.UUID)
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReleaseByHolder 释放给定 holder 字符串持有的所有活跃 worktree。
|
||||||
|
// 实现仅委托 repo;audit 由 caller 在更高层根据自己上下文写入。
|
||||||
|
func (s *worktreeService) ReleaseByHolder(ctx context.Context, holder string) ([]*Worktree, error) {
|
||||||
|
return s.repo.ReleaseWorktreesByHolder(ctx, holder)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *worktreeService) audit(ctx context.Context, uid *uuid.UUID, action, ttype, tid string, meta map[string]any) {
|
func (s *worktreeService) audit(ctx context.Context, uid *uuid.UUID, action, ttype, tid string, meta map[string]any) {
|
||||||
_ = s.rec.Record(ctx, audit.Entry{
|
_ = s.rec.Record(ctx, audit.Entry{
|
||||||
UserID: uid, Action: action, TargetType: ttype, TargetID: tid, Metadata: meta,
|
UserID: uid, Action: action, TargetType: ttype, TargetID: tid, Metadata: meta,
|
||||||
|
|||||||
Reference in New Issue
Block a user