Promote root output.md only when the runner succeeded, protected parents were untouched, and the local target is trusted. Restore errors abort instead of being ignored. Co-authored-by: Cursor <cursoragent@cursor.com>
324 lines
11 KiB
Go
324 lines
11 KiB
Go
package squad
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
)
|
||
|
||
const (
|
||
InputLayoutSnapshot = "snapshot"
|
||
RepairSourceOutputName = "source_output.md"
|
||
RepairTargetName = "output.md"
|
||
GovAuditContainment = "repair_containment_violation"
|
||
GovAuditRestoreFailure = "repair_restore_failure"
|
||
RepairPromoteOK = "ok"
|
||
RepairDiscardRunError = "run_error"
|
||
RepairDiscardContainment = "containment"
|
||
RepairDiscardNoLocal = "missing_local"
|
||
)
|
||
|
||
// ProtectedParentInputs are the only parent files snapshotted for repair
|
||
// containment. Other workspace files are residual risk if the model writes them.
|
||
var ProtectedParentInputs = []string{"output.md", "template.md", "blackboard.json"}
|
||
|
||
// WorkspaceInputSnapshot is a byte-accurate copy of authoritative parent inputs
|
||
// taken before a contained repair step runs.
|
||
type WorkspaceInputSnapshot struct {
|
||
Output []byte
|
||
Template []byte
|
||
Blackboard []byte
|
||
OutputOK bool
|
||
TemplateOK bool
|
||
BlackboardOK bool
|
||
}
|
||
|
||
func digestBytes(b []byte) string {
|
||
sum := sha256.Sum256(b)
|
||
return hex.EncodeToString(sum[:8])
|
||
}
|
||
|
||
func SnapshotWorkspaceInputs(wsDir string) WorkspaceInputSnapshot {
|
||
var snap WorkspaceInputSnapshot
|
||
if b, err := os.ReadFile(filepath.Join(wsDir, "output.md")); err == nil {
|
||
snap.Output, snap.OutputOK = append([]byte(nil), b...), true
|
||
}
|
||
if b, err := os.ReadFile(filepath.Join(wsDir, "template.md")); err == nil {
|
||
snap.Template, snap.TemplateOK = append([]byte(nil), b...), true
|
||
}
|
||
if b, err := os.ReadFile(filepath.Join(wsDir, "blackboard.json")); err == nil {
|
||
snap.Blackboard, snap.BlackboardOK = append([]byte(nil), b...), true
|
||
}
|
||
return snap
|
||
}
|
||
|
||
func restoreProtectedFile(path string, existed bool, data []byte) error {
|
||
_, statErr := os.Lstat(path)
|
||
nowExists := statErr == nil
|
||
if existed {
|
||
if err := writeRegularFile(path, data); err != nil {
|
||
return fmt.Errorf("restore %s: %w", filepath.Base(path), err)
|
||
}
|
||
return nil
|
||
}
|
||
if nowExists {
|
||
if err := os.RemoveAll(path); err != nil {
|
||
return fmt.Errorf("restore: remove created %s: %w", filepath.Base(path), err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// restoreWorkspaceInputsFn is the restore strategy used by EnsureQualityOrRepair.
|
||
// Tests replace it via OverrideRestoreWorkspaceInputsForTest.
|
||
var restoreWorkspaceInputsFn = RestoreWorkspaceInputs
|
||
|
||
// OverrideRestoreWorkspaceInputsForTest swaps the restore strategy and returns a reset func.
|
||
func OverrideRestoreWorkspaceInputsForTest(fn func(string, WorkspaceInputSnapshot) error) func() {
|
||
prev := restoreWorkspaceInputsFn
|
||
if fn == nil {
|
||
restoreWorkspaceInputsFn = RestoreWorkspaceInputs
|
||
} else {
|
||
restoreWorkspaceInputsFn = fn
|
||
}
|
||
return func() { restoreWorkspaceInputsFn = prev }
|
||
}
|
||
|
||
func RestoreWorkspaceInputs(wsDir string, snap WorkspaceInputSnapshot) error {
|
||
if err := restoreProtectedFile(filepath.Join(wsDir, "output.md"), snap.OutputOK, snap.Output); err != nil {
|
||
return err
|
||
}
|
||
if err := restoreProtectedFile(filepath.Join(wsDir, "template.md"), snap.TemplateOK, snap.Template); err != nil {
|
||
return err
|
||
}
|
||
if err := restoreProtectedFile(filepath.Join(wsDir, "blackboard.json"), snap.BlackboardOK, snap.Blackboard); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func parentFileMutated(path string, existed bool, data []byte) bool {
|
||
cur, err := os.ReadFile(path)
|
||
nowExists := err == nil
|
||
if existed != nowExists {
|
||
return true
|
||
}
|
||
if existed && !bytes.Equal(cur, data) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func ParentInputsMutated(wsDir string, snap WorkspaceInputSnapshot) []string {
|
||
var changed []string
|
||
if parentFileMutated(filepath.Join(wsDir, "output.md"), snap.OutputOK, snap.Output) {
|
||
changed = append(changed, "output.md")
|
||
}
|
||
if parentFileMutated(filepath.Join(wsDir, "template.md"), snap.TemplateOK, snap.Template) {
|
||
changed = append(changed, "template.md")
|
||
}
|
||
if parentFileMutated(filepath.Join(wsDir, "blackboard.json"), snap.BlackboardOK, snap.Blackboard) {
|
||
changed = append(changed, "blackboard.json")
|
||
}
|
||
return changed
|
||
}
|
||
|
||
// DecideRepairPromote is the only gate that may overlay root output.md.
|
||
// runErr, any protected-parent mutation, or an untrusted local target each void the round.
|
||
func DecideRepairPromote(runErr error, mutated []string, localOK bool) (promote bool, reason string) {
|
||
if runErr != nil {
|
||
return false, RepairDiscardRunError
|
||
}
|
||
if len(mutated) > 0 {
|
||
return false, RepairDiscardContainment
|
||
}
|
||
if !localOK {
|
||
return false, RepairDiscardNoLocal
|
||
}
|
||
return true, RepairPromoteOK
|
||
}
|
||
|
||
func writeRegularFile(dst string, data []byte) error {
|
||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||
return err
|
||
}
|
||
_ = os.Remove(dst)
|
||
return os.WriteFile(dst, data, 0o644)
|
||
}
|
||
|
||
func copyRegularFile(src, dst string) error {
|
||
data, err := os.ReadFile(src)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return writeRegularFile(dst, data)
|
||
}
|
||
|
||
// LocalNeedSnapshotName maps a parent Need onto a local filename.
|
||
// Root/step output.md must not occupy the target slot (that would fake a deliverable).
|
||
func LocalNeedSnapshotName(need, targetDeliverable string) string {
|
||
base := filepath.Base(filepath.FromSlash(need))
|
||
if base == "" || base == "." {
|
||
return ""
|
||
}
|
||
if targetDeliverable == "" {
|
||
targetDeliverable = RepairTargetName
|
||
}
|
||
if base == targetDeliverable {
|
||
return RepairSourceOutputName
|
||
}
|
||
return base
|
||
}
|
||
|
||
func TaskMarkdownEscapesParent(task string) bool {
|
||
return strings.Contains(task, "../")
|
||
}
|
||
|
||
// TrustedLocalDeliverable returns a non-empty regular file inside subDir.
|
||
// Symlinks and paths that escape the sub-workspace are rejected.
|
||
func TrustedLocalDeliverable(subDir, name string) ([]byte, bool) {
|
||
if name == "" || strings.Contains(name, "..") {
|
||
return nil, false
|
||
}
|
||
p := filepath.Join(subDir, filepath.FromSlash(name))
|
||
info, err := os.Lstat(p)
|
||
if err != nil || !info.Mode().IsRegular() {
|
||
return nil, false
|
||
}
|
||
abs, err := filepath.Abs(p)
|
||
if err != nil {
|
||
return nil, false
|
||
}
|
||
root, err := filepath.Abs(subDir)
|
||
if err != nil {
|
||
return nil, false
|
||
}
|
||
rel, err := filepath.Rel(root, abs)
|
||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||
return nil, false
|
||
}
|
||
data, err := os.ReadFile(p)
|
||
if err != nil || len(bytes.TrimSpace(data)) == 0 {
|
||
return nil, false
|
||
}
|
||
return data, true
|
||
}
|
||
|
||
// SnapshotInputsIntoSubdir copies parent template/blackboard/Needs as regular
|
||
// local files. output.md needs become source_output.md so the runner target stays empty.
|
||
func SnapshotInputsIntoSubdir(wsDir, subDir string, needs []string, targetDeliverable string) error {
|
||
if targetDeliverable == "" {
|
||
targetDeliverable = RepairTargetName
|
||
}
|
||
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||
return err
|
||
}
|
||
if _, err := os.Stat(filepath.Join(wsDir, "template.md")); err == nil {
|
||
if err := copyRegularFile(filepath.Join(wsDir, "template.md"), filepath.Join(subDir, "template.md")); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if _, err := os.Stat(filepath.Join(wsDir, "blackboard.json")); err == nil {
|
||
if err := copyRegularFile(filepath.Join(wsDir, "blackboard.json"), filepath.Join(subDir, "blackboard.json")); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
seen := map[string]struct{}{"template.md": {}, "blackboard.json": {}}
|
||
for _, need := range needs {
|
||
local := LocalNeedSnapshotName(need, targetDeliverable)
|
||
if local == "" || local == targetDeliverable {
|
||
continue
|
||
}
|
||
if _, dup := seen[local]; dup {
|
||
continue
|
||
}
|
||
src := filepath.Join(wsDir, filepath.FromSlash(need))
|
||
if _, err := os.Stat(src); err != nil {
|
||
continue
|
||
}
|
||
if err := copyRegularFile(src, filepath.Join(subDir, local)); err != nil {
|
||
return err
|
||
}
|
||
seen[local] = struct{}{}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func BuildContainedTaskMarkdown(step StepSpec, deliverableName string) string {
|
||
if deliverableName == "" {
|
||
deliverableName = RepairTargetName
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("# 任务执行指令: %s\n\n", step.Title))
|
||
b.WriteString(fmt.Sprintf("- **步骤 ID**: `%s`\n", step.ID))
|
||
b.WriteString(fmt.Sprintf("- **分配角色**: `%s`\n", step.Role))
|
||
b.WriteString(fmt.Sprintf("- **执行模式**: `%s`\n", step.Mode))
|
||
b.WriteString(fmt.Sprintf("- **交付产物**: `%s`\n\n", deliverableName))
|
||
b.WriteString("## 环境与输入导航\n")
|
||
b.WriteString("只阅读当前目录下的本地快照,禁止使用 `..` 或写入上级目录:\n")
|
||
b.WriteString("- `template.md`:交付模板\n")
|
||
b.WriteString("- `blackboard.json`:黑板事实(若存在)\n")
|
||
if len(step.Needs) > 0 {
|
||
b.WriteString("\n## 前序依赖产出\n")
|
||
for _, n := range step.Needs {
|
||
local := LocalNeedSnapshotName(n, deliverableName)
|
||
if local == "" {
|
||
continue
|
||
}
|
||
b.WriteString(fmt.Sprintf("- `%s`(由 `%s` 快照)\n", local, filepath.ToSlash(n)))
|
||
}
|
||
b.WriteString("\n")
|
||
}
|
||
if len(step.Accept) > 0 {
|
||
b.WriteString("## 验收标准(Checklist)\n")
|
||
for _, a := range step.Accept {
|
||
b.WriteString(fmt.Sprintf("- [ ] %s\n", a))
|
||
}
|
||
b.WriteString("\n")
|
||
}
|
||
b.WriteString("## 交付契约与写作纪律\n")
|
||
b.WriteString(fmt.Sprintf("请在当前目录下生成完整交付物 `%s`。不要引用或改写上级文件。\n", deliverableName))
|
||
return b.String()
|
||
}
|
||
|
||
func WriteRepairTaskMarkdown(subDir string, attempt int, report *QualityReport, stallHinted bool) error {
|
||
var b strings.Builder
|
||
b.WriteString(fmt.Sprintf("# 质量门禁修复任务 (第 %d 次修正)\n\n", attempt))
|
||
b.WriteString("前序提交未通过自动化质量门禁,检测到的问题如下,请针对性修复:\n")
|
||
if report != nil {
|
||
for _, v := range report.Violations {
|
||
b.WriteString(fmt.Sprintf("- ❌ %s\n", sanitizeViolationForAgent(v)))
|
||
}
|
||
}
|
||
if stallHinted {
|
||
b.WriteString("\n⚠️ 治理警告:最近两轮修复产物相似度过高,被判定为无进度空转。本轮必须针对上述违规点逐条重写对应章节,产出与之前实质不同的内容,否则任务将被强制终止。\n")
|
||
}
|
||
b.WriteString("\n只阅读当前目录本地快照,禁止使用上级路径:\n")
|
||
b.WriteString("- `source_output.md`:当前未通过门禁的初稿\n")
|
||
b.WriteString("- `template.md`:交付模板\n")
|
||
b.WriteString("- `blackboard.json`:黑板事实(若存在)\n")
|
||
b.WriteString("\n只在当前目录写入完整修复初稿 `output.md`。\n")
|
||
text := b.String()
|
||
if TaskMarkdownEscapesParent(text) {
|
||
return fmt.Errorf("repair TASK.md must not reference parent paths")
|
||
}
|
||
return writeRegularFile(filepath.Join(subDir, "TASK.md"), []byte(text))
|
||
}
|
||
|
||
func recordContainmentViolation(wsDir string, attempt int, mutated []string, localOK bool, snap WorkspaceInputSnapshot) {
|
||
digest := ""
|
||
if snap.OutputOK {
|
||
digest = digestBytes(snap.Output)
|
||
}
|
||
appendGovAudit(wsDir, GovAuditContainment, map[string]string{
|
||
"attempt": fmt.Sprintf("%d", attempt),
|
||
"mutated": strings.Join(mutated, ","),
|
||
"local_target": fmt.Sprintf("%v", localOK),
|
||
"output_digest": digest,
|
||
})
|
||
}
|