Go 210 lines
package cockpit
import (
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/ajhahnde/eeco/internal/config"
)
// GenerateAll emits an aggregate target's single shared artifact for the whole
// playbook set, reversibly. The uniform safety gate runs on EVERY playbook in
// the set first: a forbidden write-git verb in any one refuses the whole emit
// (nothing written, no ledger churn), naming the offending playbook — advisory
// targets do not relax the invariant. The artifact dir is cfg.UserDir (never
// pruned), so off can never remove the private tree. Re-emitting unchanged
// bytes is a byte-idempotent no-op.
func GenerateAll(cfg *config.Config, set []Playbook, target string) (GenerateResult, error) {
r, ok := rendererFor(target)
if !ok {
return GenerateResult{}, unknownTargetErr(target)
}
agg, ok := isAggregate(r)
if !ok {
return GenerateResult{}, fmt.Errorf("target %q is per-playbook; use Generate", target)
}
for _, pb := range set {
if hits := ScanAllowlistForWriteGitVerbs(composeAllowedTools(pb), pb.Intent.forbiddenVerbs()); len(hits) > 0 {
return GenerateResult{}, fmt.Errorf(
"refusing to emit %s: playbook %q has forbidden write-git verb(s) in allowlist: %s",
target, pb.Name, strings.Join(hits, ", "))
}
}
content, err := agg.RenderAll(set)
if err != nil {
return GenerateResult{}, err
}
dst, err := userArtifactPath(cfg, agg.AggRelPath())
if err != nil {
return GenerateResult{}, err
}
newSHA := sha256hex(content)
l, err := loadLedger(cfg)
if err != nil {
return GenerateResult{}, err
}
priorIdx := l.findAgg(target)
hasPrior := priorIdx >= 0 && l.Records[priorIdx].Installed
var prior record
if priorIdx >= 0 {
prior = l.Records[priorIdx]
}
if hasPrior && prior.SHA256 == newSHA {
if onDisk, rerr := os.ReadFile(dst); rerr == nil && sha256hex(onDisk) == newSHA {
return GenerateResult{Path: dst, Action: "already current", Fidelity: fidelityOf(r)}, nil
}
}
backup := prior.Backup
if !hasPrior {
// The aggregate dir is cfg.UserDir, which always exists and is never
// pruned, so only a pre-existing foreign file needs backing up.
if existing, rerr := os.ReadFile(dst); rerr == nil {
bp, berr := backupExisting(cfg, target, "_all", existing)
if berr != nil {
return GenerateResult{}, berr
}
backup = bp
} else if !errors.Is(rerr, os.ErrNotExist) {
return GenerateResult{}, fmt.Errorf("inspect %s: %w", dst, rerr)
}
}
if err := writeFileAtomic(dst, content, 0o644); err != nil {
return GenerateResult{}, err
}
l.upsertAgg(record{
Installed: true,
Target: target,
Playbook: "", // aggregate: keyed on target alone
Path: dst,
SHA256: newSHA,
Backup: backup,
Created: false, // dir is cfg.UserDir — never created, never pruned
At: time.Now().UTC().Format(time.RFC3339),
})
if err := saveLedger(cfg, l); err != nil {
return GenerateResult{}, err
}
action := "generated"
switch {
case hasPrior:
action = "regenerated"
case backup != "":
action = "updated"
}
return GenerateResult{Path: dst, Action: action, Backup: backup, Fidelity: fidelityOf(r)}, nil
}
// VerifyAll checks an aggregate target's on-disk artifact against the bytes
// freshly rendered for set, then runs the self-consistency safety check on the
// on-disk bytes (S4) — the advisory analog of the enforced allowlist scan,
// since these targets have no answer key. It never mutates anything.
func VerifyAll(cfg *config.Config, set []Playbook, target string) (VerifyResult, error) {
r, ok := rendererFor(target)
if !ok {
return VerifyResult{}, unknownTargetErr(target)
}
agg, ok := isAggregate(r)
if !ok {
return VerifyResult{}, fmt.Errorf("target %q is per-playbook; use Verify", target)
}
desired, err := agg.RenderAll(set)
if err != nil {
return VerifyResult{}, err
}
dst, err := userArtifactPath(cfg, agg.AggRelPath())
if err != nil {
return VerifyResult{}, err
}
onDisk, rerr := os.ReadFile(dst)
if errors.Is(rerr, os.ErrNotExist) {
return VerifyResult{Clean: false, Detail: fmt.Sprintf("%s: not emitted (run `eeco cockpit generate`)", target)}, nil
}
if rerr != nil {
return VerifyResult{}, fmt.Errorf("read %s: %w", dst, rerr)
}
if sha256hex(onDisk) != sha256hex(desired) {
return VerifyResult{Clean: false, Detail: fmt.Sprintf(
"%s: drifted (hand-edited; run `eeco cockpit generate` to restore)", target)}, nil
}
sc := checkSelfConsistencyBytes(onDisk, set)
if !sc.OK {
return VerifyResult{Clean: false, Detail: fmt.Sprintf(
"%s: self-consistency FAILED: %s", target, strings.Join(sc.Notes, "; "))}, nil
}
return VerifyResult{Clean: true, Detail: fmt.Sprintf("%s: clean (advisory — not harness-enforced)", target)}, nil
}
// OffAll removes an aggregate target's shared artifact, sha-gated and
// reversible. A hand-edited file (on-disk sha != recorded sha) is left
// untouched. It restores a backed-up foreign file or removes eeco's artifact,
// then clears the target-only record. It NEVER prunes a directory, so it can
// never remove cfg.UserDir (the private tree).
func OffAll(cfg *config.Config, target string) (OffResult, error) {
r, ok := rendererFor(target)
if !ok {
return OffResult{}, unknownTargetErr(target)
}
agg, ok := isAggregate(r)
if !ok {
return OffResult{}, fmt.Errorf("target %q is per-playbook; use Off", target)
}
dst, err := userArtifactPath(cfg, agg.AggRelPath())
if err != nil {
return OffResult{}, err
}
l, err := loadLedger(cfg)
if err != nil {
return OffResult{}, err
}
i := l.findAgg(target)
if i < 0 || !l.Records[i].Installed {
return OffResult{Changed: false, Message: fmt.Sprintf("%s: not emitted", target)}, nil
}
rec := l.Records[i]
onDisk, rerr := os.ReadFile(dst)
if errors.Is(rerr, os.ErrNotExist) {
l.clearAgg(target)
if err := saveLedger(cfg, l); err != nil {
return OffResult{}, err
}
return OffResult{Changed: true, Message: fmt.Sprintf("%s: already removed; ledger cleared", target)}, nil
}
if rerr != nil {
return OffResult{}, fmt.Errorf("read %s: %w", dst, rerr)
}
if sha256hex(onDisk) != rec.SHA256 {
return OffResult{Changed: false, Message: fmt.Sprintf("%s: edited since generate — left untouched", target)}, nil
}
if rec.Backup != "" {
// Restore the pre-eeco file by atomically replacing eeco's artifact, so
// the path is never absent mid-restore; an unreadable backup falls back
// to removing the artifact.
if bb, berr := os.ReadFile(rec.Backup); berr == nil {
if werr := writeFileAtomic(dst, bb, 0o644); werr != nil {
return OffResult{}, werr
}
} else if remErr := os.Remove(dst); remErr != nil {
return OffResult{}, fmt.Errorf("remove %s: %w", dst, remErr)
}
} else if err := os.Remove(dst); err != nil {
return OffResult{}, fmt.Errorf("remove %s: %w", dst, err)
}
l.clearAgg(target)
if err := saveLedger(cfg, l); err != nil {
return OffResult{}, err
}
return OffResult{Changed: true, Message: fmt.Sprintf("%s: removed (reversed)", target)}, nil
}