From 5d229eda104d774388fa5883a04294aa582edf8e Mon Sep 17 00:00:00 2001 From: Gnani Rahul Nutakki Date: Wed, 22 Jul 2026 18:50:02 -0500 Subject: [PATCH] feat(remediation): bind immutable desired changes Add desired-change/v1 as an opaque exact binding between one validated Git source snapshot, one canonical transformer version, cited evidence, and exact proposed bytes. Reject forged, ambiguous, oversized, and no-op claims while preserving deterministic mutation-isolated state. Keep construction package-private and leave R2/R4, renderer policy, resolver wiring, authority, credentials, I/O, persistence, mutation, and execution structurally absent. GSTACK-Checkpoint: 2026-07-22/e14-desired-change#1 Signed-off-by: Gnani Rahul Nutakki --- README.md | 22 +- docs/specs/E2-readfed-brain-integrations.md | 23 +- internal/remediation/boundary_test.go | 69 +++++ internal/remediation/desired_change.go | 117 ++++++++ internal/remediation/desired_change_test.go | 295 ++++++++++++++++++++ sessions/2026-07-22-e14-desired-change.md | 100 +++++++ 6 files changed, 616 insertions(+), 10 deletions(-) create mode 100644 internal/remediation/desired_change.go create mode 100644 internal/remediation/desired_change_test.go create mode 100644 sessions/2026-07-22-e14-desired-change.md diff --git a/README.md b/README.md index 5de0b52..e72d3d7 100644 --- a/README.md +++ b/README.md @@ -724,12 +724,22 @@ constructor recomputes the blob object ID, copies and canonically orders attache evidence, and bounds content, evidence count, and validity. A pure trusted-time check classifies the snapshot as fresh, future, or stale; it performs no I/O. -`DesiredChange` remains a later separately reviewed contract. The snapshot contains no desired -bytes, PR metadata, handler binding, actor, intent, policy decision, approval, credential, endpoint, -persistence, dispatch, mutation, or execution state, and it is not wired into the Brain, resolver, -connector runtime, PEP, or Hub. R2 and R4 remain advisory-only. This offline contract adds no API -request, egress, storage, cloud resource, telemetry cardinality, or recurring cost; a future live -adapter must separately own least-privilege contents-read credentials, rate limits, and freshness. +The separately gated output half now exists as immutable `desired-change/v1`. A `DesiredChange` +defensively embeds one exact validated snapshot, one lowercase canonical `/` +identity, exact bounded non-NUL UTF-8 output bytes, and 2–32 unique stable evidence references that +must attach the snapshot's affected resource and observed blob. It preserves bytes without Unicode, +line-ending, whitespace, or YAML normalization, rejects exact no-op output, and canonically orders +copied evidence. + +There is deliberately no exported desired-change constructor. Until a concrete deterministic +transformer or declarative renderer is separately reviewed, request and runtime code cannot label +arbitrary bytes as trusted output. The snapshot still contains no desired bytes, and neither +contract contains PR metadata, handler binding, actor, intent, policy decision, approval, +credential, endpoint, persistence, dispatch, mutation, or execution state. Neither is wired into +the Brain, resolver, connector runtime, PEP, or Hub, so R2 and R4 remain advisory-only. These +offline contracts add no API request, egress, storage, cloud resource, telemetry cardinality, or +recurring cost; a future live adapter and transformer separately own credentials, rate limits, +freshness, format semantics, and evidence-to-output policy. Phase-L kubeconfig hydration supplies LIVE pod/workload/node evidence and discrete Kubernetes Events for TIMELINE when present. DESIRED and TELEMETRY remain unavailable unless a future diff --git a/docs/specs/E2-readfed-brain-integrations.md b/docs/specs/E2-readfed-brain-integrations.md index 910b3fd..7ae962d 100644 --- a/docs/specs/E2-readfed-brain-integrations.md +++ b/docs/specs/E2-readfed-brain-integrations.md @@ -647,10 +647,25 @@ fresh. A zero clock or internally invalid snapshot fails closed. `GitSourceSnapshot` deliberately has no desired bytes, PR title/body, commit message, handler contract, actor, role, intent ID, policy or approval decision, credential, endpoint, signature, -persistence, dispatch, mutation, or execution state. `DesiredChange` is a later separately reviewed -transformer/renderer contract that must bind its input snapshot version, evidence, and output. The -snapshot is not wired into the existing resolver, Brain, connector runtime, PEP, or Hub. R2 and R4 -therefore remain operator-facing advisory rules; this split adds no production read or write path. +persistence, dispatch, mutation, or execution state. + +The separately reviewed output contract is `desired-change/v1`. One opaque `DesiredChange` binds a +defensive copy of one valid snapshot to one lowercase canonical `/` identity, +exact proposed UTF-8 bytes, and 2–32 unique stable evidence references. Construction preserves the +output byte sequence exactly, applies the same 64 KiB and non-NUL bounds as the snapshot, rejects an +exact no-op against current content, canonically sorts copied evidence, and requires both the +affected resource and exact observed blob to remain attached. The embedded snapshot preserves the +repository, base ref, commit, path, blob, current bytes, evidence, and validity window as one exact +later composition precondition. + +Desired-change construction is package-private. No concrete R2 memory-limit transformer, R4 +live-to-Git reconciler, YAML/Helm/Kustomize renderer, or file mapper is approved in this contract, +so callers cannot relabel supplied replacement bytes as trusted output. The change exposes only its +closed contract version and has no PR metadata, handler binding, actor, role, intent ID, policy or +approval decision, credential, endpoint, signature, persistence, dispatch, mutation, or execution +state. Neither half is wired into the existing resolver, Brain, connector runtime, PEP, or Hub. +R2 and R4 therefore remain operator-facing advisory rules; this split adds no production read or +write path. ### 3.7 Where the Brain lives (open decision) diff --git a/internal/remediation/boundary_test.go b/internal/remediation/boundary_test.go index 4d49788..eb356fa 100644 --- a/internal/remediation/boundary_test.go +++ b/internal/remediation/boundary_test.go @@ -3,6 +3,7 @@ package remediation import ( + "go/ast" "go/parser" "go/token" "io/fs" @@ -137,6 +138,74 @@ func TestGitSourceSnapshotBoundaryIsObservedOnlyAndOpaque(t *testing.T) { } } +func TestDesiredChangeBoundaryIsTransformerOwnedAndOpaque(t *testing.T) { + t.Parallel() + change := reflect.TypeFor[DesiredChange]() + if change.NumField() != 5 { + t.Fatalf("DesiredChange fields = %d, want exact reviewed shape", change.NumField()) + } + for index := range change.NumField() { + field := change.Field(index) + if field.IsExported() { + t.Fatalf("DesiredChange exposes mutable field %s", field.Name) + } + name := strings.ToLower(field.Name) + for _, forbidden := range []string{ + "title", "body", "commitmessage", "handler", "actor", "role", "intent", "approval", + "policy", "credential", "token", "secret", "signature", "endpoint", "dispatch", "execute", + } { + if strings.Contains(name, forbidden) { + t.Fatalf("DesiredChange exposes forbidden authority field %s", field.Name) + } + } + } + if change.NumMethod() != 1 || change.Method(0).Name != "Version" { + t.Fatalf("DesiredChange methods = %#v, want only Version", change) + } + + err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() || filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") { + return nil + } + file, parseErr := parser.ParseFile(token.NewFileSet(), path, nil, 0) + if parseErr != nil { + return parseErr + } + for _, rawDeclaration := range file.Decls { + declaration, ok := rawDeclaration.(*ast.FuncDecl) + if ok && declaration.Recv == nil && token.IsExported(declaration.Name.Name) && + returnsDesiredChange(declaration) { + t.Errorf("DesiredChange construction escaped the reviewed package boundary as %s in %s", declaration.Name.Name, path) + } + } + return nil + }) + if err != nil { + t.Fatalf("inspect DesiredChange package boundary: %v", err) + } +} + +func returnsDesiredChange(declaration *ast.FuncDecl) bool { + if declaration.Type.Results == nil { + return false + } + found := false + for _, result := range declaration.Type.Results.List { + ast.Inspect(result.Type, func(node ast.Node) bool { + identifier, ok := node.(*ast.Ident) + if ok && identifier.Name == "DesiredChange" { + found = true + return false + } + return !found + }) + } + return found +} + func assertExactFields(t *testing.T, value reflect.Type, expected []string) { t.Helper() if value.NumField() != len(expected) { diff --git a/internal/remediation/desired_change.go b/internal/remediation/desired_change.go new file mode 100644 index 0000000..3e517d4 --- /dev/null +++ b/internal/remediation/desired_change.go @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 + +package remediation + +import ( + "fmt" + "sort" + "strings" + + "github.com/ArdurAI/sith/internal/fleet" +) + +const ( + // DesiredChangeVersion is the first immutable snapshot-to-output binding contract. + DesiredChangeVersion = "desired-change/v1" + + maxDesiredChangeEvidenceRefs = 32 +) + +// DesiredChange is immutable after construction. It binds exact proposed bytes to one exact +// GitSourceSnapshot, one reviewed transformer version, and cited evidence. Its fields remain +// private, and construction remains package-private until a concrete transformer policy is +// separately reviewed. +type DesiredChange struct { + version string + snapshot GitSourceSnapshot + transformerVersion string + desiredContent string + evidenceRefs []fleet.ResourceRef +} + +// Version reports the closed desired-change contract without exposing proposed bytes or source +// state. +func (change DesiredChange) Version() string { return change.version } + +// newDesiredChange is deliberately package-private. Only a separately reviewed deterministic +// transformer or declarative renderer in this package may turn output bytes into a DesiredChange; +// request and runtime callers cannot relabel arbitrary bytes as trusted transformer output. +func newDesiredChange( + snapshot GitSourceSnapshot, + transformerVersion string, + desiredContent string, + evidenceRefs []fleet.ResourceRef, +) (DesiredChange, error) { + change := DesiredChange{ + version: DesiredChangeVersion, + snapshot: cloneGitSourceSnapshot(snapshot), + transformerVersion: transformerVersion, + desiredContent: desiredContent, + evidenceRefs: cloneResourceRefs(evidenceRefs), + } + sort.Slice(change.evidenceRefs, func(left, right int) bool { + return resourceRefLess(change.evidenceRefs[left], change.evidenceRefs[right]) + }) + if err := change.validate(); err != nil { + return DesiredChange{}, fmt.Errorf("construct desired change: change is invalid") + } + return change, nil +} + +func (change DesiredChange) validate() error { + if change.version != DesiredChangeVersion || change.snapshot.validate() != nil || + !validTransformerVersion(change.transformerVersion) || + !validGitSourceContent(change.desiredContent) || + change.desiredContent == change.snapshot.currentContent || len(change.evidenceRefs) < 2 || + len(change.evidenceRefs) > maxDesiredChangeEvidenceRefs { + return fmt.Errorf("desired change is invalid") + } + + subjectAttached := false + blobAttached := false + for index, ref := range change.evidenceRefs { + if validateStableRef(ref) != nil || + (index > 0 && !resourceRefLess(change.evidenceRefs[index-1], ref)) { + return fmt.Errorf("desired change evidence is invalid") + } + subjectAttached = subjectAttached || sameResourceRef(ref, change.snapshot.subject) + blobAttached = blobAttached || gitSourceBlobRefMatches(ref, change.snapshot) + } + if !subjectAttached || !blobAttached { + return fmt.Errorf("desired change evidence is unattached") + } + return nil +} + +func cloneGitSourceSnapshot(snapshot GitSourceSnapshot) GitSourceSnapshot { + cloned := snapshot + cloned.subject = cloneResourceRef(snapshot.subject) + cloned.evidenceRefs = cloneResourceRefs(snapshot.evidenceRefs) + return cloned +} + +func validTransformerVersion(value string) bool { + if validateSafeText(value, maxIdentityBytes, false) != nil || value != strings.ToLower(value) { + return false + } + parts := strings.Split(value, "/") + if len(parts) != 2 { + return false + } + for _, part := range parts { + if part == "" || !isLowerAlphaNumeric(part[0]) || !isLowerAlphaNumeric(part[len(part)-1]) { + return false + } + for _, character := range part { + if (character < 'a' || character > 'z') && (character < '0' || character > '9') && + character != '-' && character != '_' && character != '.' { + return false + } + } + } + return true +} + +func isLowerAlphaNumeric(value byte) bool { + return (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') +} diff --git a/internal/remediation/desired_change_test.go b/internal/remediation/desired_change_test.go new file mode 100644 index 0000000..a956d49 --- /dev/null +++ b/internal/remediation/desired_change_test.go @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 + +package remediation + +import ( + "fmt" + "slices" + "strings" + "sync" + "testing" + + "github.com/ArdurAI/sith/internal/fleet" +) + +const testTransformerVersion = "fixture-renderer/v1" + +func TestDesiredChangePreservesExactSnapshotAndOutput(t *testing.T) { + t.Parallel() + snapshot := mustGitSourceSnapshot(t) + evidence := validDesiredChangeEvidence(snapshot) + slices.Reverse(evidence) + desiredContent := "apiVersion: v1\r\nmetadata:\n name: café\t\nspec:\n replicas: 4\n" + + change, err := newDesiredChange(snapshot, testTransformerVersion, desiredContent, evidence) + if err != nil { + t.Fatalf("newDesiredChange() error = %v", err) + } + if change.Version() != DesiredChangeVersion || change.transformerVersion != testTransformerVersion || + change.desiredContent != desiredContent || !sameGitSourceSnapshot(change.snapshot, snapshot) { + t.Fatalf("change did not preserve the exact binding: %#v", change) + } + if len(change.evidenceRefs) != 2 || !sameResourceRef(change.evidenceRefs[0], testBlobRef()) || + !sameResourceRef(change.evidenceRefs[1], testSubjectRef()) { + t.Fatalf("evidence = %#v, want canonical blob and subject order", change.evidenceRefs) + } + + reordered, err := newDesiredChange( + snapshot, + testTransformerVersion, + desiredContent, + validDesiredChangeEvidence(snapshot), + ) + if err != nil { + t.Fatalf("newDesiredChange(reordered) error = %v", err) + } + if !slices.EqualFunc(change.evidenceRefs, reordered.evidenceRefs, sameResourceRef) { + t.Fatalf("evidence ordering changed with caller order: %#v != %#v", change.evidenceRefs, reordered.evidenceRefs) + } +} + +func TestDesiredChangeConstructionIsMutationIsolated(t *testing.T) { + t.Parallel() + snapshot := mustGitSourceSnapshot(t) + evidence := validDesiredChangeEvidence(snapshot) + desiredContent := "replicas: 4\n" + change, err := newDesiredChange(snapshot, testTransformerVersion, desiredContent, evidence) + if err != nil { + t.Fatal(err) + } + + snapshot.subject.Name = "mutated-subject" + snapshot.repository.Repository = "other" + snapshot.baseCommit = strings.Repeat("c", 40) + snapshot.currentContent = "mutated source" + snapshot.evidenceRefs[0].Name = "mutated-snapshot-evidence" + evidence[0].Name = "mutated-change-evidence" + + if err := change.validate(); err != nil { + t.Fatalf("change retained caller mutation: %v", err) + } + if change.snapshot.subject.Name != "payments" || change.snapshot.repository.Repository != "sith" || + change.snapshot.baseCommit != testBaseSHA || change.snapshot.currentContent == "mutated source" || + change.snapshot.evidenceRefs[0].Name == "mutated-snapshot-evidence" || + change.evidenceRefs[1].Name == "mutated-change-evidence" || change.desiredContent != desiredContent { + t.Fatalf("change retained caller-owned state: %#v", change) + } +} + +func TestNewDesiredChangeRejectsInvalidClaims(t *testing.T) { + tests := []struct { + name string + mutate func(*desiredChangeFixture) + }{ + {"zero snapshot", func(input *desiredChangeFixture) { input.snapshot = GitSourceSnapshot{} }}, + {"forged snapshot", func(input *desiredChangeFixture) { input.snapshot.version = "forged/v9" }}, + {"forged snapshot subject", func(input *desiredChangeFixture) { input.snapshot.subject.Name = "other" }}, + {"empty transformer", func(input *desiredChangeFixture) { input.transformerVersion = "" }}, + {"uppercase transformer", func(input *desiredChangeFixture) { input.transformerVersion = "Fixture/v1" }}, + {"unversioned transformer", func(input *desiredChangeFixture) { input.transformerVersion = "fixture-renderer" }}, + {"ambiguous transformer", func(input *desiredChangeFixture) { input.transformerVersion = "fixture/renderer/v1" }}, + {"option-shaped transformer", func(input *desiredChangeFixture) { input.transformerVersion = "-fixture/v1" }}, + {"spaced transformer", func(input *desiredChangeFixture) { input.transformerVersion = "fixture renderer/v1" }}, + {"invalid UTF-8 output", func(input *desiredChangeFixture) { input.desiredContent = string([]byte{0xff}) }}, + {"NUL output", func(input *desiredChangeFixture) { input.desiredContent = "secret\x00value" }}, + {"oversized output", func(input *desiredChangeFixture) { + input.desiredContent = strings.Repeat("x", maxGitSourceSnapshotContentBytes+1) + }}, + {"no-op output", func(input *desiredChangeFixture) { + input.desiredContent = input.snapshot.currentContent + }}, + {"no evidence", func(input *desiredChangeFixture) { input.evidenceRefs = nil }}, + {"subject-only evidence", func(input *desiredChangeFixture) { + input.evidenceRefs = []fleet.ResourceRef{input.snapshot.subject} + }}, + {"blob-only evidence", func(input *desiredChangeFixture) { + input.evidenceRefs = []fleet.ResourceRef{testBlobRef()} + }}, + {"foreign subject evidence", func(input *desiredChangeFixture) { input.evidenceRefs[0].Name = "other" }}, + {"foreign blob evidence", func(input *desiredChangeFixture) { + input.evidenceRefs[1].Name = strings.Repeat("e", 40) + }}, + {"duplicate evidence", func(input *desiredChangeFixture) { + input.evidenceRefs = append(input.evidenceRefs, input.evidenceRefs[0]) + }}, + {"unsafe evidence", func(input *desiredChangeFixture) { input.evidenceRefs[0].Name = "secret\nforged" }}, + {"evidence attributes", func(input *desiredChangeFixture) { + input.evidenceRefs[0].Attributes = map[string]string{"uid": "private"} + }}, + {"too much evidence", func(input *desiredChangeFixture) { + for index := len(input.evidenceRefs); index <= maxDesiredChangeEvidenceRefs; index++ { + input.evidenceRefs = append(input.evidenceRefs, fleet.ResourceRef{ + SourceKind: "github", Scope: "github.com", Kind: "Observation", + Namespace: "ArdurAI/sith", Name: fmt.Sprintf("evidence-%02d", index), + }) + } + }}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + input := validDesiredChangeFixture(t) + test.mutate(&input) + change, err := newDesiredChange( + input.snapshot, + input.transformerVersion, + input.desiredContent, + input.evidenceRefs, + ) + if err == nil || change.Version() != "" { + t.Fatalf("newDesiredChange() = %#v, %v, want rejection", change, err) + } + if strings.Contains(err.Error(), "secret") || len(err.Error()) > 160 { + t.Fatalf("constructor leaked or returned unbounded error: %q", err) + } + }) + } +} + +func TestDesiredChangeRejectsForgedState(t *testing.T) { + t.Parallel() + fixture := validDesiredChangeFixture(t) + change, err := newDesiredChange( + fixture.snapshot, + fixture.transformerVersion, + fixture.desiredContent, + fixture.evidenceRefs, + ) + if err != nil { + t.Fatal(err) + } + tests := []struct { + name string + mutate func(*DesiredChange) + }{ + {"version", func(value *DesiredChange) { value.version = "forged/v9" }}, + {"snapshot", func(value *DesiredChange) { value.snapshot.observedBlobSHA = strings.Repeat("e", 40) }}, + {"transformer", func(value *DesiredChange) { value.transformerVersion = "forged" }}, + {"no-op output", func(value *DesiredChange) { value.desiredContent = value.snapshot.currentContent }}, + {"evidence order", func(value *DesiredChange) { slices.Reverse(value.evidenceRefs) }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + forged := change + forged.snapshot = cloneGitSourceSnapshot(change.snapshot) + forged.evidenceRefs = cloneResourceRefs(change.evidenceRefs) + test.mutate(&forged) + if err := forged.validate(); err == nil { + t.Fatalf("forged change validated: %#v", forged) + } + }) + } +} + +func TestDesiredChangeValidationIsConcurrentAndReadOnly(t *testing.T) { + t.Parallel() + fixture := validDesiredChangeFixture(t) + change, err := newDesiredChange( + fixture.snapshot, + fixture.transformerVersion, + fixture.desiredContent, + fixture.evidenceRefs, + ) + if err != nil { + t.Fatal(err) + } + before := change + before.snapshot = cloneGitSourceSnapshot(change.snapshot) + before.evidenceRefs = cloneResourceRefs(change.evidenceRefs) + + const readers = 64 + errors := make(chan error, readers) + var group sync.WaitGroup + for range readers { + group.Add(1) + go func() { + defer group.Done() + if validateErr := change.validate(); validateErr != nil { + errors <- validateErr + return + } + if change.Version() != DesiredChangeVersion { + errors <- fmt.Errorf("version = %q", change.Version()) + } + }() + } + group.Wait() + if !sameDesiredChange(change, before) { + t.Fatalf("concurrent validation mutated change: %#v != %#v", change, before) + } + close(errors) + for err := range errors { + t.Error(err) + } +} + +func FuzzDesiredChangeConstructor(f *testing.F) { + for _, seed := range []struct{ transformer, content string }{ + {testTransformerVersion, "replicas: 4\n"}, + {"invalid", "secret\x00value"}, + {"renderer/2026-07-22", "name: café\r\n"}, + } { + f.Add(seed.transformer, seed.content) + } + f.Fuzz(func(t *testing.T, transformer, content string) { + snapshot := mustGitSourceSnapshot(t) + change, err := newDesiredChange(snapshot, transformer, content, validDesiredChangeEvidence(snapshot)) + if err != nil { + return + } + if change.transformerVersion != transformer || change.desiredContent != content || + !sameGitSourceSnapshot(change.snapshot, snapshot) { + t.Fatalf("accepted change altered its exact binding: %#v", change) + } + if validateErr := change.validate(); validateErr != nil { + t.Fatalf("accepted change did not revalidate: %v", validateErr) + } + }) +} + +type desiredChangeFixture struct { + snapshot GitSourceSnapshot + transformerVersion string + desiredContent string + evidenceRefs []fleet.ResourceRef +} + +func validDesiredChangeFixture(t testing.TB) desiredChangeFixture { + t.Helper() + snapshot := mustGitSourceSnapshot(t) + return desiredChangeFixture{ + snapshot: snapshot, + transformerVersion: testTransformerVersion, + desiredContent: "replicas: 4\n", + evidenceRefs: validDesiredChangeEvidence(snapshot), + } +} + +func mustGitSourceSnapshot(t testing.TB) GitSourceSnapshot { + t.Helper() + snapshot, err := NewGitSourceSnapshot(validGitSourceSnapshotInput()) + if err != nil { + t.Fatalf("NewGitSourceSnapshot() error = %v", err) + } + return snapshot +} + +func validDesiredChangeEvidence(snapshot GitSourceSnapshot) []fleet.ResourceRef { + return []fleet.ResourceRef{cloneResourceRef(snapshot.subject), testBlobRef()} +} + +func sameGitSourceSnapshot(left, right GitSourceSnapshot) bool { + return left.version == right.version && left.workspace == right.workspace && + sameResourceRef(left.subject, right.subject) && left.source == right.source && + left.observedAt.Equal(right.observedAt) && left.validUntil.Equal(right.validUntil) && + left.repository == right.repository && left.baseRef == right.baseRef && + left.baseCommit == right.baseCommit && left.filePath == right.filePath && + left.observedBlobSHA == right.observedBlobSHA && left.currentContent == right.currentContent && + slices.EqualFunc(left.evidenceRefs, right.evidenceRefs, sameResourceRef) +} + +func sameDesiredChange(left, right DesiredChange) bool { + return left.version == right.version && sameGitSourceSnapshot(left.snapshot, right.snapshot) && + left.transformerVersion == right.transformerVersion && left.desiredContent == right.desiredContent && + slices.EqualFunc(left.evidenceRefs, right.evidenceRefs, sameResourceRef) +} diff --git a/sessions/2026-07-22-e14-desired-change.md b/sessions/2026-07-22-e14-desired-change.md new file mode 100644 index 0000000..5587c16 --- /dev/null +++ b/sessions/2026-07-22-e14-desired-change.md @@ -0,0 +1,100 @@ +# Session — 2026-07-22 — E14 immutable desired change + +**Builder:** Gnani Rahul Nutakki · **Branch:** `gnanirahulnutakki/desired-change-20260722` +**Slice:** [#307](https://github.com/ArdurAI/sith/issues/307), E14 +[#46](https://github.com/ArdurAI/sith/issues/46) · **Status:** complete local proof; hosted proof pending + +## [G] Goal + +Implement the owner-approved second half of the F14.6 provenance split: one immutable +`DesiredChange` that binds exact proposed file bytes to an exact `GitSourceSnapshot`, cited +evidence, and a transformer version without enabling R2/R4 writes. + +## [S] Scope + +- Add a versioned opaque desired-change contract outside `internal/brain`. +- Bind one validated snapshot, one canonical transformer version, exact desired bytes, and a + bounded unique evidence set. +- Keep construction package-private until a concrete deterministic transformer or declarative + renderer policy receives separate review. +- Exclude R2/R4 transformation logic, PR metadata, handler binding, actor, intent, policy, + approval, credential, endpoint, persistence, dispatch, mutation, and execution behavior. + +## [A] Decision and implementation + +- The owner selected the snapshot-plus-change decomposition in + [issue comment 5051813734](https://github.com/ArdurAI/sith/issues/46#issuecomment-5051813734). + Snapshot child #303 landed first through #305; child #307 locks this separate output contract. +- `desired-change/v1` privately contains a defensive copy of one valid + `git-source-snapshot/v1`, a lowercase canonical `/` identity, exact desired + content, and copied evidence references. +- Desired content is a non-NUL UTF-8 sequence capped at 64 KiB. Empty output, CRLF, tabs, Unicode, + and trailing whitespace remain exact bytes; no Unicode, line-ending, whitespace, YAML, Helm, or + Kustomize normalization occurs. +- Exact output equal to the snapshot current bytes is rejected as a no-op. +- Evidence is capped at 32 unique stable references, canonically sorted, and must attach both the + affected resource and the exact observed Git blob. The snapshot's own nested resource and + evidence values are deep-copied. +- The only exported operation is `Version`. The constructor and validator remain package-private; + an AST boundary test rejects an exported construction function. +- No transformer implementation or allowlisted R2/R4 policy exists in this slice. A future + reviewed transformer must live at the trusted package boundary before it can construct the + contract. + +## [T] Proof + +- Focused remediation tests pass under the race detector, including 50 consecutive repetitions. +- Adversarial coverage rejects zero and forged snapshots, malformed transformer versions, + invalid/NUL/oversized/no-op output, missing/duplicate/unsafe/foreign evidence, private-field + forgery, input alias mutation, and nondeterministic evidence ordering. +- The dedicated constructor fuzzer passes 50,000 executions. Fuzz and 64-reader concurrent tests + preserve exact bytes and source binding without mutation. +- Reflection and AST tests lock the five-field private shape, single-method public surface, and + absence of an exported constructor. The recursive production import guard continues to exclude + I/O, policy, persistence, and runtime authority. +- `make ci` passes formatting, vet, zero-issue lint, current vulnerability scanning, every race + test, shell/tooling policies, all nine Prometheus rules, performance, binary end-to-end, and the + production build. The remediation package reaches 94.8% statement coverage. +- `make e2e-isolation` passes PostgreSQL 18.4 forced RLS and both 50,000-execution cross-workspace + fuzzers. +- `make release-check` passes module verification, two reproducible four-platform builds, SPDX + SBOMs, formula generation, and the amd64/arm64 distroless OCI layout. +- The pinned Kubernetes 1.36.1 Kind gate passes two-cluster fleet fan-out, OCI image, and Argo + Application projection under the race detector in 295.351 seconds. Teardown leaves no Kind + cluster or isolated release builder. +- The independent CodeRabbit loop reviewed all six files. Two documentation/boundary findings and + one concurrent-test proof finding were incorporated; the final full-diff pass reports zero + findings. +- Hosted exact-head proof and exact post-merge proof remain required before closure. + +## [S] Security, reliability, and cost + +The contract is pure and offline. It cannot be constructed by request/runtime packages, stores no +secret or authority, and adds no API request, egress, storage, cloud resource, telemetry +cardinality, or recurring cost. Before a transformer is approved, its review must cover parser +ambiguity, multi-document YAML, Helm/Kustomize semantics, file mapping, safety bounds, rollback, +and deterministic evidence-to-output binding. + +## [R] Primary references + +- [GitHub REST Git blobs](https://docs.github.com/en/rest/git/blobs?apiVersion=2026-03-10) +- [GitHub REST Git trees](https://docs.github.com/en/rest/git/trees?apiVersion=2026-03-10) +- [GitHub REST Git commits](https://docs.github.com/en/rest/git/commits?apiVersion=2026-03-10) + +## [N] Next + +Complete local fuzz, full CI, isolation, release, and real Kind gates; run independent review; +then create one signed DCO/GSTACK commit and require exact-head hosted CI/CodeQL plus exact +post-merge `dev` proof. Close only child #307. F14.6 and E14 remain open; R2/R4 transformer policy, +live Git reads, resolver composition, Hub identity, PEP, approval, dispatch, and execution are +separate slices. + +## [C] Checkpoint #1 + +The desired-change contract, package-private construction boundary, adversarial tests, docs, and +complete local gate matrix are frozen on exact base +`01b5ae7a0d329e19d11696bb24a8f73babb0449b`. README was reviewed and updated before commit because +the public architecture now includes the separately gated output contract. The self-managed QA +kubeconfig was not used; this slice is pure/offline and the real-cluster requirement was satisfied +by disposable local Kind clusters. Remaining gates are the signed commit, exact-head hosted proof, +merge without rewriting the signed feature commit, and exact post-merge `dev` proof.