iop/apps/agent/internal/taskloop/parity.go

341 lines
12 KiB
Go

package taskloop
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/yaml.v3"
)
const paritySchemaVersion = 1
//go:embed testdata/parity.yaml
var embeddedParityManifest []byte
// ParityManifest records the S13 disposition of each retained reference
// behavior. It is intentionally evidence-only: runtime behavior remains in
// the shared agenttask and agentpolicy packages.
type ParityManifest struct {
SchemaVersion int `yaml:"schema_version"`
ReferenceRoot string `yaml:"reference_root"`
DeletionGate string `yaml:"deletion_gate"`
Behaviors []ParityBehavior `yaml:"behaviors"`
Disposal ParityDisposal `yaml:"disposal"`
}
type ParityBehavior struct {
ID string `yaml:"id"`
ReferenceSources []string `yaml:"reference_sources"`
Disposition string `yaml:"disposition"`
GoOwner string `yaml:"go_owner"`
GoSource []string `yaml:"go_source"`
GoTest []string `yaml:"go_test"`
Invariants string `yaml:"invariants"`
ProductionCaller string `yaml:"production_caller"`
}
type ParityDisposal struct {
State string `yaml:"state"`
Procedure string `yaml:"procedure"`
ReferenceFiles []ParityReferenceFile `yaml:"reference_files"`
}
type ParityReferenceFile struct {
Path string `yaml:"path"`
SHA256 string `yaml:"sha256"`
Disposition string `yaml:"disposition"`
CoveredBy []string `yaml:"covered_by"`
}
// LoadEmbeddedParityManifest returns the repository-owned S13 manifest.
func LoadEmbeddedParityManifest() (ParityManifest, error) {
return ParseParityManifest(embeddedParityManifest)
}
// ValidateEmbeddedParityManifest validates the checked-in manifest against the
// supplied repository root and returns its stable operator report.
func ValidateEmbeddedParityManifest(root string) (string, error) {
manifest, err := LoadEmbeddedParityManifest()
if err != nil {
return "", err
}
if err := ValidateParityManifest(root, manifest); err != nil {
return "", err
}
return FormatParityReport(manifest), nil
}
// RepositoryRoot walks upward from start until it finds the module root.
func RepositoryRoot(start string) (string, error) {
directory, err := filepath.Abs(start)
if err != nil {
return "", err
}
for {
if info, err := os.Stat(filepath.Join(directory, "go.mod")); err == nil && !info.IsDir() {
return directory, nil
}
parent := filepath.Dir(directory)
if parent == directory {
return "", fmt.Errorf("repository root not found from %q", start)
}
directory = parent
}
}
func ParseParityManifest(data []byte) (ParityManifest, error) {
var manifest ParityManifest
if err := yaml.Unmarshal(data, &manifest); err != nil {
return ParityManifest{}, fmt.Errorf("parity manifest: decode: %w", err)
}
return manifest, nil
}
// ValidateParityManifest proves that the inventory is complete in retained
// mode, or completely absent in disposed mode. The latter keeps the recorded
// checksums as historical pre-deletion evidence without requiring deleted
// files to exist.
func ValidateParityManifest(root string, manifest ParityManifest) error {
if manifest.SchemaVersion != paritySchemaVersion {
return fmt.Errorf("parity manifest: unsupported schema_version %d", manifest.SchemaVersion)
}
if strings.TrimSpace(manifest.ReferenceRoot) == "" || strings.TrimSpace(manifest.DeletionGate) == "" {
return fmt.Errorf("parity manifest: reference_root and deletion_gate are required")
}
if len(manifest.Behaviors) == 0 {
return fmt.Errorf("parity manifest: behaviors are required")
}
behaviorIDs := make(map[string]struct{}, len(manifest.Behaviors))
behaviorSources := make(map[string]struct{})
for _, behavior := range manifest.Behaviors {
if strings.TrimSpace(behavior.ID) == "" {
return fmt.Errorf("parity manifest: unclassified behavior")
}
if _, exists := behaviorIDs[behavior.ID]; exists {
return fmt.Errorf("parity manifest: duplicate behavior %q", behavior.ID)
}
behaviorIDs[behavior.ID] = struct{}{}
if !allowedDisposition(behavior.Disposition) {
return fmt.Errorf("parity manifest: behavior %q has unclassified disposition %q", behavior.ID, behavior.Disposition)
}
if strings.TrimSpace(behavior.GoOwner) == "" || strings.TrimSpace(behavior.Invariants) == "" || behavior.ProductionCaller != "go-only" {
return fmt.Errorf("parity manifest: behavior %q lacks Go ownership evidence", behavior.ID)
}
if len(behavior.GoSource) == 0 || len(behavior.GoTest) == 0 {
return fmt.Errorf("parity manifest: behavior %q lacks concrete Go evidence", behavior.ID)
}
for _, source := range behavior.GoSource {
if err := validateLiteralExistingPath(root, source); err != nil {
return fmt.Errorf("parity manifest: behavior %q: %w", behavior.ID, err)
}
}
for _, source := range behavior.ReferenceSources {
if err := validateLiteralPath(source); err != nil {
return fmt.Errorf("parity manifest: behavior %q: %w", behavior.ID, err)
}
behaviorSources[source] = struct{}{}
}
for _, testName := range behavior.GoTest {
if strings.TrimSpace(testName) == "" {
return fmt.Errorf("parity manifest: behavior %q has empty Go test", behavior.ID)
}
}
}
if strings.TrimSpace(manifest.Disposal.Procedure) == "" {
return fmt.Errorf("parity manifest: disposal procedure is required")
}
if !allowedDisposalState(manifest.Disposal.State) {
return fmt.Errorf("parity manifest: unknown disposal state %q", manifest.Disposal.State)
}
discovered, err := discoverParityReferenceFiles(root, manifest.ReferenceRoot)
if err != nil {
return err
}
discoveredSet := make(map[string]struct{}, len(discovered))
for _, path := range discovered {
discoveredSet[path] = struct{}{}
}
disposal := make(map[string]ParityReferenceFile, len(manifest.Disposal.ReferenceFiles))
for _, reference := range manifest.Disposal.ReferenceFiles {
if _, exists := disposal[reference.Path]; exists {
return fmt.Errorf("parity manifest: duplicate disposal path %q", reference.Path)
}
if !allowedDisposition(reference.Disposition) || len(reference.CoveredBy) == 0 {
return fmt.Errorf("parity manifest: disposal entry %q is unclassified", reference.Path)
}
if err := validateLiteralPath(reference.Path); err != nil {
return fmt.Errorf("parity manifest: disposal entry: %w", err)
}
for _, behaviorID := range reference.CoveredBy {
if _, exists := behaviorIDs[behaviorID]; !exists {
return fmt.Errorf("parity manifest: disposal entry %q covers unknown behavior %q", reference.Path, behaviorID)
}
}
if len(reference.SHA256) != sha256.Size*2 {
return fmt.Errorf("parity manifest: invalid checksum for %q", reference.Path)
}
disposal[reference.Path] = reference
}
if err := validateParityInventory(behaviorSources, disposal); err != nil {
return err
}
if manifest.Disposal.State == "disposed" {
if len(discovered) != 0 {
return fmt.Errorf("parity manifest: disposed state has retained fixture %q", discovered[0])
}
for path := range disposal {
if _, err := os.Lstat(filepath.Join(root, filepath.FromSlash(path))); err == nil {
return fmt.Errorf("parity manifest: disposed state has survivor %q", path)
} else if !os.IsNotExist(err) {
return fmt.Errorf("parity manifest: inspect disposed entry %q: %w", path, err)
}
}
return nil
}
for _, path := range discovered {
if _, exists := disposal[path]; !exists {
return fmt.Errorf("parity manifest: unrecorded retained fixture %q", path)
}
if _, exists := behaviorSources[path]; !exists {
return fmt.Errorf("parity manifest: missing behavior classification for %q", path)
}
}
for path, reference := range disposal {
if _, exists := discoveredSet[path]; !exists {
return fmt.Errorf("parity manifest: retained state is missing fixture %q", path)
}
contents, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path)))
if err != nil {
return fmt.Errorf("parity manifest: read disposal entry %q: %w", path, err)
}
digest := sha256.Sum256(contents)
if !strings.EqualFold(reference.SHA256, hex.EncodeToString(digest[:])) {
return fmt.Errorf("parity manifest: checksum drift for %q", path)
}
}
return nil
}
// validateParityInventory keeps disposition evidence anchored to the complete
// classified source inventory even after the files themselves are disposed.
func validateParityInventory(behaviorSources map[string]struct{}, disposal map[string]ParityReferenceFile) error {
missing := make([]string, 0)
for path := range behaviorSources {
if _, exists := disposal[path]; !exists {
missing = append(missing, path)
}
}
if len(missing) != 0 {
sort.Strings(missing)
return fmt.Errorf("parity manifest: disposal inventory is missing classified reference %q", missing[0])
}
extra := make([]string, 0)
for path := range disposal {
if _, exists := behaviorSources[path]; !exists {
extra = append(extra, path)
}
}
if len(extra) != 0 {
sort.Strings(extra)
return fmt.Errorf("parity manifest: disposal inventory has unclassified reference %q", extra[0])
}
return nil
}
// discoverParityReferenceFiles returns every retained Python source or test
// fixture below the manifest-owned root in deterministic repository-relative
// order. The manifest must enumerate this result exactly.
func discoverParityReferenceFiles(root, referenceRoot string) ([]string, error) {
if strings.TrimSpace(referenceRoot) == "" || filepath.IsAbs(referenceRoot) {
return nil, fmt.Errorf("parity manifest: invalid reference_root %q", referenceRoot)
}
cleaned := filepath.Clean(filepath.FromSlash(referenceRoot))
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
return nil, fmt.Errorf("parity manifest: invalid reference_root %q", referenceRoot)
}
directory := filepath.Join(root, cleaned)
info, err := os.Stat(directory)
if err != nil {
return nil, fmt.Errorf("parity manifest: reference_root %q: %w", referenceRoot, err)
}
if !info.IsDir() {
return nil, fmt.Errorf("parity manifest: reference_root %q must be a directory", referenceRoot)
}
var files []string
err = filepath.WalkDir(directory, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() || !entry.Type().IsRegular() {
return nil
}
name := entry.Name()
if !strings.HasSuffix(name, ".py") && !strings.HasSuffix(name, ".py.orig") {
return nil
}
relative, err := filepath.Rel(root, path)
if err != nil {
return err
}
files = append(files, filepath.ToSlash(relative))
return nil
})
if err != nil {
return nil, fmt.Errorf("parity manifest: discover retained fixtures: %w", err)
}
sort.Strings(files)
return files, nil
}
// FormatParityReport emits a bounded, stable report for the operator command.
func FormatParityReport(manifest ParityManifest) string {
ids := make([]string, 0, len(manifest.Behaviors))
for _, behavior := range manifest.Behaviors {
ids = append(ids, behavior.ID)
}
sort.Strings(ids)
return fmt.Sprintf("parity: ok\n schema_version: %d\n behaviors: %d\n disposal_state: %s\n disposal_files: %d\n deletion_gate: %s\n ids: %s\n",
manifest.SchemaVersion, len(ids), manifest.Disposal.State, len(manifest.Disposal.ReferenceFiles), manifest.DeletionGate, strings.Join(ids, ","))
}
func allowedDisposition(value string) bool {
return value == "absorb" || value == "replace" || value == "not-applicable"
}
func allowedDisposalState(value string) bool {
return value == "retained" || value == "disposed"
}
func validateLiteralPath(value string) error {
if strings.TrimSpace(value) == "" || strings.ContainsAny(value, "*?[") || filepath.IsAbs(value) {
return fmt.Errorf("invalid literal path %q", value)
}
cleaned := filepath.Clean(filepath.FromSlash(value))
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
return fmt.Errorf("invalid literal path %q", value)
}
return nil
}
func validateLiteralExistingPath(root, value string) error {
if err := validateLiteralPath(value); err != nil {
return err
}
cleaned := filepath.Clean(filepath.FromSlash(value))
info, err := os.Stat(filepath.Join(root, cleaned))
if err != nil {
return fmt.Errorf("stale path %q: %w", value, err)
}
if info.IsDir() {
return fmt.Errorf("path %q must be a file", value)
}
return nil
}