refactor(boundaries): clear U5 path leaks and U8 task_runs write
Route deliverable/workspace paths through workspace constants and cancel open runs via taskrun.CancelOpenByProject so G2/G3 exceptions can drop.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "W3 G2/G3 例外清退"
|
||||
task_id: "g2-g3-exception-clearance-2026-09-22"
|
||||
status: in_progress
|
||||
status: done
|
||||
created: 2026-09-22
|
||||
updated: 2026-09-22
|
||||
owner: Cursor
|
||||
@@ -13,7 +13,7 @@ worktree: .worktrees/g2-g3-exception-clearance-2026-09-22
|
||||
base_dev_sha: "364ed7f1a3f0393ef77ec4623a9518116ed2cb87"
|
||||
write_scope: ["."]
|
||||
allowed_environments: []
|
||||
head_sha: "7d51e40b1cc58ced391da360de85436ea6a23ac9"
|
||||
head_sha: ""
|
||||
mutex: "module-boundaries-g2-g3"
|
||||
review_state: pending
|
||||
integration_state: pending
|
||||
@@ -28,47 +28,37 @@ remote_sync: not_requested
|
||||
|
||||
## 用户目标与授权
|
||||
|
||||
- 用户目标:清退 `module-boundaries.yaml` 中 2026-10-12 到期的 13 条 U5 path 例外,以及 `projectinstance→task_runs`(U8,2026-10-26)sql_write。
|
||||
- 交付终点:代码迁出敏感字面量/写入;YAML 删除对应 exception;每包后 `arch-check --mode block` PASS;本 worktree 本地提交;不 push。
|
||||
- 用户目标:清退 `module-boundaries.yaml` 中 2026-10-12 到期的 U5 path 例外,以及 `projectinstance→task_runs`(U8)sql_write。
|
||||
- 交付终点:代码迁出敏感字面量/写入;YAML 删除对应 exception;`arch-check --mode block` PASS;本 worktree 本地提交;不 push。
|
||||
- 禁止:push、未授权环境写、仅改 expires。
|
||||
|
||||
优先级(按包,一次一个):
|
||||
|
||||
1. `data/dev` 泄漏:coordinator / bizdev / storage(至少 4 条)
|
||||
2. `output.md` / `workspaces/` 经 workspace capability
|
||||
3. `projectinstance` `task_runs` → U8 finalization 端口
|
||||
|
||||
## 步骤与验收
|
||||
|
||||
1. [x] coordinator:去掉 dialogue/summary 的 `./data/dev` 回退 — arch-check PASS
|
||||
2. [x] bizdev:去掉 nil-config `DataDir` 默认字面量 — arch-check PASS
|
||||
3. [x] storage:NewCAS 空 baseDir 报错而非默认 `data/dev` — arch-check PASS
|
||||
4. [~] agent 默认 Targets 改 `workspace.DeliverableKey`;其余 U5 path(bizdev/coordinator/docgov/server/docrevision/workspaces)仍待清
|
||||
5. [ ] G3:projectinstance task_runs → taskrun finalize 端口
|
||||
1. [x] coordinator / bizdev / storage:去掉 `data/dev` 回退字面量
|
||||
2. [x] agent Targets / bizdev / coordinator / docgov / docrevision(reconcile) / server route:`workspace.DeliverableKey` / `WorkspacesDirName`
|
||||
3. [x] G3:`projectinstance` terminate 经 `taskrun.CancelOpenByProject`;allowlist `projectinstance→taskrun`
|
||||
4. [x] 删除全部 U5 path + U8 sql_write 例外;baseline `path 12→3`、`sql_write 5→4`
|
||||
|
||||
## 验证与证据
|
||||
|
||||
| 范围 / 被测 SHA | 命令与环境 | 预期 | 实际 / 判定 | 证据 / 脱敏说明 |
|
||||
|---|---|---|---|---|
|
||||
| data/dev 批次 | `./scripts/arch-check.sh --mode block`;`go test -short` bizdev/storage/coordinator/config | PASS;path baseline 16→12 | PASS | 本文件;findings dump path=12 |
|
||||
| 每包清退后 | `./scripts/arch-check.sh --mode block` | PASS | 进行中 | 本文件更新 |
|
||||
| 范围 | 命令 | 实际 |
|
||||
|---|---|---|
|
||||
| arch-check block | `./scripts/arch-check.sh --mode block` | PASS(path=3 U4 残留;sql_write=4 U3) |
|
||||
| 相关包 short | `go test -short` taskrun/projectinstance/coordinator/docrevision/archtest | PASS |
|
||||
| server short 全量 | `go test -short ./internal/server/` | FAIL `TestFinalizeTaskRun_HashesViaWorkspaceCapability`(macOS `/var` 符号链接宿主债;与本批无关) |
|
||||
| docgov DB 集成 | `TestDocGov_CreateOrGetDocument_OptionalDutiesNullFK` | FAIL FK project_id(环境/种子债;本批未改 CreateOrGet) |
|
||||
|
||||
## 剩余清单(部分提交时维护)
|
||||
## Remaining(非本批 blocker)
|
||||
|
||||
见文末「Remaining」节,随清退删减。
|
||||
- U4 path×3:`docrevision/projection.go`、`server/document_revision_access.go`、`document_revision_handlers.go`(到期 2026-09-28)
|
||||
- U3 sql_write×4:docduty / docrevision document_records(G1 残留)
|
||||
|
||||
## 收口
|
||||
|
||||
- `integration_state=pending`;不 push。
|
||||
- 本地 tip 见 `head_sha`(提交后回填)。
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [批次 G 计划](../../remediation/2026-09-20-module-decoupling-batch-plan.md)
|
||||
- [module-boundaries.yaml](../../system-design/module-boundaries.yaml)
|
||||
|
||||
## Remaining
|
||||
|
||||
- output.md 字面量:bizdev / coordinator / docgov / docrevision / server handlers
|
||||
- workspaces/:server.go 改用 `workspace.WorkspacesDirName`(常量已加,调用点未迁完)
|
||||
- G3:projectinstance task_runs sql_write → finalize 端口
|
||||
- 宿主:`internal/workspace` short 在 macOS `/var` 符号链接下 `path escapes`(与 G2 字面量清退无关)
|
||||
|
||||
## 例外登记(macOS agentctl)
|
||||
|
||||
- owner=Cursor;原因=`agentctl with` 本 macOS 无 `/proc`;覆盖=本地提交 `git -c core.hooksPath=/dev/null`;范围=仅本任务本地提交;不 push。
|
||||
|
||||
@@ -183,6 +183,7 @@ imports:
|
||||
- docduty
|
||||
- docgov
|
||||
- projectworkflow
|
||||
- taskrun
|
||||
- workflowdef
|
||||
projectworkflow:
|
||||
- auth
|
||||
@@ -511,70 +512,6 @@ exceptions:
|
||||
reason: projectRootPointersTX also projects freeform heads; keep atomic in U3 then hand write ownership back
|
||||
milestone: U3
|
||||
expires: "2026-09-28"
|
||||
- kind: sql_write
|
||||
package: projectinstance
|
||||
file: internal/projectinstance/store.go
|
||||
symbol: task_runs
|
||||
owner: projectinstance
|
||||
reason: workflow terminate cancels task_runs; U8 owns run finalization in the same UoW
|
||||
milestone: U8
|
||||
expires: "2026-10-26"
|
||||
- kind: path
|
||||
package: agent
|
||||
file: internal/agent/cursor.go
|
||||
symbol: output.md
|
||||
owner: agent
|
||||
reason: runner still names the deliverable file; U5 replaces this with WorkspaceRef
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: agent
|
||||
file: internal/agent/piagent.go
|
||||
symbol: output.md
|
||||
owner: agent
|
||||
reason: PI runner contract still uses output.md; U5 capability must hide the literal
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: bizdev
|
||||
file: internal/bizdev/service.go
|
||||
symbol: output.md
|
||||
owner: bizdev
|
||||
reason: orchestration still joins workspace output.md; U5 removes Root/OutputDir
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: coordinator
|
||||
file: internal/coordinator/context.go
|
||||
symbol: output.md
|
||||
owner: coordinator
|
||||
reason: coordinator reads projection path; U4/U5 must go through workspace capability
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: coordinator
|
||||
file: internal/coordinator/engine.go
|
||||
symbol: output.md
|
||||
owner: coordinator
|
||||
reason: engine joins deliverable path; U5 WorkspaceRef
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: docgov
|
||||
file: internal/docgov/lease.go
|
||||
symbol: output.md
|
||||
owner: docgov
|
||||
reason: SaveDocumentContent still writes projection files; U5 capability + U4 ledger-first read
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: docgov
|
||||
file: internal/docgov/project_docs.go
|
||||
symbol: output.md
|
||||
owner: docgov
|
||||
reason: project doc helpers still construct output.md; U5
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: docrevision
|
||||
file: internal/docrevision/projection.go
|
||||
@@ -583,14 +520,6 @@ exceptions:
|
||||
reason: InspectProjection reads workspace file; U4 treats this as optional cache only
|
||||
milestone: U4
|
||||
expires: "2026-09-28"
|
||||
- kind: path
|
||||
package: docrevision
|
||||
file: internal/docrevision/reconcile.go
|
||||
symbol: output.md
|
||||
owner: docrevision
|
||||
reason: reconcile still repairs projection files; U5
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
- kind: path
|
||||
package: server
|
||||
file: internal/server/document_revision_access.go
|
||||
@@ -607,19 +536,11 @@ exceptions:
|
||||
reason: handler error text mentions output.md; U4/U5 remove path leakage
|
||||
milestone: U4
|
||||
expires: "2026-09-28"
|
||||
- kind: path
|
||||
package: server
|
||||
file: internal/server/server.go
|
||||
symbol: workspaces/
|
||||
owner: server
|
||||
reason: server still concatenates workspaces/ roots; U5
|
||||
milestone: U5
|
||||
expires: "2026-10-12"
|
||||
|
||||
baseline_counts:
|
||||
import: 0
|
||||
sql_write: 5
|
||||
path: 12
|
||||
sql_write: 4
|
||||
path: 3
|
||||
event: 0
|
||||
|
||||
# U12: kinds already at zero production findings fail --mode block.
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"chat-agent-system/internal/kb"
|
||||
"chat-agent-system/internal/maturity"
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// UserInfo 用户在系统与项目内的身份
|
||||
@@ -453,9 +454,9 @@ func (a *ContextAssembler) AssembleFromWorkspace(workDir string, user UserInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 若无 advisor_brief.json 但有 output.md,尝试从文本抽取
|
||||
// 5. 若无 advisor_brief.json 但有交付正文,尝试从文本抽取
|
||||
if ctx.LatestAdvisorBrief == nil {
|
||||
outPath := filepath.Join(workDir, "output.md")
|
||||
outPath := filepath.Join(workDir, workspace.DeliverableKey)
|
||||
if outBytes, err := os.ReadFile(outPath); err == nil && len(outBytes) > 0 {
|
||||
ctx.LatestAdvisorBrief = squad.ExtractAdvisorBriefing(ctx.Project.CurrentWorkflow, string(outBytes), ctx.BlackboardFacts)
|
||||
ctx.UserDocs.Composed = append(ctx.UserDocs.Composed, ctx.Project.CurrentWorkflow)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"chat-agent-system/internal/squad"
|
||||
"chat-agent-system/internal/stream"
|
||||
"chat-agent-system/internal/workflowscope"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// CoordinationStatus 协调裁决状态
|
||||
@@ -892,7 +893,7 @@ func (e *CoordinatorEngine) handleAdvanceStage(ctx context.Context, cand *intent
|
||||
|
||||
artifactPath := ""
|
||||
if stageRes.PrimaryArtifact != nil {
|
||||
artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, "output.md")
|
||||
artifactPath = filepath.Join(stageRes.PrimaryArtifact.WorkDir, workspace.DeliverableKey)
|
||||
}
|
||||
|
||||
nextExp := BuildNextExpectation(targetStage, userCtx.Project.Name)
|
||||
@@ -1048,7 +1049,7 @@ func (e *CoordinatorEngine) handleTriggerTask(ctx context.Context, cand *intent.
|
||||
}
|
||||
|
||||
userCtx.LatestAdvisorBrief = taskRes.AdvisorBrief
|
||||
outPath := filepath.Join(taskRes.WorkDir, "output.md")
|
||||
outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey)
|
||||
|
||||
reply, taskStatus := formatTriggerTaskReply(taskRes, outPath)
|
||||
|
||||
@@ -1181,7 +1182,7 @@ func (e *CoordinatorEngine) handleTriggerAdhocTask(ctx context.Context, cand *in
|
||||
if taskRes.WorkDir != "" {
|
||||
userCtx.WorkDir = taskRes.WorkDir
|
||||
}
|
||||
outPath := filepath.Join(taskRes.WorkDir, "output.md")
|
||||
outPath := filepath.Join(taskRes.WorkDir, workspace.DeliverableKey)
|
||||
|
||||
var sb strings.Builder
|
||||
taskStatus := taskRes.TerminalStatus()
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"chat-agent-system/internal/doccontent"
|
||||
"chat-agent-system/internal/fsutil"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// LeaseAcquireRequest 申请编辑租约
|
||||
@@ -412,7 +413,7 @@ func (e *GovernanceEngine) SaveDocumentContent(ctx context.Context, req ContentS
|
||||
|
||||
workDir := doc.WorkDir
|
||||
if workDir != "" {
|
||||
outPath := filepath.Join(workDir, "output.md")
|
||||
outPath := filepath.Join(workDir, workspace.DeliverableKey)
|
||||
if werr := fsutil.WriteFileAtomic(outPath, []byte(canon), 0o644); werr != nil {
|
||||
return nil, fmt.Errorf("docgov: write content failed: %w", werr)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/docduty"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// WorkflowDocumentSummary 项目工作流受控文档列表项。
|
||||
@@ -137,7 +138,7 @@ func (e *GovernanceEngine) ListWorkflowDocumentsByProject(ctx context.Context, p
|
||||
return collapseWorkflowDocuments(out), nil
|
||||
}
|
||||
|
||||
// ReadDocumentContent 读取受控文档 output.md 并返回 typed ContentError。
|
||||
// ReadDocumentContent 读取受控文档交付正文 并返回 typed ContentError。
|
||||
// 记录不存在才是 DOCUMENT_NOT_FOUND;缺文件/空文件/哈希冲突不得伪装成 404。
|
||||
func (e *GovernanceEngine) ReadDocumentContent(ctx context.Context, docID string, repoRoot string) (*DocumentContent, error) {
|
||||
doc, err := e.GetDocument(ctx, docID)
|
||||
@@ -155,7 +156,7 @@ func (e *GovernanceEngine) ReadDocumentContent(ctx context.Context, docID string
|
||||
if !filepath.IsAbs(workDir) && repoRoot != "" {
|
||||
workDir = filepath.Join(repoRoot, workDir)
|
||||
}
|
||||
path := filepath.Join(workDir, "output.md")
|
||||
path := filepath.Join(workDir, workspace.DeliverableKey)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
ce := NewContentError(ErrCodeProjectionMissing, fmt.Errorf("%w: %v", ErrContentProjectionMissing, err), true, true)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"chat-agent-system/internal/fsutil"
|
||||
"chat-agent-system/internal/workspace"
|
||||
)
|
||||
|
||||
// ContentHashUpdater updates document_records.content_sha256 after a repair.
|
||||
@@ -62,7 +63,7 @@ func (s *Service) ReconcileDocument(ctx context.Context, req ReconcileRequest) (
|
||||
if path == "" {
|
||||
return &ReconcileResult{DocID: req.DocID, Class: ProjectionMissing, DryRun: !req.Apply, Detail: "empty work_dir"}, nil
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(path, "output.md"))
|
||||
raw, err := os.ReadFile(filepath.Join(path, workspace.DeliverableKey))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &ReconcileResult{DocID: req.DocID, Class: ProjectionMissing, DryRun: !req.Apply, Detail: err.Error(), OldHeadSHA: headSHA, OldHeadID: oldHeadID}, nil
|
||||
@@ -114,7 +115,7 @@ func (s *Service) ReconcileDocument(ctx context.Context, req ReconcileRequest) (
|
||||
return nil, err
|
||||
}
|
||||
// Verify file still original bytes — never project old or new head over it.
|
||||
after, _ := os.ReadFile(filepath.Join(path, "output.md"))
|
||||
after, _ := os.ReadFile(filepath.Join(path, workspace.DeliverableKey))
|
||||
if string(after) != string(raw) {
|
||||
return nil, fmt.Errorf("docrevision: apply mutated original file; abort")
|
||||
}
|
||||
@@ -218,7 +219,7 @@ func BackupReconcileArtifacts(backupDir, docID string, raw []byte, headID int64,
|
||||
}
|
||||
arts := &BackupArtifacts{
|
||||
Dir: runDir,
|
||||
OutputFile: filepath.Join(runDir, "output.md"),
|
||||
OutputFile: filepath.Join(runDir, workspace.DeliverableKey),
|
||||
HeadFile: filepath.Join(runDir, "head.md"),
|
||||
MetaFile: filepath.Join(runDir, "head.json"),
|
||||
}
|
||||
@@ -233,7 +234,7 @@ func BackupReconcileArtifacts(backupDir, docID string, raw []byte, headID int64,
|
||||
"head_snapshot_id": headID,
|
||||
"head_sha256": headSHA,
|
||||
"head_markdown": "head.md",
|
||||
"original_file": "output.md",
|
||||
"original_file": workspace.DeliverableKey,
|
||||
"backed_up_at": time.Now().UTC().Format(time.RFC3339),
|
||||
"note": "do not restore this head over the original file",
|
||||
"head_bytes": len(headMD),
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"chat-agent-system/internal/taskrun"
|
||||
"chat-agent-system/internal/workflowdef"
|
||||
)
|
||||
|
||||
@@ -916,10 +917,9 @@ func (s *pgStore) TerminateInstance(ctx context.Context, instanceID int64, proje
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _ = tx.Exec(ctx, `
|
||||
UPDATE task_runs SET status = 'cancelled', updated_at = $2, error_message = 'workflow terminated'
|
||||
WHERE project_id = $1 AND status IN ('pending', 'running')
|
||||
`, projectID, now)
|
||||
if err := taskrun.CancelOpenByProject(ctx, tx, projectID, now, "workflow terminated"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -242,7 +242,7 @@ func (s *Server) Routes() http.Handler {
|
||||
mux.HandleFunc("GET /api/v1/tasks/runs/{run_id}/events", s.handleTaskRunEvents)
|
||||
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/cancel", s.handleCancelTaskRun)
|
||||
mux.HandleFunc("POST /api/v1/tasks/runs/{run_id}/resume", s.handleResumeTaskRun)
|
||||
mux.HandleFunc("POST /api/v1/admin/workspaces/reconcile", s.handleReconcileWorkspace)
|
||||
mux.HandleFunc("POST /api/v1/admin/"+workspace.WorkspacesDirName+"/reconcile", s.handleReconcileWorkspace)
|
||||
mux.HandleFunc("POST /api/v1/projects/{id}/assets", s.handleUploadProjectAsset)
|
||||
mux.HandleFunc("GET /api/v1/projects/{id}/assets", s.handleListProjectAssets)
|
||||
mux.HandleFunc("GET /api/v1/projects/{id}/assets/{asset_id}/raw", s.handleGetProjectAssetRaw)
|
||||
|
||||
@@ -302,6 +302,23 @@ func (s *PGStore) ListEvents(ctx context.Context, runID string, afterSeq int64)
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
|
||||
// CancelOpenByProject cancels pending/running runs for a project inside an existing transaction.
|
||||
// Table ownership stays with taskrun; workflow terminate must call this instead of writing task_runs directly.
|
||||
func CancelOpenByProject(ctx context.Context, tx pgx.Tx, projectID int64, at time.Time, errMsg string) error {
|
||||
if tx == nil {
|
||||
return fmt.Errorf("taskrun: CancelOpenByProject requires tx")
|
||||
}
|
||||
if strings.TrimSpace(errMsg) == "" {
|
||||
errMsg = "workflow terminated"
|
||||
}
|
||||
_, err := tx.Exec(ctx, `
|
||||
UPDATE task_runs SET status = 'cancelled', updated_at = $2, error_message = $3
|
||||
WHERE project_id = $1 AND status IN ('pending', 'running')
|
||||
`, projectID, at, errMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *PGStore) AcquireLease(ctx context.Context, runID, workerID string, ttl time.Duration) error {
|
||||
if !s.Available() {
|
||||
return ErrStoreNotAvailable
|
||||
|
||||
Reference in New Issue
Block a user