From a099afaf1b39331178a2e899b4d52924c3da256f Mon Sep 17 00:00:00 2001 From: Syed <40798652+thelostorbital@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:32:52 +0530 Subject: [PATCH 1/3] feat: compile deterministic test bootstrap plans --- internal/isolation/bootstrap/canonical.go | 317 +++++++++ .../isolation/bootstrap/canonical_test.go | 225 +++++++ internal/isolation/bootstrap/compile.go | 621 ++++++++++++++++++ internal/isolation/bootstrap/compile_test.go | 389 +++++++++++ internal/isolation/bootstrap/helpers_test.go | 197 ++++++ internal/isolation/bootstrap/types.go | 263 ++++++++ 6 files changed, 2012 insertions(+) create mode 100644 internal/isolation/bootstrap/canonical.go create mode 100644 internal/isolation/bootstrap/canonical_test.go create mode 100644 internal/isolation/bootstrap/compile.go create mode 100644 internal/isolation/bootstrap/compile_test.go create mode 100644 internal/isolation/bootstrap/helpers_test.go create mode 100644 internal/isolation/bootstrap/types.go diff --git a/internal/isolation/bootstrap/canonical.go b/internal/isolation/bootstrap/canonical.go new file mode 100644 index 0000000..679d755 --- /dev/null +++ b/internal/isolation/bootstrap/canonical.go @@ -0,0 +1,317 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +package bootstrap + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/netip" + "regexp" + "slices" + "strings" + "time" + + "github.com/thelostorbital/ctrldb/internal/config" + "github.com/thelostorbital/ctrldb/internal/domain" + "github.com/thelostorbital/ctrldb/internal/isolation" + "github.com/thelostorbital/ctrldb/internal/policy" +) + +var ( + desiredProjectPattern = regexp.MustCompile(`^[a-z][a-z0-9-]{4,28}[a-z0-9]$`) + desiredLocationPattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,61}[a-z0-9]$`) +) + +// CanonicalJSON returns the compact, hash-bound representation suitable for +// review, approval, and M1-05's BootstrapEnvelopeV1 handoff. +func (value CompiledPlan) CanonicalJSON() ([]byte, error) { + contract, err := validateCompiledPayload(value.payload) + if err != nil || contract.Digest() != value.contract.Digest() { + return nil, invalidCompiled("payload") + } + digest, err := hashJSON(value.payload) + if err != nil || digest != value.documentHash { + return nil, invalidCompiled("document hash") + } + return json.Marshal(compiledWireV1{SchemaVersion: CompiledPlanSchemaV1, Payload: clonePayload(value.payload), DocumentSHA256: value.documentHash}) +} + +// ParseCompiledPlan accepts only canonical JSON with a complete valid +// cross-binding. Duplicate, unknown, null, or trailing fields fail closed. +func ParseCompiledPlan(encoded []byte) (CompiledPlan, error) { + if len(encoded) == 0 || !json.Valid(encoded) { + return CompiledPlan{}, invalidCompiled("document") + } + if err := rejectDuplicateKeys(encoded); err != nil { + return CompiledPlan{}, err + } + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.DisallowUnknownFields() + var wire compiledWireV1 + if err := decoder.Decode(&wire); err != nil { + return CompiledPlan{}, invalidCompiled("document schema") + } + var trailing any + if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { + return CompiledPlan{}, invalidCompiled("trailing data") + } + if wire.SchemaVersion != CompiledPlanSchemaV1 || !sha256Pattern.MatchString(wire.DocumentSHA256) { + return CompiledPlan{}, invalidCompiled("schema or document hash") + } + contract, err := validateCompiledPayload(wire.Payload) + if err != nil { + return CompiledPlan{}, err + } + digest, err := hashJSON(wire.Payload) + if err != nil || digest != wire.DocumentSHA256 { + return CompiledPlan{}, invalidCompiled("document integrity") + } + canonical, err := json.Marshal(wire) + if err != nil || !bytes.Equal(canonical, encoded) { + return CompiledPlan{}, invalidCompiled("noncanonical encoding") + } + return CompiledPlan{payload: clonePayload(wire.Payload), documentHash: wire.DocumentSHA256, contract: contract}, nil +} + +func sealCompiled(payload compiledPayloadV1, contract domain.ExecutionContract) (CompiledPlan, error) { + validated, err := validateCompiledPayload(payload) + if err != nil || validated.Digest() != contract.Digest() { + return CompiledPlan{}, invalidCompiled("compiler output") + } + digest, err := hashJSON(payload) + if err != nil { + return CompiledPlan{}, invalidCompiled("document hash") + } + return CompiledPlan{payload: clonePayload(payload), documentHash: digest, contract: contract}, nil +} + +func validateCompiledPayload(payload compiledPayloadV1) (domain.ExecutionContract, error) { + if err := policy.ValidatePlan(payload.Plan); err != nil { + return domain.ExecutionContract{}, invalidCompiled("PlanV1") + } + if err := validateDesiredState(payload.Desired, payload.Plan); err != nil { + return domain.ExecutionContract{}, err + } + expectedResources, err := buildDesiredResources(payload.Desired) + if err != nil || !equalCanonicalValue(expectedResources, payload.DesiredResources) { + return domain.ExecutionContract{}, invalidCompiled("desired resources") + } + if err := validateLimitsAndPricing(payload.Limits, payload.Pricing, payload.Plan); err != nil { + return domain.ExecutionContract{}, err + } + if !slices.Equal(payload.CleanupCapabilities, isolation.InitialCleanupCapabilities()) { + return domain.ExecutionContract{}, invalidCompiled("cleanup capabilities") + } + if err := validateBinding(payload.Binding, payload.Plan); err != nil { + return domain.ExecutionContract{}, err + } + definitions := stepRegistry(expectedResources) + expectedPlan, contract, err := buildPlanValues( + payload.Plan.PlanID, payload.Desired.Project, payload.Plan.Environment, payload.Desired.Account, + payload.Plan.CreatedAt, payload.Plan.ExpiresAt, payload.Plan.PolicyHash.Local, payload.Plan.PolicyHash.Approved, + payload.Pricing, definitions, expectedResources, payload.Limits, + ) + if err != nil || !equalCanonicalValue(expectedPlan, payload.Plan) { + return domain.ExecutionContract{}, invalidCompiled("plan bindings") + } + expectedIntents := buildIntents(definitions, payload.Binding.BindingSHA256) + if !equalCanonicalValue(expectedIntents, payload.Intents) { + return domain.ExecutionContract{}, invalidCompiled("intent registry") + } + if !equalCanonicalValue(buildRiskSummary(payload.Limits), payload.Risks) { + return domain.ExecutionContract{}, invalidCompiled("risk summary") + } + return contract, nil +} + +func validateDesiredState(desired HarnessDesiredState, plan domain.Plan) error { + if desired.Account != plan.Principal || desired.Project != plan.ProjectID || + !desiredProjectPattern.MatchString(desired.Project) || !desiredLocationPattern.MatchString(desired.Region) || + !desiredLocationPattern.MatchString(desired.Zone) || !strings.HasPrefix(desired.Zone, desired.Region+"-") { + return invalidCompiled("desired provider context") + } + prefix, err := netip.ParsePrefix(desired.CIDR) + if err != nil || prefix != prefix.Masked() || !prefix.Addr().IsPrivate() { + return invalidCompiled("desired CIDR") + } + if !equalCanonicalValue(desired.Labels, map[string]string{ + config.LabelManagedBy: config.LabelManagedByValue, + config.LabelEnvironment: config.TestEnvironmentLabel, + config.LabelPurpose: config.TestResourcePurposeLabel, + }) || desired.NamePrefix != config.TestResourcePrefix { + return invalidCompiled("desired labels") + } + values := []string{desired.ControlBucket, desired.AuditBucket, desired.VPC, desired.Subnet, desired.Router, desired.NAT, + desired.IAPFirewall, desired.InternalFirewall, desired.NodeTag, desired.OperatorPrincipal, + desired.DestructivePrincipal, desired.VMPrincipal, desired.WipePrincipal, desired.CIPrincipal, + desired.OperatorRole, desired.DestructiveRole, desired.WipeRunJob, desired.WipeSchedulerJob, desired.ImageDigest} + for _, value := range values { + if value == "" || strings.TrimSpace(value) != value { + return invalidCompiled("desired value") + } + } + if desired.IAPFirewall != iapFirewallName || desired.InternalFirewall != internalFirewallName || + desired.NodeTag != testNodeTag || desired.OperatorRole != operatorRoleName || + desired.DestructiveRole != destructiveRoleName || desired.WipeScheduleUTC != wipeScheduleUTC || + !strings.HasPrefix(desired.ImageDigest, "sha256:") || !sha256Pattern.MatchString(strings.TrimPrefix(desired.ImageDigest, "sha256:")) { + return invalidCompiled("protocol-owned desired state") + } + return nil +} + +func validateLimitsAndPricing(limits RunLimits, price PricingEvidence, plan domain.Plan) error { + if limits.MaximumMachineType == "" || limits.MaximumMachineType != price.MachineType || + limits.MaximumGuestCPUs != price.GuestCPUs || limits.MaximumMemoryMiB != price.MemoryMiB || + limits.MaximumDiskGiB <= 0 || limits.MaximumInstances <= 0 || limits.MaximumLifetimeSec <= 0 || + limits.MaximumCostMicros < 0 || limits.MaximumCostMicros > maximumExactMicros || + limits.EstimatedCostMicros != price.EstimatedRunMicros || limits.EstimatedCostMicros < 0 || + limits.EstimatedCostMicros > limits.MaximumCostMicros || price.Schema != PricingSchemaV1 || + !sha256Pattern.MatchString(price.Revision) || !validUTC(price.ObservedAt) || !validUTC(price.ValidUntil) || + !price.ObservedAt.Before(price.ValidUntil) || price.ObservedAt.After(plan.CreatedAt) || + !plan.CreatedAt.Before(price.ValidUntil) { + return invalidCompiled("limits or pricing") + } + parsedDate, err := time.Parse(time.DateOnly, price.PriceTableDate) + if err != nil || parsedDate.After(plan.CreatedAt) || plan.CreatedAt.Sub(parsedDate) > maximumPricingAge { + return invalidCompiled("price table date") + } + return nil +} + +func validateBinding(binding EnvelopeBinding, plan domain.Plan) error { + if binding.WorkflowID != WorkflowID || binding.PlanID != plan.PlanID || binding.PlanHash != plan.PlanHash || + binding.Account != plan.Principal || !sha256Pattern.MatchString(binding.ManifestHash) || + !sha256Pattern.MatchString(binding.ObservationRevision) || !validUTC(binding.ObservedAt) || + !validUTC(binding.ValidUntil) || !binding.ObservedAt.Before(binding.ValidUntil) || + plan.CreatedAt.Before(binding.ObservedAt) || !plan.CreatedAt.Before(binding.ValidUntil) || + !sha256Pattern.MatchString(binding.BindingSHA256) { + return invalidCompiled("envelope binding") + } + copy := binding + copy.BindingSHA256 = "" + digest, err := hashJSON(copy) + if err != nil || digest != binding.BindingSHA256 { + return invalidCompiled("envelope binding integrity") + } + return nil +} + +func rejectDuplicateKeys(encoded []byte) error { + decoder := json.NewDecoder(bytes.NewReader(encoded)) + decoder.UseNumber() + if err := consumeUniqueValue(decoder); err != nil { + return err + } + if _, err := decoder.Token(); !errors.Is(err, io.EOF) { + return invalidCompiled("trailing data") + } + return nil +} + +func consumeUniqueValue(decoder *json.Decoder) error { + token, err := decoder.Token() + if err != nil || token == nil { + return invalidCompiled("malformed or null JSON") + } + delimiter, composite := token.(json.Delim) + if !composite { + return nil + } + switch delimiter { + case '{': + seen := make(map[string]struct{}) + for decoder.More() { + keyToken, err := decoder.Token() + key, ok := keyToken.(string) + if err != nil || !ok { + return invalidCompiled("object key") + } + if _, exists := seen[key]; exists { + return invalidCompiled("duplicate field") + } + seen[key] = struct{}{} + if err := consumeUniqueValue(decoder); err != nil { + return err + } + } + _, err = decoder.Token() + case '[': + for decoder.More() { + if err := consumeUniqueValue(decoder); err != nil { + return err + } + } + _, err = decoder.Token() + default: + return invalidCompiled("JSON delimiter") + } + if err != nil { + return invalidCompiled("malformed JSON") + } + return nil +} + +func clonePayload(payload compiledPayloadV1) compiledPayloadV1 { + payload.Plan = clonePlan(payload.Plan) + payload.Desired = cloneDesired(payload.Desired) + payload.DesiredResources = append([]DesiredResource(nil), payload.DesiredResources...) + payload.CleanupCapabilities = append([]isolation.CleanupCapability(nil), payload.CleanupCapabilities...) + payload.Intents = cloneIntents(payload.Intents) + payload.Risks = cloneRisks(payload.Risks) + return payload +} + +func clonePlan(plan domain.Plan) domain.Plan { + encoded, _ := json.Marshal(plan) + var result domain.Plan + _ = json.Unmarshal(encoded, &result) + return result +} + +func cloneDesired(value HarnessDesiredState) HarnessDesiredState { + value.Labels = cloneMap(value.Labels) + return value +} + +func cloneIntents(values []StepIntent) []StepIntent { + result := make([]StepIntent, len(values)) + for index, value := range values { + value.ResourceIDs = cloneStrings(value.ResourceIDs) + value.Dependencies = cloneStrings(value.Dependencies) + value.Preconditions = cloneStrings(value.Preconditions) + value.Verification = cloneStrings(value.Verification) + value.Transition = cloneTransition(value.Transition) + result[index] = value + } + return result +} + +func cloneTransition(value *HarnessTransition) *HarnessTransition { + if value == nil { + return nil + } + copy := *value + return © +} + +func cloneRisks(value RiskSummary) RiskSummary { + value.PermanentResiduals = append([]string(nil), value.PermanentResiduals...) + value.Risks = append([]string(nil), value.Risks...) + value.Rollback = append([]string(nil), value.Rollback...) + value.Compensation = append([]string(nil), value.Compensation...) + return value +} + +func invalidCompiled(field string) error { + return fmt.Errorf("%w: %s", ErrInvalidCompiledPlan, field) +} + +func equalCanonicalValue(left, right any) bool { + leftJSON, leftErr := json.Marshal(left) + rightJSON, rightErr := json.Marshal(right) + return leftErr == nil && rightErr == nil && bytes.Equal(leftJSON, rightJSON) +} diff --git a/internal/isolation/bootstrap/canonical_test.go b/internal/isolation/bootstrap/canonical_test.go new file mode 100644 index 0000000..ec63171 --- /dev/null +++ b/internal/isolation/bootstrap/canonical_test.go @@ -0,0 +1,225 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +package bootstrap + +import ( + "bytes" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/thelostorbital/ctrldb/internal/domain" + "github.com/thelostorbital/ctrldb/internal/isolation" +) + +func TestParseCompiledPlanRejectsMalformedOrNoncanonicalDocuments(t *testing.T) { + t.Parallel() + + valid := mustCanonical(t, mustCompile(t, validCompileRequest(t))) + tests := []struct { + name string + encoded []byte + }{ + {name: "empty", encoded: nil}, + {name: "malformed", encoded: []byte(`{"schemaVersion":`)}, + {name: "null", encoded: []byte(`null`)}, + {name: "trailing object", encoded: append(append([]byte{}, valid...), []byte(`{}`)...)}, + {name: "noncanonical whitespace", encoded: append([]byte("\n"), valid...)}, + {name: "duplicate top level", encoded: []byte(`{"schemaVersion":"a","schemaVersion":"b","payload":{},"documentSha256":"` + repeatedHex("a") + `"}`)}, + {name: "unknown top level", encoded: addTopLevelField(t, valid, "unknown", true)}, + {name: "wrong schema", encoded: rewriteWire(t, valid, func(wire *compiledWireV1) { wire.SchemaVersion = "wf-test-plan/v2" })}, + {name: "outer hash mismatch", encoded: rewriteWire(t, valid, func(wire *compiledWireV1) { wire.DocumentSHA256 = repeatedHex("f") })}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if _, err := ParseCompiledPlan(test.encoded); !errors.Is(err, ErrInvalidCompiledPlan) { + t.Fatalf("ParseCompiledPlan() error = %v; want ErrInvalidCompiledPlan", err) + } + }) + } +} + +func TestParseCompiledPlanRejectsRehashedSemanticTampering(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + tests := []struct { + name string + mutate func(*compiledPayloadV1) + }{ + {name: "prefix without reserved value", mutate: func(value *compiledPayloadV1) { value.Desired.NamePrefix = "other-test-" }}, + {name: "labels without prefix", mutate: func(value *compiledPayloadV1) { value.Desired.Labels["managed-by"] = "other" }}, + {name: "unsupported cleanup kind", mutate: func(value *compiledPayloadV1) { + value.CleanupCapabilities[0] = isolation.CleanupCapability("compute.snapshots") + }}, + {name: "reordered cleanup set", mutate: func(value *compiledPayloadV1) { + value.CleanupCapabilities[0], value.CleanupCapabilities[1] = value.CleanupCapabilities[1], value.CleanupCapabilities[0] + }}, + {name: "disposable permanent singleton", mutate: func(value *compiledPayloadV1) { value.DesiredResources[0].Permanence = DisposableRun }}, + {name: "unknown mutation intent", mutate: func(value *compiledPayloadV1) { value.Intents[0].Kind = IntentKind("test-instance-create") }}, + {name: "reordered steps", mutate: func(value *compiledPayloadV1) { + value.Intents[0], value.Intents[1] = value.Intents[1], value.Intents[0] + }}, + {name: "duplicate step ID", mutate: func(value *compiledPayloadV1) { value.Intents[1].StepID = value.Intents[0].StepID }}, + {name: "wrong identity", mutate: func(value *compiledPayloadV1) { value.Intents[0].ExecutingIdentity = domain.IdentityOperator }}, + {name: "missing permission", mutate: func(value *compiledPayloadV1) { value.Plan.Permissions = value.Plan.Permissions[1:] }}, + {name: "missing verification", mutate: func(value *compiledPayloadV1) { value.Intents[0].Verification = []string{} }}, + {name: "missing compensation", mutate: func(value *compiledPayloadV1) { value.Intents[0].Compensation = "" }}, + {name: "missing dependency", mutate: func(value *compiledPayloadV1) { value.Intents[1].Dependencies = []string{} }}, + {name: "zero timeout", mutate: func(value *compiledPayloadV1) { value.Intents[0].TimeoutSeconds = 0 }}, + {name: "forged open transition", mutate: func(value *compiledPayloadV1) { + value.Intents[len(value.Intents)-1].Transition.FromBootstrapPhase = "open" + }}, + {name: "cost cap bypass", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumCostMicros++ }}, + {name: "estimated cost mismatch", mutate: func(value *compiledPayloadV1) { value.Limits.EstimatedCostMicros++ }}, + {name: "machine shape bypass", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumGuestCPUs++ }}, + {name: "resource fingerprint", mutate: func(value *compiledPayloadV1) { value.DesiredResources[0].DesiredStateFingerprint = repeatedHex("c") }}, + {name: "plan envelope", mutate: func(value *compiledPayloadV1) { value.Binding.PlanHash = repeatedHex("d") }}, + {name: "observation envelope", mutate: func(value *compiledPayloadV1) { value.Binding.ObservationRevision = repeatedHex("e") }}, + {name: "risk summary", mutate: func(value *compiledPayloadV1) { value.Risks.ExpectedDowntimeSeconds = 1 }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + encoded := rehashedMutation(t, compiled, test.mutate) + if _, err := ParseCompiledPlan(encoded); !errors.Is(err, ErrInvalidCompiledPlan) { + t.Fatalf("ParseCompiledPlan() error = %v; want ErrInvalidCompiledPlan", err) + } + }) + } +} + +func TestCapacityChangesCannotRetainTheApprovedPlan(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + tests := []struct { + name string + mutate func(*compiledPayloadV1) + }{ + {name: "disk", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumDiskGiB++ }}, + {name: "instances", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumInstances++ }}, + {name: "lifetime", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumLifetimeSec++ }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + encoded := rehashedMutation(t, compiled, test.mutate) + if _, err := ParseCompiledPlan(encoded); !errors.Is(err, ErrInvalidCompiledPlan) { + t.Fatalf("ParseCompiledPlan(rehashed cap) error = %v; want ErrInvalidCompiledPlan", err) + } + }) + } +} + +func TestCompiledPlanHashBindsManifestObservationAndEnvelope(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + original := mustCanonical(t, compiled) + wire := decodeWire(t, original) + wire.Payload.Binding.ManifestHash = repeatedHex("c") + wire.Payload.Binding.ObservationRevision = repeatedHex("d") + wire.Payload.Binding.BindingSHA256 = "" + bindingHash, err := hashJSON(wire.Payload.Binding) + if err != nil { + t.Fatalf("hashJSON(binding) unexpected error: %v", err) + } + wire.Payload.Binding.BindingSHA256 = bindingHash + for index := range wire.Payload.Intents { + wire.Payload.Intents[index].EnvelopeBindingSHA256 = bindingHash + } + wire.DocumentSHA256, err = hashJSON(wire.Payload) + if err != nil { + t.Fatalf("hashJSON(payload) unexpected error: %v", err) + } + changed, err := json.Marshal(wire) + if err != nil { + t.Fatalf("json.Marshal(wire) unexpected error: %v", err) + } + if bytes.Equal(original, changed) || compiled.DocumentHash() == wire.DocumentSHA256 { + t.Fatal("manifest and observation substitutions did not change the approval artifact") + } +} + +func TestCompiledPlanContainsNoCommandOrSecretBearingFields(t *testing.T) { + t.Parallel() + + encoded := mustCanonical(t, mustCompile(t, validCompileRequest(t))) + for _, forbidden := range []string{`"argv"`, `"command"`, `"token"`, `"credential"`, `"mongoUri"`, `"password"`} { + if bytes.Contains(bytes.ToLower(encoded), bytes.ToLower([]byte(forbidden))) { + t.Fatalf("canonical plan contains forbidden provider or secret-bearing field %q", forbidden) + } + } + if !bytes.Contains(encoded, []byte(`"executingIdentity":"human"`)) || + !bytes.Contains(encoded, []byte(`"boundary":"audit-retention-lock"`)) { + t.Fatal("canonical plan omitted required identity or irreversible-boundary metadata") + } +} + +func rehashedMutation(t *testing.T, compiled CompiledPlan, mutate func(*compiledPayloadV1)) []byte { + t.Helper() + + payload := clonePayload(compiled.payload) + mutate(&payload) + hash, err := hashJSON(payload) + if err != nil { + t.Fatalf("hashJSON(payload) unexpected error: %v", err) + } + encoded, err := json.Marshal(compiledWireV1{SchemaVersion: CompiledPlanSchemaV1, Payload: payload, DocumentSHA256: hash}) + if err != nil { + t.Fatalf("json.Marshal(wire) unexpected error: %v", err) + } + return encoded +} + +func decodeWire(t *testing.T, encoded []byte) compiledWireV1 { + t.Helper() + + var wire compiledWireV1 + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatalf("json.Unmarshal(wire) unexpected error: %v", err) + } + return wire +} + +func rewriteWire(t *testing.T, encoded []byte, mutate func(*compiledWireV1)) []byte { + t.Helper() + + wire := decodeWire(t, encoded) + mutate(&wire) + result, err := json.Marshal(wire) + if err != nil { + t.Fatalf("json.Marshal(wire) unexpected error: %v", err) + } + return result +} + +func addTopLevelField(t *testing.T, encoded []byte, key string, value any) []byte { + t.Helper() + + var document map[string]any + if err := json.Unmarshal(encoded, &document); err != nil { + t.Fatalf("json.Unmarshal(document) unexpected error: %v", err) + } + document[key] = value + result, err := json.Marshal(document) + if err != nil { + t.Fatalf("json.Marshal(document) unexpected error: %v", err) + } + return result +} + +func TestCanonicalParserRejectsNestedDuplicateFields(t *testing.T) { + t.Parallel() + + valid := string(mustCanonical(t, mustCompile(t, validCompileRequest(t)))) + needle := `"workflowId":"WF-TEST-01"` + duplicated := strings.Replace(valid, needle, needle+`,`+needle, 1) + if duplicated == valid { + t.Fatal("test fixture did not insert a duplicate field") + } + if _, err := ParseCompiledPlan([]byte(duplicated)); !errors.Is(err, ErrInvalidCompiledPlan) { + t.Fatalf("ParseCompiledPlan(duplicate nested field) error = %v; want ErrInvalidCompiledPlan", err) + } +} diff --git a/internal/isolation/bootstrap/compile.go b/internal/isolation/bootstrap/compile.go new file mode 100644 index 0000000..3d4b5a6 --- /dev/null +++ b/internal/isolation/bootstrap/compile.go @@ -0,0 +1,621 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +package bootstrap + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "math" + "regexp" + "slices" + "strings" + "time" + + "github.com/thelostorbital/ctrldb/internal/domain" + "github.com/thelostorbital/ctrldb/internal/isolation" + "github.com/thelostorbital/ctrldb/internal/observation" + "github.com/thelostorbital/ctrldb/internal/policy" + "github.com/thelostorbital/ctrldb/internal/redact" +) + +const ( + PricingSchemaV1 = "ctrldb.ctrlboard.dev/pricing-evidence/v1" + minimumPlanValidity = 30 * time.Minute + maximumPricingAge = 31 * 24 * time.Hour + maximumExactMicros = int64(1 << 53) + iapFirewallName = "ctrldb-test-iap-ssh" + internalFirewallName = "ctrldb-test-internal" + testNodeTag = "ctrldb-test-node" + operatorRoleName = "ctrldbTestOperator" + destructiveRoleName = "ctrldbTestDestructive" + wipeScheduleUTC = "0 20 * * *" +) + +var ( + sha256Pattern = regexp.MustCompile(`^[0-9a-f]{64}$`) + planIDPattern = regexp.MustCompile(`^plan-[0-9a-f]{16}$`) +) + +var requiredAPIs = [...]string{ + "artifactregistry.googleapis.com", + "cloudresourcemanager.googleapis.com", + "cloudscheduler.googleapis.com", + "compute.googleapis.com", + "iam.googleapis.com", + "iamcredentials.googleapis.com", + "run.googleapis.com", + "secretmanager.googleapis.com", + "serviceusage.googleapis.com", + "storage.googleapis.com", +} + +// RequiredAPIs returns the closed provider service set that must be observed +// as enabled before WF-TEST-01 can be planned. +func RequiredAPIs() []string { + return append([]string(nil), requiredAPIs[:]...) +} + +type stepDefinition struct { + id string + kind IntentKind + identity domain.ExecutionIdentity + resourceIDs []string + dependencies []string + preconditions []string + verification []string + retry domain.RetryPolicy + timeoutSeconds int64 + cancelSafe bool + compensation string + ponr PointOfNoReturnClass + transition *HarnessTransition + permissions []string + summary string + success string + failure domain.FailureBehavior +} + +// Compile validates all immutable inputs and seals a deterministic WF-TEST-01 +// plan. It does not read files, run a process, contact a provider, or persist. +func Compile(request CompileRequest) (CompiledPlan, error) { + if err := validateCompileRequest(request); err != nil { + return CompiledPlan{}, err + } + + desired := desiredState(request) + desiredResources, err := buildDesiredResources(desired) + if err != nil { + return CompiledPlan{}, invalidCompile("desired resources") + } + if err := rejectTargetCollisions(request.Preflight, desiredResources); err != nil { + return CompiledPlan{}, err + } + + limits, err := buildRunLimits(request) + if err != nil { + return CompiledPlan{}, err + } + definitions := stepRegistry(desiredResources) + plan, contract, err := buildPlanValues( + request.PlanID, request.Configuration.Project(), request.Configuration.Environment(), request.Preflight.Account(), + request.CreatedAt, request.ExpiresAt, request.LocalPolicyHash, request.ApprovedPolicyHash, + request.Pricing, definitions, desiredResources, limits, + ) + if err != nil { + return CompiledPlan{}, err + } + binding, err := buildEnvelopeBinding(request, plan) + if err != nil { + return CompiledPlan{}, err + } + intents := buildIntents(definitions, binding.BindingSHA256) + payload := compiledPayloadV1{ + Plan: plan, Binding: binding, Desired: desired, DesiredResources: desiredResources, + Limits: limits, Pricing: request.Pricing, + CleanupCapabilities: isolation.InitialCleanupCapabilities(), Intents: intents, + Risks: buildRiskSummary(limits), + } + + return sealCompiled(payload, contract) +} + +func validateCompileRequest(request CompileRequest) error { + configuration := request.Configuration + preflight := request.Preflight + if !planIDPattern.MatchString(request.PlanID) { + return invalidCompile("plan ID") + } + if !validUTC(request.CreatedAt) || !validUTC(request.ExpiresAt) || + !request.ExpiresAt.After(request.CreatedAt) || + request.ExpiresAt.Sub(request.CreatedAt) < minimumPlanValidity { + return invalidCompile("plan time window") + } + if !sha256Pattern.MatchString(request.LocalPolicyHash) || + request.LocalPolicyHash != request.ApprovedPolicyHash { + return blocked("approved policy hash") + } + if configuration.ManifestHash() == "" || configuration.Environment() == "" || + configuration.Project() == "" || configuration.Region() == "" || configuration.Zone() == "" { + return invalidCompile("harness configuration") + } + if preflight.Project() != configuration.Project() || preflight.Region() != configuration.Region() || + preflight.Zone() != configuration.Zone() || preflight.Account() == "" { + return blocked("provider context") + } + if preflight.GcloudVersion() != observation.SupportedGcloudVersion || + preflight.CompletenessPolicy() != observation.GcloudCompletenessPolicy || !preflight.Exhaustive() || + !slices.Equal(preflight.Schemas(), observation.RequiredSchemas()) { + return blocked("observation provenance") + } + if !preflight.FreshAt(request.CreatedAt) { + return blocked("stale observation") + } + overlaps, err := preflight.CIDROverlapsAt(configuration.CIDR(), request.CreatedAt) + if err != nil || overlaps { + return blocked("CIDR overlap") + } + publicRules, err := preflight.PublicMongoDBIngressAt(request.CreatedAt) + if err != nil || len(publicRules) != 0 { + return blocked("public MongoDB ingress") + } + for _, service := range RequiredAPIs() { + if preflight.APIState(service) != observation.APIEnabled { + return blocked("required API state") + } + } + if err := validatePricing(request); err != nil { + return err + } + return nil +} + +func validatePricing(request CompileRequest) error { + price := request.Pricing + if price.Schema != PricingSchemaV1 || !sha256Pattern.MatchString(price.Revision) || + price.MachineType != request.Configuration.Caps().MaxMachineType() || + price.GuestCPUs <= 0 || price.MemoryMiB <= 0 || price.EstimatedRunMicros < 0 || + price.EstimatedRunMicros > maximumExactMicros || !validUTC(price.ObservedAt) || + !validUTC(price.ValidUntil) || !price.ObservedAt.Before(price.ValidUntil) || + request.CreatedAt.Before(price.ObservedAt) || !request.CreatedAt.Before(price.ValidUntil) { + return invalidCompile("pricing evidence") + } + parsedDate, err := time.Parse(time.DateOnly, price.PriceTableDate) + if err != nil || parsedDate.After(request.CreatedAt) || request.CreatedAt.Sub(parsedDate) > maximumPricingAge { + return invalidCompile("price table date") + } + if price.EstimatedRunMicros > request.Configuration.Caps().MaxEstimatedCostMicros() { + return blocked("run cost cap") + } + matches := 0 + for _, machine := range request.Preflight.MachineTypes() { + if machine.Name == price.MachineType { + matches++ + if machine.Zone != request.Configuration.Zone() || machine.Deprecated || + machine.GuestCPUs != price.GuestCPUs || machine.MemoryMiB != price.MemoryMiB { + return blocked("machine capability") + } + } + } + if matches != 1 { + return blocked("machine capability") + } + return nil +} + +func buildRunLimits(request CompileRequest) (RunLimits, error) { + caps := request.Configuration.Caps() + lifetime := caps.MaxLifetime() + if lifetime <= 0 || lifetime%time.Second != 0 || caps.MaxDiskGiB() <= 0 || caps.MaxInstances() <= 0 || + caps.MaxEstimatedCostMicros() < 0 || caps.MaxEstimatedCostMicros() > maximumExactMicros || + int64(caps.MaxInstances()) <= 0 { + return RunLimits{}, invalidCompile("run limits") + } + return RunLimits{ + MaximumMachineType: caps.MaxMachineType(), MaximumGuestCPUs: request.Pricing.GuestCPUs, + MaximumMemoryMiB: request.Pricing.MemoryMiB, MaximumDiskGiB: caps.MaxDiskGiB(), + MaximumInstances: int64(caps.MaxInstances()), MaximumLifetimeSec: int64(lifetime / time.Second), + MaximumCostMicros: caps.MaxEstimatedCostMicros(), EstimatedCostMicros: request.Pricing.EstimatedRunMicros, + }, nil +} + +func desiredState(request CompileRequest) HarnessDesiredState { + configuration := request.Configuration + return HarnessDesiredState{ + Account: request.Preflight.Account(), Project: configuration.Project(), Region: configuration.Region(), Zone: configuration.Zone(), + CIDR: configuration.CIDR(), NamePrefix: configuration.NamePrefix(), Labels: configuration.Labels(), ControlBucket: configuration.ControlBucket(), + AuditBucket: configuration.AuditBucket(), VPC: configuration.VPC(), Subnet: configuration.Subnet(), + Router: configuration.Router(), NAT: configuration.NAT(), IAPFirewall: iapFirewallName, + InternalFirewall: internalFirewallName, NodeTag: testNodeTag, + OperatorPrincipal: configuration.OperatorPrincipal(), DestructivePrincipal: configuration.DestructivePrincipal(), + VMPrincipal: configuration.VMPrincipal(), WipePrincipal: configuration.WipeServiceAccount(), + CIPrincipal: configuration.CIPrincipal(), OperatorRole: operatorRoleName, + DestructiveRole: destructiveRoleName, WipeRunJob: configuration.WipeRunJob(), + WipeSchedulerJob: configuration.WipeSchedulerJob(), WipeScheduleUTC: wipeScheduleUTC, + ImageDigest: configuration.ImageDigest(), + } +} + +func buildDesiredResources(desired HarnessDesiredState) ([]DesiredResource, error) { + project, region := desired.Project, desired.Region + resources := []DesiredResource{ + resource("audit-bucket", ResourceBucket, desired.AuditBucket, project, region, + provider(project, "global", string(ResourceBucket), desired.AuditBucket), ""), + resource("control-bucket", ResourceBucket, desired.ControlBucket, project, region, + provider(project, "global", string(ResourceBucket), desired.ControlBucket), ""), + resource("test-network", ResourceNetwork, desired.VPC, project, "global", + provider(project, "global", "networks", desired.VPC), ""), + resource("test-subnet", ResourceSubnetwork, desired.Subnet, project, region, + provider(project, "regions", region, "subnetworks", desired.Subnet), provider(project, "global", "networks", desired.VPC)), + resource("test-router", ResourceRouter, desired.Router, project, region, + provider(project, "regions", region, "routers", desired.Router), provider(project, "global", "networks", desired.VPC)), + resource("test-nat", ResourceNAT, desired.NAT, project, region, + provider(project, "regions", region, "routers", desired.Router, "nats", desired.NAT), provider(project, "regions", region, "routers", desired.Router)), + resource("test-iap-firewall", ResourceFirewall, desired.IAPFirewall, project, "global", + provider(project, "global", "firewalls", desired.IAPFirewall), provider(project, "global", "networks", desired.VPC)), + resource("test-internal-firewall", ResourceFirewall, desired.InternalFirewall, project, "global", + provider(project, "global", "firewalls", desired.InternalFirewall), provider(project, "global", "networks", desired.VPC)), + resource("test-operator-sa", ResourceServiceAccount, desired.OperatorPrincipal, project, "global", + provider(project, "serviceAccounts", desired.OperatorPrincipal), ""), + resource("test-destructive-sa", ResourceServiceAccount, desired.DestructivePrincipal, project, "global", + provider(project, "serviceAccounts", desired.DestructivePrincipal), ""), + resource("test-vm-sa", ResourceServiceAccount, desired.VMPrincipal, project, "global", + provider(project, "serviceAccounts", desired.VMPrincipal), ""), + resource("test-wipe-sa", ResourceServiceAccount, desired.WipePrincipal, project, "global", + provider(project, "serviceAccounts", desired.WipePrincipal), ""), + resource("test-operator-role", ResourceCustomRole, desired.OperatorRole, project, "global", + provider(project, "roles", desired.OperatorRole), ""), + resource("test-destructive-role", ResourceCustomRole, desired.DestructiveRole, project, "global", + provider(project, "roles", desired.DestructiveRole), ""), + resource("test-wipe-job", ResourceRunJob, desired.WipeRunJob, project, region, + provider(project, "regions", region, string(ResourceRunJob), desired.WipeRunJob), ""), + resource("test-wipe-scheduler", ResourceSchedulerJob, desired.WipeSchedulerJob, project, region, + provider(project, "locations", region, "jobs", desired.WipeSchedulerJob), provider(project, "regions", region, string(ResourceRunJob), desired.WipeRunJob)), + } + for index := range resources { + descriptor := desiredResourceDescriptor(resources[index], desired) + fingerprint, err := hashJSON(descriptor) + if err != nil { + return nil, err + } + resources[index].DesiredStateFingerprint = fingerprint + } + return resources, nil +} + +func resource(id string, kind ResourceKind, name, project, location, providerID, parent string) DesiredResource { + return DesiredResource{ID: id, Kind: kind, Name: name, Project: project, Location: location, + ProviderID: providerID, ParentProviderID: parent, Permanence: PermanentSingleton} +} + +type resourceDescriptor struct { + Resource DesiredResource `json:"resource"` + State resourceStateV1 `json:"state"` +} + +type resourceStateV1 struct { + Mode string `json:"mode"` + CIDR string `json:"cidr"` + Labels map[string]string `json:"labels"` + Source string `json:"source"` + Targets []string `json:"targets"` + Protocol string `json:"protocol"` + Port int `json:"port"` + ScheduleUTC string `json:"scheduleUtc"` + ImageDigest string `json:"imageDigest"` + Principal string `json:"principal"` + ControlPrefix string `json:"controlPrefix"` +} + +func desiredResourceDescriptor(resource DesiredResource, desired HarnessDesiredState) resourceDescriptor { + copy := resource + copy.DesiredStateFingerprint = "" + state := resourceStateV1{Labels: map[string]string{}, Targets: []string{}} + switch resource.ID { + case "audit-bucket": + state.Mode = "ubla-pap-standard-versioned-retention-365d-locked-archive-after-365d" + case "control-bucket": + state.Mode = "ubla-pap-standard-versioned-soft-delete-30d" + case "test-network": + state.Mode = "custom-subnet" + case "test-subnet": + state.Mode, state.CIDR = "private-google-access", desired.CIDR + case "test-router": + state.Mode = "custom-network-router" + case "test-nat": + state.Mode = "auto-ip-all-subnet-ranges" + case "test-iap-firewall": + state.Mode, state.Source, state.Targets, state.Protocol, state.Port = "ingress", "35.235.240.0/20", []string{desired.NodeTag}, "tcp", 22 + case "test-internal-firewall": + state.Mode, state.Source, state.Targets, state.Protocol, state.Port = "ingress", desired.NodeTag, []string{desired.NodeTag}, "tcp", 27017 + case "test-operator-sa", "test-destructive-sa", "test-vm-sa", "test-wipe-sa": + state.Mode, state.Principal = "no-keys", resource.Name + case "test-operator-role": + state.Mode = "closed-test-operator-capability" + case "test-destructive-role": + state.Mode = "closed-test-destructive-capability" + case "test-wipe-job": + state.Mode, state.ImageDigest, state.Principal = "test-wipe", desired.ImageDigest, desired.WipePrincipal + case "test-wipe-scheduler": + state.Mode, state.ScheduleUTC, state.Principal = "run-jobs-v2-oauth", desired.WipeScheduleUTC, desired.WipePrincipal + } + state.ControlPrefix = "test/" + return resourceDescriptor{Resource: copy, State: state} +} + +func rejectTargetCollisions(preflight observation.HarnessPreflight, desired []DesiredResource) error { + for _, observed := range preflight.Resources() { + for _, target := range desired { + if string(observed.Kind) != string(target.Kind) || observed.Name != target.Name || observed.Project != target.Project { + continue + } + if target.Kind == ResourceBucket { + return blocked("bucket collision") + } + if observed.Location == target.Location && canonicalProviderID(observed.ProviderID) == target.ProviderID { + return blocked("desired target collision") + } + } + } + return nil +} + +func canonicalProviderID(value string) string { + parts := strings.Split(value, "/") + for index := range parts { + if parts[index] == "projects" && index+1 < len(parts) { + return strings.Join(parts[index:], "/") + } + } + return value +} + +func provider(project string, parts ...string) string { + return strings.Join(append([]string{"projects", project}, parts...), "/") +} + +func buildPlanValues( + planID, projectID, environment, account string, + createdAt, expiresAt time.Time, + localPolicyHash, approvedPolicyHash string, + pricing PricingEvidence, + definitions []stepDefinition, + desiredResources []DesiredResource, + limits RunLimits, +) (domain.Plan, domain.ExecutionContract, error) { + planResources := make([]domain.PlanResource, len(definitions)) + steps := make([]domain.PlanStep, len(definitions)) + permissions := make([]domain.PlanPermission, 0) + contractSteps := make([]domain.ExecutionStepContract, len(definitions)) + for index, definition := range definitions { + targets, ok := desiredResourcesByID(desiredResources, definition.resourceIDs) + if !ok { + return domain.Plan{}, domain.ExecutionContract{}, invalidCompile("step resource binding") + } + fingerprint, err := hashJSON(struct { + ID string `json:"id"` + Kind IntentKind `json:"kind"` + Resources []DesiredResource `json:"resources"` + Limits RunLimits `json:"limits"` + }{definition.id, definition.kind, targets, limits}) + if err != nil { + return domain.Plan{}, domain.ExecutionContract{}, invalidCompile("step fingerprint") + } + planResource := domain.PlanResource{Kind: "bootstrap-action", Scope: "projects/" + projectID + "/global", Name: definition.id, Fingerprint: fingerprint} + planResources[index] = planResource + steps[index] = domain.PlanStep{ + ID: definition.id, Executor: "typed-adapter", ExecutingIdentity: definition.identity, + CommandRedacted: redact.Sanitize(definition.summary), Idempotent: true, Retry: definition.retry, + CancelSafe: definition.cancelSafe, TimeoutSeconds: definition.timeoutSeconds, + SuccessCondition: redact.Sanitize(definition.success), FailureBehavior: definition.failure, + Targets: []domain.PlanResource{planResource}, + } + for _, permission := range definition.permissions { + permissions = append(permissions, domain.PlanPermission{StepID: definition.id, Identity: definition.identity, + Permission: permission, Resource: planResource, Granted: true}) + } + effect := domain.StepEffectMutation + if definition.kind == IntentIsolationGate { + effect = domain.StepEffectRead + } + contractSteps[index] = domain.ExecutionStepContract{ + ID: definition.id, Executor: "typed-adapter", ExecutingIdentity: definition.identity, + CommandSummary: redact.Sanitize(definition.summary), Effect: effect, + MinimumApproval: domain.ApprovalSecuritySensitive, TargetKinds: []string{"bootstrap-action"}, + RequiredPermissions: append([]string(nil), definition.permissions...), Idempotent: true, + Retry: definition.retry, CancelSafe: definition.cancelSafe, TimeoutSeconds: definition.timeoutSeconds, + SuccessCondition: redact.Sanitize(definition.success), FailureBehavior: definition.failure, + } + } + contract, err := domain.NewExecutionContract(WorkflowID, "audit-retention-lock", definitions[0].id, + domain.PointOfNoReturnMutationObserved, contractSteps) + if err != nil { + return domain.Plan{}, domain.ExecutionContract{}, fmt.Errorf("%w: execution contract", ErrInvalidCompileRequest) + } + ceilingUSD, ok := microsToUSD(limits.MaximumCostMicros) + if !ok { + return domain.Plan{}, domain.ExecutionContract{}, invalidCompile("cost ceiling") + } + estimateUSD, ok := microsToUSD(limits.EstimatedCostMicros) + if !ok { + return domain.Plan{}, domain.ExecutionContract{}, invalidCompile("cost estimate") + } + plan := domain.Plan{ + PlanID: planID, WorkflowID: WorkflowID, ProjectID: projectID, + Environment: environment, EnvironmentClass: domain.EnvironmentDisposable, + Principal: account, CreatedAt: createdAt, ExpiresAt: expiresAt, + ApprovalClass: domain.ApprovalSecuritySensitive, CoolingOffSeconds: 0, + Identity: domain.DefaultIdentityPlan(), PolicyHash: domain.PlanPolicyHash{Local: localPolicyHash, Approved: approvedPolicyHash, Match: true}, + Resources: planResources, Preconditions: planPreconditions(), Permissions: permissions, Steps: steps, + Cost: domain.PlanCost{RunRate: domain.PlanCostRate{AmountUSD: estimateUSD, Period: "run"}, + Items: []domain.PlanCostItem{{Resource: "wf-test-01", Kind: "test-harness", AmountUSD: estimateUSD}}, + Source: domain.CostSourceListPriceTable, PriceTableDate: pricing.PriceTableDate, + Stale: false, Assumptions: []redact.Text{redact.Sanitize("estimate uses the explicitly selected region, machine shape, disk, count, and lifetime caps")}, + Unpriced: []string{}, Budget: domain.PlanCostBudget{State: domain.BudgetOK, CeilingUSD: &ceilingUSD}}, + Downtime: domain.PlanDowntime{ExpectedSeconds: 0, Kind: "none"}, Exposure: domain.ExposureNone, + Protection: []redact.Text{redact.Sanitize("production resources remain outside the reserved disposable namespace"), redact.Sanitize("permanent control resources are excluded from disposable cleanup")}, + Rollback: domain.PlanRollback{Boundary: "audit-retention-lock", Assets: []domain.PlanRecoveryAsset{}}, + PointOfNoReturn: definitions[0].id, PointOfNoReturnTrigger: domain.PointOfNoReturnMutationObserved, + Verification: []redact.Text{redact.Sanitize("every desired provider object is re-observed at exact desired state"), redact.Sanitize("T8 completes every TEST-ISO proof before opening test admission")}, + } + sealed, err := policy.SealPlan(plan) + if err != nil { + return domain.Plan{}, domain.ExecutionContract{}, fmt.Errorf("%w: PlanV1 validation", ErrInvalidCompileRequest) + } + return sealed, contract, nil +} + +func desiredResourcesByID(resources []DesiredResource, identifiers []string) ([]DesiredResource, bool) { + result := make([]DesiredResource, len(identifiers)) + for index, identifier := range identifiers { + matches := 0 + for _, resource := range resources { + if resource.ID == identifier { + matches++ + result[index] = resource + } + } + if matches != 1 { + return nil, false + } + } + return result, true +} + +func planPreconditions() []domain.PlanPrecondition { + values := []struct{ id, detail string }{ + {"manifest-bound", "validated disposable manifest is hash-bound"}, + {"provider-context-bound", "account, project, region, and zone match"}, + {"observation-fresh", "provider evidence is current"}, + {"schemas-complete", "the closed discovery catalogue is exhaustive"}, + {"cidr-clear", "the selected private CIDR does not overlap"}, + {"api-set-enabled", "every required Google Cloud API is enabled"}, + {"no-public-mongodb", "no contracted classic rule exposes MongoDB internet-wide"}, + {"machine-cap-resolved", "the selected cap machine has exact observed numeric dimensions"}, + {"cost-cap-respected", "the upward-rounded integer micro-USD estimate is within the manifest cap"}, + {"target-identities-absent", "no exact desired provider identity collides"}, + {"bucket-conflict-guarded", "M1-05 must treat create-time global bucket-name conflict as a blocking outcome"}, + {"capability-set-closed", "mutation and cleanup are limited to the recorded three-kind capability set"}, + {"pre-t8-admission", "only this approved WF-TEST-01 envelope may mutate before T8"}, + } + result := make([]domain.PlanPrecondition, len(values)) + for index, value := range values { + result[index] = domain.PlanPrecondition{ID: value.id, OK: true, Detail: redact.Sanitize(value.detail)} + } + return result +} + +func buildEnvelopeBinding(request CompileRequest, plan domain.Plan) (EnvelopeBinding, error) { + binding := EnvelopeBinding{WorkflowID: WorkflowID, PlanID: plan.PlanID, PlanHash: plan.PlanHash, Account: request.Preflight.Account(), + ManifestHash: request.Configuration.ManifestHash(), ObservationRevision: request.Preflight.Revision(), + ObservedAt: request.Preflight.ObservedAt(), ValidUntil: request.Preflight.ValidUntil()} + digest, err := hashJSON(binding) + if err != nil { + return EnvelopeBinding{}, invalidCompile("envelope binding") + } + binding.BindingSHA256 = digest + return binding, nil +} + +func buildIntents(definitions []stepDefinition, binding string) []StepIntent { + result := make([]StepIntent, len(definitions)) + for index, definition := range definitions { + result[index] = StepIntent{StepID: definition.id, Kind: definition.kind, EnvelopeBindingSHA256: binding, + ExecutingIdentity: definition.identity, ResourceIDs: cloneStrings(definition.resourceIDs), + Dependencies: cloneStrings(definition.dependencies), Preconditions: cloneStrings(definition.preconditions), + Verification: cloneStrings(definition.verification), Retry: definition.retry, + TimeoutSeconds: definition.timeoutSeconds, CancelSafe: definition.cancelSafe, + Compensation: definition.compensation, PointOfNoReturn: definition.ponr, + Transition: cloneTransition(definition.transition)} + } + return result +} + +func buildRiskSummary(limits RunLimits) RiskSummary { + return RiskSummary{ExpectedCostCeilingMicros: limits.MaximumCostMicros, EstimatedRunCostMicros: limits.EstimatedCostMicros, + ExpectedDowntimeSeconds: 0, ProductionExposure: "none", + PermanentResiduals: []string{"audit bucket with locked 365-day retention", "versioned control bucket", "test harness singletons"}, + Risks: []string{"audit retention lock is irreversible", "partial bootstrap must resume only from the same envelope hash", "Cloud NAT and retained audit data continue to incur bounded cost"}, + Rollback: []string{"before the audit retention lock, remove only resources created by this operation", "after the lock, preserve both control buckets and converge or pause"}, + Compensation: []string{"remove only in-operation reversible bindings and harness objects", "never age-wipe permanent singletons"}, + PointOfNoReturn: "the irreversible boundary begins when K1 observes the audit bucket 365-day retention lock mutation"} +} + +func stepRegistry(resources []DesiredResource) []stepDefinition { + allPreconditions := []string{"manifest-bound", "provider-context-bound", "observation-fresh", "schemas-complete", "cidr-clear", "api-set-enabled", "no-public-mongodb", "machine-cap-resolved", "cost-cap-respected", "target-identities-absent", "bucket-conflict-guarded", "capability-set-closed", "pre-t8-admission"} + retry3210 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 2, MaxBackoffSeconds: 10} + retry3520 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 5, MaxBackoffSeconds: 20} + retry5220 := domain.RetryPolicy{MaxAttempts: 5, InitialBackoffSeconds: 2, MaxBackoffSeconds: 20} + retry3530 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 5, MaxBackoffSeconds: 30} + once := domain.RetryPolicy{MaxAttempts: 1} + return []stepDefinition{ + {id: "k1-audit-bootstrap", kind: IntentAuditBootstrap, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket"}, preconditions: allPreconditions, verification: []string{"envelope hash and server generation match", "retention is locked for 365 days", "archive lifecycle has no delete rule"}, retry: retry3210, timeoutSeconds: 120, cancelSafe: false, compensation: "before retention lock remove only an audit bucket created by this operation; after lock pause and preserve", ponr: PONRAuditRetentionLock, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update", "storage.objects.create", "storage.objects.get"}, summary: "create and verify the audit bootstrap handoff, then lock retention", success: "the exact envelope is durable and the compliant audit retention policy is locked", failure: domain.FailurePause}, + {id: "k2-control-bucket", kind: IntentControlBucket, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k1-audit-bootstrap"}, preconditions: allPreconditions, verification: []string{"control bucket desired state matches exactly"}, retry: retry3210, timeoutSeconds: 60, cancelSafe: true, compensation: "remove only a preexisting-empty control bucket created by this operation before durable state is written", ponr: PONRReversible, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update"}, summary: "create the permanent versioned control bucket", success: "the control bucket has exact UBLA, PAP, versioning, and soft-delete state", failure: domain.FailureRollback}, + {id: "k3-bucket-iam", kind: IntentBucketIAM, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket", "control-bucket"}, dependencies: []string{"k2-control-bucket"}, preconditions: allPreconditions, verification: []string{"bucket IAM policies equal the closed rendered policy"}, retry: retry5220, timeoutSeconds: 120, cancelSafe: true, compensation: "remove only IAM bindings added by this operation", ponr: PONRReversible, permissions: []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}, summary: "apply exact control and audit bucket IAM", success: "bucket IAM equals the closed prefix-scoped policy", failure: domain.FailureRollback}, + {id: "k4-seed-control", kind: IntentSeedControl, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k3-bucket-iam"}, preconditions: allPreconditions, verification: []string{"seed objects exist and preexisting generations are unchanged"}, retry: retry3210, timeoutSeconds: 30, cancelSafe: true, compensation: "preserve all create-only seed objects and converge from their exact contents", ponr: PONRReversible, permissions: []string{"storage.objects.create", "storage.objects.get"}, summary: "create the exact control-store seed objects", success: "all required create-only seed objects are present", failure: domain.FailurePause}, + {id: "k5-lock-round-trip", kind: IntentLockRoundTrip, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k4-seed-control"}, preconditions: allPreconditions, verification: []string{"generation-preconditioned acquire and release succeeds"}, retry: once, timeoutSeconds: 30, cancelSafe: true, compensation: "release only the lock generation acquired by this operation", ponr: PONRReversible, permissions: []string{"storage.objects.create", "storage.objects.get", "storage.objects.update"}, summary: "prove the control-store lock round trip", success: "one generation-bound lock is acquired and released", failure: domain.FailurePause}, + {id: "t1-network", kind: IntentNetwork, identity: domain.IdentityHuman, resourceIDs: []string{"test-network"}, dependencies: []string{"k5-lock-round-trip"}, preconditions: allPreconditions, verification: []string{"network identity and custom-subnet state match"}, retry: retry3520, timeoutSeconds: 60, cancelSafe: true, compensation: "explicit teardown may remove only the network recorded as created by this operation", ponr: PONRReversible, permissions: []string{"compute.networks.create", "compute.networks.get"}, summary: "create the dedicated custom-mode test network", success: "the exact permanent test network exists", failure: domain.FailureRollback}, + {id: "t2-subnet", kind: IntentSubnet, identity: domain.IdentityHuman, resourceIDs: []string{"test-subnet"}, dependencies: []string{"t1-network"}, preconditions: allPreconditions, verification: []string{"subnet range, region, network, and private Google access match"}, retry: retry3520, timeoutSeconds: 60, cancelSafe: true, compensation: "explicit teardown may remove only the subnet recorded as created by this operation", ponr: PONRReversible, permissions: []string{"compute.subnetworks.create", "compute.subnetworks.get"}, summary: "create the explicit non-overlapping test subnet", success: "the exact regional test subnet exists", failure: domain.FailureRollback}, + {id: "t3-nat", kind: IntentNAT, identity: domain.IdentityHuman, resourceIDs: []string{"test-router", "test-nat"}, dependencies: []string{"t2-subnet"}, preconditions: allPreconditions, verification: []string{"router and NAT identities and operational state match"}, retry: retry3520, timeoutSeconds: 60, cancelSafe: true, compensation: "explicit teardown removes only the recorded NAT and router in dependency order", ponr: PONRReversible, permissions: []string{"compute.routers.create", "compute.routers.get", "compute.routers.update"}, summary: "create the test router and auto-allocated Cloud NAT", success: "the exact regional router and operational NAT exist", failure: domain.FailureRollback}, + {id: "t4-firewall", kind: IntentFirewall, identity: domain.IdentityHuman, resourceIDs: []string{"test-iap-firewall", "test-internal-firewall"}, dependencies: []string{"t3-nat"}, preconditions: allPreconditions, verification: []string{"IAP SSH and node-internal MongoDB rules match exactly", "no public or non-test source and target is present"}, retry: retry3520, timeoutSeconds: 60, cancelSafe: true, compensation: "delete only firewall rules recorded as created by this operation", ponr: PONRReversible, permissions: []string{"compute.firewalls.create", "compute.firewalls.get"}, summary: "create the two closed test firewall rules", success: "only the exact IAP SSH and test-node MongoDB rules exist", failure: domain.FailureRollback}, + {id: "t5-identities", kind: IntentIdentities, identity: domain.IdentityHuman, resourceIDs: []string{"test-operator-sa", "test-destructive-sa", "test-vm-sa", "test-wipe-sa", "test-operator-role", "test-destructive-role"}, dependencies: []string{"t4-firewall"}, preconditions: allPreconditions, verification: []string{"service accounts have no keys", "role and conditional binding fingerprints match", "expected allows and denies are proven exactly"}, retry: retry5220, timeoutSeconds: 180, cancelSafe: true, compensation: "remove only roles, bindings, and service accounts created by this operation", ponr: PONRReversible, permissions: []string{"iam.roles.create", "iam.roles.get", "iam.roles.update", "iam.serviceAccounts.create", "iam.serviceAccounts.get", "iam.serviceAccounts.setIamPolicy", "resourcemanager.projects.getIamPolicy", "resourcemanager.projects.setIamPolicy"}, summary: "create test identities, closed roles, and conditional bindings", success: "identity desired state and exact permission matrix are proven", failure: domain.FailureRollback}, + {id: "t6-control-prefix", kind: IntentControlPrefix, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"t5-identities"}, preconditions: allPreconditions, verification: []string{"only the test control prefix bindings are present"}, retry: retry5220, timeoutSeconds: 60, cancelSafe: true, compensation: "remove only prefix bindings added by this operation", ponr: PONRReversible, permissions: []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}, summary: "bind test identities to the control-store test prefix", success: "the exact test-prefix IAM bindings are present", failure: domain.FailureRollback}, + {id: "t7-nightly-wipe", kind: IntentNightlyWipe, identity: domain.IdentityHuman, resourceIDs: []string{"test-wipe-job", "test-wipe-scheduler"}, dependencies: []string{"t6-control-prefix"}, preconditions: allPreconditions, verification: []string{"wipe job image and identity match", "scheduler uses the exact Run Jobs v2 OAuth target", "first run reports zero deletions"}, retry: retry3530, timeoutSeconds: 180, cancelSafe: true, compensation: "delete only the job and scheduler recorded as created by this operation", ponr: PONRReversible, permissions: []string{"cloudscheduler.jobs.create", "cloudscheduler.jobs.get", "iam.serviceAccounts.actAs", "run.jobs.create", "run.jobs.get"}, summary: "create and dry-run the immutable nightly wipe job", success: "the pinned wipe job and UTC scheduler exist and the first run deletes nothing", failure: domain.FailureRollback}, + {id: "t8-isolation-gate", kind: IntentIsolationGate, identity: domain.IdentityHuman, resourceIDs: desiredResourceIDs(resources), dependencies: []string{"t7-nightly-wipe"}, preconditions: allPreconditions, verification: []string{"every TEST-ISO proof passes", "harness fingerprints and cleanup capabilities match", "pending and unusable may transition to open and usable"}, retry: once, timeoutSeconds: 900, cancelSafe: true, compensation: "on failure retain pending and unusable state and keep TEST-I, TEST-D, and unrelated mutation blocked", ponr: PONRVerificationOnly, transition: &HarnessTransition{FromBootstrapPhase: "pending", FromTestUsability: "unusable", ToBootstrapPhase: "open", ToTestUsability: "usable"}, permissions: []string{"cloudscheduler.jobs.get", "compute.firewalls.get", "compute.networks.get", "iam.serviceAccounts.get", "resourcemanager.projects.getIamPolicy", "run.jobs.get"}, summary: "run the complete TEST-ISO gate without opening admission early", success: "every T8 proof passes and the proposed state transition is eligible to persist", failure: domain.FailurePause}, + } +} + +func desiredResourceIDs(resources []DesiredResource) []string { + result := make([]string, len(resources)) + for index, resource := range resources { + result[index] = resource.ID + } + return result +} + +func hashJSON(value any) (string, error) { + encoded, err := json.Marshal(value) + if err != nil { + return "", err + } + digest := sha256.Sum256(encoded) + return hex.EncodeToString(digest[:]), nil +} + +func microsToUSD(value int64) (float64, bool) { + if value < 0 || value > maximumExactMicros { + return 0, false + } + amount := float64(value) / 1_000_000 + return amount, int64(math.Round(amount*1_000_000)) == value +} + +func validUTC(value time.Time) bool { + if value.IsZero() { + return false + } + _, offset := value.Zone() + return offset == 0 +} + +func invalidCompile(field string) error { + return fmt.Errorf("%w: %s", ErrInvalidCompileRequest, field) +} + +func blocked(gate string) error { + return fmt.Errorf("%w: %s", ErrPlanBlocked, gate) +} + +func cloneMap(input map[string]string) map[string]string { + result := make(map[string]string, len(input)) + for key, value := range input { + result[key] = value + } + return result +} + +func cloneStrings(input []string) []string { + return append([]string{}, input...) +} diff --git a/internal/isolation/bootstrap/compile_test.go b/internal/isolation/bootstrap/compile_test.go new file mode 100644 index 0000000..314e93d --- /dev/null +++ b/internal/isolation/bootstrap/compile_test.go @@ -0,0 +1,389 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +package bootstrap + +import ( + "bytes" + "errors" + "reflect" + "slices" + "strings" + "testing" + "time" + + "github.com/thelostorbital/ctrldb/internal/config" + "github.com/thelostorbital/ctrldb/internal/domain" + "github.com/thelostorbital/ctrldb/internal/isolation" + "github.com/thelostorbital/ctrldb/internal/observation" +) + +func TestCompileProducesCompleteWFTestPlan(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + wantSteps := []string{ + "k1-audit-bootstrap", "k2-control-bucket", "k3-bucket-iam", "k4-seed-control", "k5-lock-round-trip", + "t1-network", "t2-subnet", "t3-nat", "t4-firewall", "t5-identities", "t6-control-prefix", + "t7-nightly-wipe", "t8-isolation-gate", + } + intents := compiled.Intents() + if len(intents) != len(wantSteps) { + t.Fatalf("len(Intents()) = %d; want %d", len(intents), len(wantSteps)) + } + for index, want := range wantSteps { + if intents[index].StepID != want { + t.Errorf("Intents()[%d].StepID = %q; want %q", index, intents[index].StepID, want) + } + if intents[index].ExecutingIdentity != domain.IdentityHuman || intents[index].TimeoutSeconds <= 0 || + len(intents[index].Preconditions) == 0 || len(intents[index].Verification) == 0 || + intents[index].Compensation == "" { + t.Errorf("Intents()[%d] lacks required execution metadata: %#v", index, intents[index]) + } + } + last := intents[len(intents)-1] + if last.Kind != IntentIsolationGate || last.Transition == nil || + last.Transition.FromBootstrapPhase != "pending" || last.Transition.FromTestUsability != "unusable" || + last.Transition.ToBootstrapPhase != "open" || last.Transition.ToTestUsability != "usable" { + t.Fatalf("T8 transition = %#v; want pending/unusable to open/usable", last.Transition) + } + steps := compiled.ExecutionContract().Steps() + if steps[len(steps)-1].Effect != domain.StepEffectRead { + t.Errorf("T8 effect = %q; want read", steps[len(steps)-1].Effect) + } + for index := 0; index < len(steps)-1; index++ { + if steps[index].Effect != domain.StepEffectMutation { + t.Errorf("step %q effect = %q; want mutation", steps[index].ID, steps[index].Effect) + } + } + + desired := compiled.DesiredState() + if desired.NamePrefix != "ctrldb-test-" || desired.Project != testProject || desired.Region != testRegion || desired.Zone != testZone { + t.Fatalf("DesiredState() lost explicit provider or ownership choices: %#v", desired) + } + for _, resource := range compiled.DesiredResources() { + if resource.Permanence != PermanentSingleton { + t.Errorf("resource %q permanence = %q; want permanent singleton", resource.ID, resource.Permanence) + } + } + if got := compiled.CleanupCapabilities(); !slices.Equal(got, isolation.InitialCleanupCapabilities()) { + t.Fatalf("CleanupCapabilities() = %v; want %v", got, isolation.InitialCleanupCapabilities()) + } + if compiled.Plan().PointOfNoReturn != "k1-audit-bootstrap" || compiled.Risks().ExpectedDowntimeSeconds != 0 || + compiled.Risks().ProductionExposure != "none" { + t.Fatal("compiled review surface lost the retention boundary or safety summary") + } + if compiled.Plan().PointOfNoReturnTrigger != domain.PointOfNoReturnMutationObserved { + t.Fatalf("point-of-no-return trigger = %q; want mutation-observed", compiled.Plan().PointOfNoReturnTrigger) + } + + encoded := mustCanonical(t, compiled) + parsed, err := ParseCompiledPlan(encoded) + if err != nil { + t.Fatalf("ParseCompiledPlan() unexpected error: %v", err) + } + if !bytes.Equal(encoded, mustCanonical(t, parsed)) || parsed.DocumentHash() != compiled.DocumentHash() { + t.Fatal("canonical round trip changed bytes or document hash") + } +} + +func TestRequiredAPIsAreClosedCanonicalAndDetached(t *testing.T) { + t.Parallel() + + want := []string{ + "artifactregistry.googleapis.com", "cloudresourcemanager.googleapis.com", "cloudscheduler.googleapis.com", + "compute.googleapis.com", "iam.googleapis.com", "iamcredentials.googleapis.com", "run.googleapis.com", + "secretmanager.googleapis.com", "serviceusage.googleapis.com", "storage.googleapis.com", + } + if got := RequiredAPIs(); !slices.Equal(got, want) { + t.Fatalf("RequiredAPIs() = %v; want %v", got, want) + } + got := RequiredAPIs() + got[0] = "tampered.googleapis.com" + if RequiredAPIs()[0] != want[0] { + t.Fatal("RequiredAPIs() exposed mutable package state") + } +} + +func TestCompileIsStableAcrossEquivalentObservationOrdering(t *testing.T) { + t.Parallel() + + request := validCompileRequest(t) + seed := validPreflightSeed(testProject, testRegion, testZone, nil, nil, nil) + slices.Reverse(seed.Schemas) + slices.Reverse(seed.APIs) + second, err := observation.NewHarnessPreflight(seed) + if err != nil { + t.Fatalf("NewHarnessPreflight(reordered) unexpected error: %v", err) + } + if second.Revision() != request.Preflight.Revision() { + t.Fatal("semantically identical reordered observation changed its content revision") + } + request.Preflight = second + first := mustCompile(t, validCompileRequest(t)) + reordered := mustCompile(t, request) + if !bytes.Equal(mustCanonical(t, first), mustCanonical(t, reordered)) || first.DocumentHash() != reordered.DocumentHash() { + t.Fatal("equivalent reordered observation changed canonical plan bytes") + } +} + +func TestCompileBindsObservationWindowWithoutPerturbingContentRevision(t *testing.T) { + t.Parallel() + + firstRequest := validCompileRequest(t) + seed := validPreflightSeed(testProject, testRegion, testZone, nil, nil, nil) + seed.ObservedAt = seed.ObservedAt.Add(15 * time.Second) + seed.ValidUntil = seed.ValidUntil.Add(15 * time.Second) + second, err := observation.NewHarnessPreflight(seed) + if err != nil { + t.Fatalf("NewHarnessPreflight(shifted window) unexpected error: %v", err) + } + if second.Revision() != firstRequest.Preflight.Revision() { + t.Fatal("observation timestamps perturbed the content-only revision") + } + secondRequest := firstRequest + secondRequest.Preflight = second + first := mustCompile(t, firstRequest) + shifted := mustCompile(t, secondRequest) + if first.DocumentHash() == shifted.DocumentHash() || bytes.Equal(mustCanonical(t, first), mustCanonical(t, shifted)) { + t.Fatal("freshness window was not bound into the compiled artifact") + } +} + +func TestCompileRejectsStaleOrMismatchedInputs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mutate func(*CompileRequest) + want error + }{ + {name: "stale observation", mutate: func(value *CompileRequest) { + value.CreatedAt = testNow.Add(4 * time.Minute) + value.ExpiresAt = value.CreatedAt.Add(31 * time.Minute) + }, want: ErrPlanBlocked}, + {name: "manifest project", mutate: func(value *CompileRequest) { + value.Preflight = validPreflight(t, "other-project", testRegion, testZone, nil, nil, nil) + }, want: ErrPlanBlocked}, + {name: "manifest region", mutate: func(value *CompileRequest) { + value.Preflight = validPreflight(t, testProject, "us-east1", "us-east1-b", nil, nil, nil) + }, want: ErrPlanBlocked}, + {name: "missing configuration", mutate: func(value *CompileRequest) { value.Configuration = zeroHarnessConfiguration() }, want: ErrInvalidCompileRequest}, + {name: "missing preflight", mutate: func(value *CompileRequest) { value.Preflight = observation.HarnessPreflight{} }, want: ErrPlanBlocked}, + {name: "policy mismatch", mutate: func(value *CompileRequest) { value.ApprovedPolicyHash = repeatedHex("c") }, want: ErrPlanBlocked}, + {name: "short validity", mutate: func(value *CompileRequest) { value.ExpiresAt = value.CreatedAt.Add(29 * time.Minute) }, want: ErrInvalidCompileRequest}, + {name: "non UTC creation", mutate: func(value *CompileRequest) { value.CreatedAt = value.CreatedAt.In(time.FixedZone("offset", 3600)) }, want: ErrInvalidCompileRequest}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := validCompileRequest(t) + test.mutate(&request) + _, err := Compile(request) + if !errors.Is(err, test.want) { + t.Fatalf("Compile() error = %v; want %v", err, test.want) + } + }) + } +} + +func TestCompileRejectsProviderSafetyFailures(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + resources []observation.Resource + subnets []observation.SubnetRange + firewalls []observation.FirewallRule + disable string + }{ + {name: "bucket collision", resources: []observation.Resource{{Kind: observation.ResourceBucket, Name: "example-project-ctrldb-audit", Project: testProject, Location: "us", ProviderID: provider(testProject, "global", string(observation.ResourceBucket), "example-project-ctrldb-audit")}}}, + {name: "CIDR overlap", subnets: []observation.SubnetRange{{Name: "existing", Project: testProject, Region: testRegion, CIDR: "10.40.0.128/25", ProviderID: provider(testProject, "regions", testRegion, "subnetworks", "existing")}}}, + {name: "public MongoDB", firewalls: []observation.FirewallRule{{ + Name: "public-db", Project: testProject, + ProviderID: provider(testProject, "global", "firewalls", "public-db"), + Direction: "INGRESS", SourceRanges: []string{"0.0.0.0/0"}, + Allowed: []observation.Protocol{{Name: "tcp", Ports: []string{"27017"}}}, + }}}, + {name: "disabled API", disable: "compute.googleapis.com"}, + {name: "unknown API", disable: "absent:compute.googleapis.com"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + seed := validPreflightSeed(testProject, testRegion, testZone, test.resources, test.subnets, test.firewalls) + for index := range seed.APIs { + if seed.APIs[index].Name == test.disable { + seed.APIs[index].State = observation.APIDisabled + } + } + if strings.HasPrefix(test.disable, "absent:") { + service := strings.TrimPrefix(test.disable, "absent:") + seed.APIs = slices.DeleteFunc(seed.APIs, func(value observation.APIService) bool { return value.Name == service }) + } + preflight, err := observation.NewHarnessPreflight(seed) + if err != nil { + t.Fatalf("NewHarnessPreflight() unexpected error: %v", err) + } + request := validCompileRequest(t) + request.Preflight = preflight + _, err = Compile(request) + if !errors.Is(err, ErrPlanBlocked) { + t.Fatalf("Compile() error = %v; want ErrPlanBlocked", err) + } + }) + } +} + +func TestCompileAcceptsAdjacentCIDRBoundary(t *testing.T) { + t.Parallel() + + adjacent := observation.SubnetRange{ + Name: "adjacent", Project: testProject, Region: testRegion, CIDR: "10.40.1.0/24", + ProviderID: provider(testProject, "regions", testRegion, "subnetworks", "adjacent"), + } + request := validCompileRequest(t) + request.Preflight = validPreflight(t, testProject, testRegion, testZone, nil, []observation.SubnetRange{adjacent}, nil) + if _, err := Compile(request); err != nil { + t.Fatalf("Compile(adjacent CIDR) unexpected error: %v", err) + } +} + +func TestCompileComparesCompleteCollisionIdentity(t *testing.T) { + t.Parallel() + + otherScope := observation.Resource{ + Kind: observation.ResourceRunJob, Name: "ctrldb-test-wipe", Project: testProject, Location: "us-east1", + ProviderID: provider(testProject, "regions", "us-east1", string(observation.ResourceRunJob), "ctrldb-test-wipe"), + } + request := validCompileRequest(t) + request.Preflight = validPreflight(t, testProject, testRegion, testZone, []observation.Resource{otherScope}, nil, nil) + if _, err := Compile(request); err != nil { + t.Fatalf("Compile(other-scope same name) unexpected error: %v", err) + } + + exact := otherScope + exact.Location = testRegion + exact.ProviderID = provider(testProject, "regions", testRegion, string(observation.ResourceRunJob), exact.Name) + request.Preflight = validPreflight(t, testProject, testRegion, testZone, []observation.Resource{exact}, nil, nil) + if _, err := Compile(request); !errors.Is(err, ErrPlanBlocked) { + t.Fatalf("Compile(exact collision) error = %v; want ErrPlanBlocked", err) + } +} + +func TestCompileRejectsUnresolvedOrAmbiguousPricing(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mutate func(*CompileRequest) + }{ + {name: "unobserved machine", mutate: func(value *CompileRequest) { value.Pricing.MachineType = "n2-standard-2" }}, + {name: "numeric mismatch", mutate: func(value *CompileRequest) { value.Pricing.GuestCPUs++ }}, + {name: "over cost cap", mutate: func(value *CompileRequest) { value.Pricing.EstimatedRunMicros = 25_000_001 }}, + {name: "float precision overflow", mutate: func(value *CompileRequest) { value.Pricing.EstimatedRunMicros = maximumExactMicros + 1 }}, + {name: "negative cost", mutate: func(value *CompileRequest) { value.Pricing.EstimatedRunMicros = -1 }}, + {name: "future price table", mutate: func(value *CompileRequest) { value.Pricing.PriceTableDate = "2026-09-08" }}, + {name: "stale price table", mutate: func(value *CompileRequest) { value.Pricing.PriceTableDate = "2026-08-01" }}, + {name: "expired pricing", mutate: func(value *CompileRequest) { value.Pricing.ValidUntil = value.CreatedAt }}, + {name: "unknown schema", mutate: func(value *CompileRequest) { value.Pricing.Schema = "pricing/v2" }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := validCompileRequest(t) + test.mutate(&request) + if _, err := Compile(request); err == nil { + t.Fatal("Compile() succeeded; want fail-closed error") + } + }) + } +} + +func TestCompileRejectsDeprecatedMachineObservation(t *testing.T) { + t.Parallel() + + seed := validPreflightSeed(testProject, testRegion, testZone, nil, nil, nil) + seed.MachineTypes[0].Deprecated = true + preflight, err := observation.NewHarnessPreflight(seed) + if err != nil { + t.Fatalf("NewHarnessPreflight(deprecated machine) unexpected error: %v", err) + } + request := validCompileRequest(t) + request.Preflight = preflight + if _, err := Compile(request); !errors.Is(err, ErrPlanBlocked) { + t.Fatalf("Compile(deprecated machine) error = %v; want ErrPlanBlocked", err) + } +} + +func TestCompileAcceptsExactIntegerCostBoundary(t *testing.T) { + t.Parallel() + + request := validCompileRequest(t) + request.Pricing.EstimatedRunMicros = 25_000_000 + compiled := mustCompile(t, request) + if compiled.Limits().EstimatedCostMicros != compiled.Limits().MaximumCostMicros { + t.Fatal("exact micro-USD cap boundary was not preserved") + } +} + +func TestInvalidProvenanceCannotReachCompiler(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mutate func(*observation.Seed) + }{ + {name: "partial schemas", mutate: func(value *observation.Seed) { value.Schemas = value.Schemas[1:] }}, + {name: "unsupported schema", mutate: func(value *observation.Seed) { value.Schemas[0] = "gcloud-560/unsupported-v1" }}, + {name: "wrong-zone machine", mutate: func(value *observation.Seed) { value.MachineTypes[0].Zone = "us-central1-b" }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + seed := validPreflightSeed(testProject, testRegion, testZone, nil, nil, nil) + test.mutate(&seed) + if _, err := observation.NewHarnessPreflight(seed); !errors.Is(err, observation.ErrInvalidObservation) { + t.Fatalf("NewHarnessPreflight() error = %v; want ErrInvalidObservation", err) + } + }) + } +} + +func TestCompiledPlanGettersAreDetached(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + original := mustCanonical(t, compiled) + desired := compiled.DesiredState() + desired.Labels["managed-by"] = "tampered" + intents := compiled.Intents() + intents[0].Dependencies = append(intents[0].Dependencies, "tampered") + intents[len(intents)-1].Transition.ToBootstrapPhase = "tampered" + capabilities := compiled.CleanupCapabilities() + capabilities[0] = isolation.CleanupCapability("tampered") + plan := compiled.Plan() + plan.Steps[0].ID = "tampered" + if !bytes.Equal(original, mustCanonical(t, compiled)) { + t.Fatal("detached getter mutation changed compiled plan") + } +} + +func zeroHarnessConfiguration() config.HarnessConfiguration { return config.HarnessConfiguration{} } + +func TestStepRegistryHasNoRepresentableTestRunOrUnrelatedIntent(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + allowed := []IntentKind{ + IntentAuditBootstrap, IntentControlBucket, IntentBucketIAM, IntentSeedControl, IntentLockRoundTrip, + IntentNetwork, IntentSubnet, IntentNAT, IntentFirewall, IntentIdentities, IntentControlPrefix, + IntentNightlyWipe, IntentIsolationGate, + } + for _, intent := range compiled.Intents() { + if !slices.Contains(allowed, intent.Kind) { + t.Fatalf("compiled pre-T8 intent %q is outside WF-TEST-01", intent.Kind) + } + } + if !reflect.DeepEqual(compiled.CleanupCapabilities(), []isolation.CleanupCapability{ + isolation.CleanupComputeDisks, isolation.CleanupComputeFirewalls, isolation.CleanupComputeInstances, + }) { + t.Fatal("compiled cleanup capability set is not the closed initial set") + } +} diff --git a/internal/isolation/bootstrap/helpers_test.go b/internal/isolation/bootstrap/helpers_test.go new file mode 100644 index 0000000..48d50d3 --- /dev/null +++ b/internal/isolation/bootstrap/helpers_test.go @@ -0,0 +1,197 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +package bootstrap + +import ( + "encoding/json" + "os" + "strings" + "testing" + "time" + + "github.com/thelostorbital/ctrldb/internal/config" + "github.com/thelostorbital/ctrldb/internal/observation" +) + +const ( + testAccount = "operator@example.invalid" + testProject = "example-project" + testRegion = "us-central1" + testZone = "us-central1-a" +) + +var testNow = time.Date(2026, 9, 7, 12, 1, 0, 0, time.UTC) + +func validCompileRequest(t *testing.T) CompileRequest { + t.Helper() + + return CompileRequest{ + Configuration: validHarnessConfiguration(t), + Preflight: validPreflight(t, testProject, testRegion, testZone, nil, nil, nil), + PlanID: "plan-0123456789abcdef", + CreatedAt: testNow, + ExpiresAt: testNow.Add(31 * time.Minute), + LocalPolicyHash: repeatedHex("a"), + ApprovedPolicyHash: repeatedHex("a"), + Pricing: PricingEvidence{ + MachineType: "e2-medium", + GuestCPUs: 2, + MemoryMiB: 4096, + EstimatedRunMicros: 5_000_000, + PriceTableDate: "2026-09-07", + Schema: PricingSchemaV1, + Revision: repeatedHex("b"), + ObservedAt: testNow.Add(-time.Minute), + ValidUntil: testNow.Add(4 * time.Minute), + }, + } +} + +func validHarnessConfiguration(t *testing.T) config.HarnessConfiguration { + t.Helper() + + manifest := fixtureManifest(t) + metadata := nestedObject(t, manifest, "metadata") + metadata["name"] = "disposable-test" + metadata["class"] = "disposable" + nestedObject(t, manifest, "spec")["testIsolation"] = map[string]any{ + "namePrefix": config.TestResourcePrefix, + "labels": map[string]any{"managed-by": "ctrldb", "environment": "disposable", "purpose": "test"}, + "operatorServiceAccount": "ctrldb-test-operator@example-project.iam.gserviceaccount.com", + "destructiveServiceAccount": "ctrldb-test-destructive@example-project.iam.gserviceaccount.com", + "network": map[string]any{ + "vpc": "ctrldb-test-vpc", "subnet": "ctrldb-test-subnet", "cidr": "10.40.0.0/24", "nat": "ctrldb-test-nat", + }, + "ciPrincipal": "principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/example-pool/attribute.repository/example-org/ctrldb", + "caps": map[string]any{ + "maxMachineType": "e2-medium", "maxDiskGiB": 100, "maxInstances": 3, + "maxLifetime": "8h", "maxEstimatedUSDPerRun": 25, + }, + "monitoringTests": "manual-only", + } + nestedObject(t, manifest, "spec", "host")["serviceAccount"] = "ctrldb-test-vm@example-project.iam.gserviceaccount.com" + reconciler := nestedObject(t, manifest, "spec", "reconciler") + reconciler["schedulerJob"] = "ctrldb-test-wipe-schedule" + reconciler["runJob"] = "ctrldb-test-wipe" + reconciler["serviceAccount"] = "ctrldb-test-wipe@example-project.iam.gserviceaccount.com" + + encoded, err := json.Marshal(manifest) + if err != nil { + t.Fatalf("json.Marshal(manifest) unexpected error: %v", err) + } + document, err := config.DecodeManifest(encoded) + if err != nil { + t.Fatalf("config.DecodeManifest() unexpected error: %v", err) + } + configuration, err := config.HarnessConfigurationFromManifest(document) + if err != nil { + t.Fatalf("config.HarnessConfigurationFromManifest() unexpected error: %v", err) + } + return configuration +} + +func fixtureManifest(t *testing.T) map[string]any { + t.Helper() + + encoded := mustReadFile(t, "../../config/testdata/manifest-v1alpha1.yaml") + document, err := config.DecodeManifestEnvelope(encoded) + if err != nil { + t.Fatalf("config.DecodeManifestEnvelope() unexpected error: %v", err) + } + var manifest map[string]any + if err := json.Unmarshal(document.JSON(), &manifest); err != nil { + t.Fatalf("json.Unmarshal(manifest) unexpected error: %v", err) + } + return manifest +} + +func nestedObject(t *testing.T, value map[string]any, path ...string) map[string]any { + t.Helper() + + current := value + for _, token := range path { + next, ok := current[token].(map[string]any) + if !ok { + t.Fatalf("fixture path %q is not an object", path) + } + current = next + } + return current +} + +func validPreflight( + t *testing.T, + project, region, zone string, + resources []observation.Resource, + subnets []observation.SubnetRange, + firewalls []observation.FirewallRule, +) observation.HarnessPreflight { + t.Helper() + + seed := validPreflightSeed(project, region, zone, resources, subnets, firewalls) + result, err := observation.NewHarnessPreflight(seed) + if err != nil { + t.Fatalf("observation.NewHarnessPreflight() unexpected error: %v", err) + } + return result +} + +func validPreflightSeed( + project, region, zone string, + resources []observation.Resource, + subnets []observation.SubnetRange, + firewalls []observation.FirewallRule, +) observation.Seed { + services := make([]observation.APIService, len(requiredAPIs)) + for index, service := range requiredAPIs { + services[index] = observation.APIService{Name: service, State: observation.APIEnabled} + } + return observation.Seed{ + Account: testAccount, Project: project, Region: region, Zone: zone, + GcloudVersion: observation.SupportedGcloudVersion, CompletenessPolicy: observation.GcloudCompletenessPolicy, + ObservedAt: testNow.Add(-time.Minute), ValidUntil: testNow.Add(4 * time.Minute), + Schemas: observation.RequiredSchemas(), + Regions: []observation.Region{{Name: region, Availability: observation.AvailabilityUp, ProviderID: provider(project, "regions", region)}}, + Zones: []observation.Zone{{Name: zone, Region: region, Availability: observation.AvailabilityUp, ProviderID: provider(project, "zones", zone)}}, + MachineTypes: []observation.MachineType{{ + Name: "e2-medium", Zone: zone, GuestCPUs: 2, MemoryMiB: 4096, + ProviderID: provider(project, "zones", zone, "machineTypes", "e2-medium"), + }}, + SubnetRanges: subnets, Resources: resources, APIs: services, Firewalls: firewalls, Exhaustive: true, + } +} + +func mustReadFile(t *testing.T, path string) []byte { + t.Helper() + + encoded, err := os.ReadFile(path) + if err != nil { + t.Fatalf("os.ReadFile(%q) unexpected error: %v", path, err) + } + return encoded +} + +func repeatedHex(character string) string { + return strings.Repeat(character, 64) +} + +func mustCompile(t *testing.T, request CompileRequest) CompiledPlan { + t.Helper() + + result, err := Compile(request) + if err != nil { + t.Fatalf("Compile() unexpected error: %v", err) + } + return result +} + +func mustCanonical(t *testing.T, plan CompiledPlan) []byte { + t.Helper() + + encoded, err := plan.CanonicalJSON() + if err != nil { + t.Fatalf("CanonicalJSON() unexpected error: %v", err) + } + return encoded +} diff --git a/internal/isolation/bootstrap/types.go b/internal/isolation/bootstrap/types.go new file mode 100644 index 0000000..8576336 --- /dev/null +++ b/internal/isolation/bootstrap/types.go @@ -0,0 +1,263 @@ +// Copyright 2026 CtrlBoard.dev +// SPDX-License-Identifier: Apache-2.0 + +// Package bootstrap compiles the I/O-free WF-TEST-01 bootstrap plan. +package bootstrap + +import ( + "errors" + "time" + + "github.com/thelostorbital/ctrldb/internal/config" + "github.com/thelostorbital/ctrldb/internal/domain" + "github.com/thelostorbital/ctrldb/internal/isolation" + "github.com/thelostorbital/ctrldb/internal/observation" +) + +const ( + CompiledPlanSchemaV1 = "ctrldb.ctrlboard.dev/wf-test-plan/v1" + WorkflowID = isolation.WFTestWorkflowID +) + +var ( + ErrInvalidCompileRequest = errors.New("invalid WF-TEST-01 compile request") + ErrPlanBlocked = errors.New("WF-TEST-01 plan blocked") + ErrInvalidCompiledPlan = errors.New("invalid compiled WF-TEST-01 plan") +) + +// CompileRequest contains every decision and observation required to compile +// WF-TEST-01. There are deliberately no mutation defaults. +type CompileRequest struct { + Configuration config.HarnessConfiguration + Preflight observation.HarnessPreflight + PlanID string + CreatedAt time.Time + ExpiresAt time.Time + LocalPolicyHash string + ApprovedPolicyHash string + Pricing PricingEvidence +} + +// PricingEvidence is a fresh, externally obtained integer-micro-USD estimate. +// M1-04 validates and binds it but never performs pricing I/O. +type PricingEvidence struct { + MachineType string `json:"machineType"` + GuestCPUs int64 `json:"guestCpus"` + MemoryMiB int64 `json:"memoryMiB"` + EstimatedRunMicros int64 `json:"estimatedRunMicros"` + PriceTableDate string `json:"priceTableDate"` + Schema string `json:"schema"` + Revision string `json:"revision"` + ObservedAt time.Time `json:"observedAt"` + ValidUntil time.Time `json:"validUntil"` +} + +// ResourceKind is the closed set of provider objects referenced by M1-04. +type ResourceKind string + +const ( + ResourceBucket ResourceKind = "storage.bucket" + ResourceNetwork ResourceKind = "compute.network" + ResourceSubnetwork ResourceKind = "compute.subnetwork" + ResourceRouter ResourceKind = "compute.router" + ResourceNAT ResourceKind = "compute.nat" + ResourceFirewall ResourceKind = "compute.firewall" + ResourceServiceAccount ResourceKind = "iam.service-account" + ResourceCustomRole ResourceKind = "iam.custom-role" + ResourceRunJob ResourceKind = "run.job" + ResourceSchedulerJob ResourceKind = "scheduler.job" +) + +// Permanence separates durable harness singletons from future run resources. +type Permanence string + +const ( + PermanentSingleton Permanence = "permanent-singleton" + DisposableRun Permanence = "disposable-run" +) + +// DesiredResource is one exact provider identity and desired-state digest. +// ProviderID and ParentProviderID are identifiers, never arbitrary commands. +type DesiredResource struct { + ID string `json:"id"` + Kind ResourceKind `json:"kind"` + Name string `json:"name"` + Project string `json:"project"` + Location string `json:"location"` + ProviderID string `json:"providerId"` + ParentProviderID string `json:"parentProviderId"` + Permanence Permanence `json:"permanence"` + DesiredStateFingerprint string `json:"desiredStateFingerprint"` +} + +// HarnessDesiredState preserves all explicit manifest selections needed by +// later typed adapters. Protocol-owned constants are named separately. +type HarnessDesiredState struct { + Account string `json:"account"` + Project string `json:"project"` + Region string `json:"region"` + Zone string `json:"zone"` + CIDR string `json:"cidr"` + NamePrefix string `json:"namePrefix"` + Labels map[string]string `json:"labels"` + ControlBucket string `json:"controlBucket"` + AuditBucket string `json:"auditBucket"` + VPC string `json:"vpc"` + Subnet string `json:"subnet"` + Router string `json:"router"` + NAT string `json:"nat"` + IAPFirewall string `json:"iapFirewall"` + InternalFirewall string `json:"internalFirewall"` + NodeTag string `json:"nodeTag"` + OperatorPrincipal string `json:"operatorPrincipal"` + DestructivePrincipal string `json:"destructivePrincipal"` + VMPrincipal string `json:"vmPrincipal"` + WipePrincipal string `json:"wipePrincipal"` + CIPrincipal string `json:"ciPrincipal"` + OperatorRole string `json:"operatorRole"` + DestructiveRole string `json:"destructiveRole"` + WipeRunJob string `json:"wipeRunJob"` + WipeSchedulerJob string `json:"wipeSchedulerJob"` + WipeScheduleUTC string `json:"wipeScheduleUtc"` + ImageDigest string `json:"imageDigest"` +} + +// RunLimits is the exact future disposable-run ceiling recorded by the plan. +type RunLimits struct { + MaximumMachineType string `json:"maximumMachineType"` + MaximumGuestCPUs int64 `json:"maximumGuestCpus"` + MaximumMemoryMiB int64 `json:"maximumMemoryMiB"` + MaximumDiskGiB int64 `json:"maximumDiskGiB"` + MaximumInstances int64 `json:"maximumInstances"` + MaximumLifetimeSec int64 `json:"maximumLifetimeSeconds"` + MaximumCostMicros int64 `json:"maximumCostMicros"` + EstimatedCostMicros int64 `json:"estimatedCostMicros"` +} + +// EnvelopeBinding is the immutable handoff M1-05 must embed in the local and +// audit BootstrapEnvelopeV1. Every intent carries this same hash. +type EnvelopeBinding struct { + WorkflowID string `json:"workflowId"` + PlanID string `json:"planId"` + PlanHash string `json:"planHash"` + Account string `json:"account"` + ManifestHash string `json:"manifestHash"` + ObservationRevision string `json:"observationRevision"` + ObservedAt time.Time `json:"observedAt"` + ValidUntil time.Time `json:"validUntil"` + BindingSHA256 string `json:"bindingSha256"` +} + +// IntentKind is the closed WF-TEST-01 K/T registry. +type IntentKind string + +const ( + IntentAuditBootstrap IntentKind = "audit-bootstrap" + IntentControlBucket IntentKind = "control-bucket" + IntentBucketIAM IntentKind = "bucket-iam" + IntentSeedControl IntentKind = "seed-control" + IntentLockRoundTrip IntentKind = "lock-round-trip" + IntentNetwork IntentKind = "network" + IntentSubnet IntentKind = "subnet" + IntentNAT IntentKind = "nat" + IntentFirewall IntentKind = "firewall" + IntentIdentities IntentKind = "identities" + IntentControlPrefix IntentKind = "control-prefix" + IntentNightlyWipe IntentKind = "nightly-wipe" + IntentIsolationGate IntentKind = "isolation-gate" +) + +// PointOfNoReturnClass is the per-step irreversible-boundary classification. +type PointOfNoReturnClass string + +const ( + PONRReversible PointOfNoReturnClass = "reversible" + PONRAuditRetentionLock PointOfNoReturnClass = "audit-retention-lock" + PONRVerificationOnly PointOfNoReturnClass = "verification-only" +) + +// HarnessTransition describes T8's proposed transition. It is review data; +// this package never persists or performs the transition. +type HarnessTransition struct { + FromBootstrapPhase string `json:"fromBootstrapPhase"` + FromTestUsability string `json:"fromTestUsability"` + ToBootstrapPhase string `json:"toBootstrapPhase"` + ToTestUsability string `json:"toTestUsability"` +} + +// StepIntent is a closed, provider-independent mutation or verification +// intent. It contains no command, argv, token, credential, or provider output. +type StepIntent struct { + StepID string `json:"stepId"` + Kind IntentKind `json:"kind"` + EnvelopeBindingSHA256 string `json:"envelopeBindingSha256"` + ExecutingIdentity domain.ExecutionIdentity `json:"executingIdentity"` + ResourceIDs []string `json:"resourceIds"` + Dependencies []string `json:"dependencies"` + Preconditions []string `json:"preconditions"` + Verification []string `json:"verification"` + Retry domain.RetryPolicy `json:"retry"` + TimeoutSeconds int64 `json:"timeoutSeconds"` + CancelSafe bool `json:"cancelSafe"` + Compensation string `json:"compensation"` + PointOfNoReturn PointOfNoReturnClass `json:"pointOfNoReturn"` + Transition *HarnessTransition `json:"transition,omitempty"` +} + +// RiskSummary is the owner-facing non-secret impact summary sealed with the +// plan. The strings are protocol constants, not caller-authored free text. +type RiskSummary struct { + ExpectedCostCeilingMicros int64 `json:"expectedCostCeilingMicros"` + EstimatedRunCostMicros int64 `json:"estimatedRunCostMicros"` + ExpectedDowntimeSeconds int64 `json:"expectedDowntimeSeconds"` + ProductionExposure string `json:"productionExposure"` + PermanentResiduals []string `json:"permanentResiduals"` + Risks []string `json:"risks"` + Rollback []string `json:"rollback"` + Compensation []string `json:"compensation"` + PointOfNoReturn string `json:"pointOfNoReturn"` +} + +type compiledPayloadV1 struct { + Plan domain.Plan `json:"plan"` + Binding EnvelopeBinding `json:"binding"` + Desired HarnessDesiredState `json:"desired"` + DesiredResources []DesiredResource `json:"desiredResources"` + Limits RunLimits `json:"limits"` + Pricing PricingEvidence `json:"pricing"` + CleanupCapabilities []isolation.CleanupCapability `json:"cleanupCapabilities"` + Intents []StepIntent `json:"intents"` + Risks RiskSummary `json:"risks"` +} + +type compiledWireV1 struct { + SchemaVersion string `json:"schemaVersion"` + Payload compiledPayloadV1 `json:"payload"` + DocumentSHA256 string `json:"documentSha256"` +} + +// CompiledPlan is immutable. Construction and parsing both validate every +// cross-binding before exposing detached values. +type CompiledPlan struct { + payload compiledPayloadV1 + documentHash string + contract domain.ExecutionContract +} + +func (value CompiledPlan) Plan() domain.Plan { return clonePlan(value.payload.Plan) } +func (value CompiledPlan) ExecutionContract() domain.ExecutionContract { return value.contract } +func (value CompiledPlan) Binding() EnvelopeBinding { return value.payload.Binding } +func (value CompiledPlan) DesiredState() HarnessDesiredState { + return cloneDesired(value.payload.Desired) +} +func (value CompiledPlan) DesiredResources() []DesiredResource { + return append([]DesiredResource(nil), value.payload.DesiredResources...) +} +func (value CompiledPlan) Limits() RunLimits { return value.payload.Limits } +func (value CompiledPlan) Pricing() PricingEvidence { return value.payload.Pricing } +func (value CompiledPlan) CleanupCapabilities() []isolation.CleanupCapability { + return append([]isolation.CleanupCapability(nil), value.payload.CleanupCapabilities...) +} +func (value CompiledPlan) Intents() []StepIntent { return cloneIntents(value.payload.Intents) } +func (value CompiledPlan) Risks() RiskSummary { return cloneRisks(value.payload.Risks) } +func (value CompiledPlan) DocumentHash() string { return value.documentHash } From 5b1567d6b036f8e5d874886cfcfc4a05f4d1175c Mon Sep 17 00:00:00 2001 From: Syed <40798652+thelostorbital@users.noreply.github.com> Date: Mon, 7 Sep 2026 07:19:27 +0530 Subject: [PATCH 2/3] fix: bind bootstrap safety evidence --- internal/config/harness.go | 62 ++++++++----- internal/config/harness_test.go | 6 ++ internal/isolation/bootstrap/canonical.go | 36 ++++++-- .../isolation/bootstrap/canonical_test.go | 8 ++ internal/isolation/bootstrap/compile.go | 84 ++++++++++++++--- internal/isolation/bootstrap/compile_test.go | 90 ++++++++++++++++++- internal/isolation/bootstrap/helpers_test.go | 45 +++++++++- internal/isolation/bootstrap/types.go | 40 ++++++++- 8 files changed, 324 insertions(+), 47 deletions(-) diff --git a/internal/config/harness.go b/internal/config/harness.go index 6359609..da81b03 100644 --- a/internal/config/harness.go +++ b/internal/config/harness.go @@ -58,6 +58,7 @@ type HarnessConfiguration struct { wipeServiceAccount string imageDigest string reconcilerEnabled bool + planValidity time.Duration caps HarnessCaps } @@ -102,6 +103,9 @@ type harnessManifestWire struct { ServiceAccount string `json:"serviceAccount"` ImageDigest string `json:"imageDigest"` } `json:"reconciler"` + Policy struct { + PlanValidity string `json:"planValidity"` + } `json:"policy"` TestIsolation *struct { NamePrefix string `json:"namePrefix"` Labels map[string]string `json:"labels"` @@ -158,6 +162,12 @@ func HarnessConfigurationFromManifest(document ManifestDocument) (HarnessConfigu return HarnessConfiguration{}, fmt.Errorf("%w: invalid test lifetime", ErrInvalidHarnessConfiguration) } lifetime := time.Duration(lifetimeSeconds.Int64()) * time.Second + planValiditySeconds, ok := durationSeconds(wire.Spec.Policy.PlanValidity) + if !ok || planValiditySeconds.Sign() <= 0 || !planValiditySeconds.IsInt64() || + planValiditySeconds.Cmp(maximumDurationSeconds) > 0 { + return HarnessConfiguration{}, fmt.Errorf("%w: invalid plan validity", ErrInvalidHarnessConfiguration) + } + planValidity := time.Duration(planValiditySeconds.Int64()) * time.Second costMicros, err := usdNumberToMicrosCeiling(wire.Spec.TestIsolation.Caps.MaxEstimatedUSDPerRun) if err != nil { return HarnessConfiguration{}, fmt.Errorf("%w: invalid test cost cap", ErrInvalidHarnessConfiguration) @@ -182,34 +192,19 @@ func HarnessConfigurationFromManifest(document ManifestDocument) (HarnessConfigu if wire.Spec.TestIsolation.Caps.MaxDiskGiB <= 0 || wire.Spec.TestIsolation.Caps.MaxInstances <= 0 { return HarnessConfiguration{}, fmt.Errorf("%w: invalid numeric caps", ErrInvalidHarnessConfiguration) } - if wire.Spec.TestIsolation.OperatorServiceAccount == wire.Spec.TestIsolation.DestructiveServiceAccount { - return HarnessConfiguration{}, fmt.Errorf("%w: test operator and destructive service accounts must be distinct", ErrInvalidHarnessConfiguration) - } - if wire.Spec.TestIsolation.OperatorServiceAccount == wire.Spec.Host.ServiceAccount || - wire.Spec.TestIsolation.DestructiveServiceAccount == wire.Spec.Host.ServiceAccount { - return HarnessConfiguration{}, fmt.Errorf("%w: test control and database VM service accounts must be distinct", ErrInvalidHarnessConfiguration) - } - if wire.Spec.Reconciler.ServiceAccount == wire.Spec.Host.ServiceAccount || - wire.Spec.Reconciler.ServiceAccount == wire.Spec.TestIsolation.OperatorServiceAccount || - wire.Spec.Reconciler.ServiceAccount == wire.Spec.TestIsolation.DestructiveServiceAccount { - return HarnessConfiguration{}, fmt.Errorf("%w: wipe reconciler service account must be distinct from runtime and test control identities", ErrInvalidHarnessConfiguration) - } - for _, account := range []string{ + if err := ValidateHarnessPrincipalSet( + wire.Spec.GCP.Project, wire.Spec.TestIsolation.OperatorServiceAccount, wire.Spec.TestIsolation.DestructiveServiceAccount, + wire.Spec.Host.ServiceAccount, wire.Spec.Reconciler.ServiceAccount, - } { - if !harnessServiceAccountPattern.MatchString(account) || - !serviceAccountBelongsToProject(account, wire.Spec.GCP.Project) { - return HarnessConfiguration{}, fmt.Errorf("%w: test control service accounts must be canonical identities in the configured project", ErrInvalidHarnessConfiguration) - } + wire.Spec.TestIsolation.CIPrincipal, + ); err != nil { + return HarnessConfiguration{}, err } if !generatedResourceNamePattern.MatchString(wire.Spec.TestIsolation.Network.Subnet) { return HarnessConfiguration{}, fmt.Errorf("%w: test subnet must be a provider-valid Compute resource name", ErrInvalidHarnessConfiguration) } - if !workloadIdentityPrincipalPattern.MatchString(wire.Spec.TestIsolation.CIPrincipal) { - return HarnessConfiguration{}, fmt.Errorf("%w: CI principal must identify one canonical workload identity subject or repository", ErrInvalidHarnessConfiguration) - } if !wire.Spec.Reconciler.Enabled { return HarnessConfiguration{}, fmt.Errorf("%w: disposable harness requires the wipe reconciler", ErrInvalidHarnessConfiguration) } @@ -228,7 +223,7 @@ func HarnessConfigurationFromManifest(document ManifestDocument) (HarnessConfigu cidr: wire.Spec.TestIsolation.Network.CIDR, router: TestRouterName, nat: wire.Spec.TestIsolation.Network.NAT, wipeSchedulerJob: wire.Spec.Reconciler.SchedulerJob, wipeRunJob: wire.Spec.Reconciler.RunJob, wipeServiceAccount: wire.Spec.Reconciler.ServiceAccount, imageDigest: wire.Spec.Reconciler.ImageDigest, - reconcilerEnabled: wire.Spec.Reconciler.Enabled, + reconcilerEnabled: wire.Spec.Reconciler.Enabled, planValidity: planValidity, caps: HarnessCaps{ maxMachineType: wire.Spec.TestIsolation.Caps.MaxMachineType, maxDiskGiB: wire.Spec.TestIsolation.Caps.MaxDiskGiB, @@ -238,6 +233,26 @@ func HarnessConfigurationFromManifest(document ManifestDocument) (HarnessConfigu }, nil } +// ValidateHarnessPrincipalSet applies the same canonical, project-ownership, +// and separation rules to manifest projections and parsed compiled plans. +func ValidateHarnessPrincipalSet(project, operator, destructive, vm, wipe, ci string) error { + accounts := []string{operator, destructive, vm, wipe} + seen := make(map[string]struct{}, len(accounts)) + for _, account := range accounts { + if !harnessServiceAccountPattern.MatchString(account) || !serviceAccountBelongsToProject(account, project) { + return fmt.Errorf("%w: harness service accounts must be canonical identities in the configured project", ErrInvalidHarnessConfiguration) + } + if _, duplicate := seen[account]; duplicate { + return fmt.Errorf("%w: harness service accounts must be distinct", ErrInvalidHarnessConfiguration) + } + seen[account] = struct{}{} + } + if !workloadIdentityPrincipalPattern.MatchString(ci) { + return fmt.Errorf("%w: CI principal must identify one canonical workload identity subject or repository", ErrInvalidHarnessConfiguration) + } + return nil +} + func usdNumberToMicrosCeiling(value json.Number) (int64, error) { amount := new(big.Rat) if value.String() == "" { @@ -303,6 +318,9 @@ func (configuration HarnessConfiguration) ImageDigest() string { return configur func (configuration HarnessConfiguration) ReconcilerEnabled() bool { return configuration.reconcilerEnabled } +func (configuration HarnessConfiguration) PlanValidity() time.Duration { + return configuration.planValidity +} func (configuration HarnessConfiguration) Caps() HarnessCaps { return configuration.caps } func (caps HarnessCaps) MaxMachineType() string { return caps.maxMachineType } diff --git a/internal/config/harness_test.go b/internal/config/harness_test.go index 6379c26..fe95896 100644 --- a/internal/config/harness_test.go +++ b/internal/config/harness_test.go @@ -60,6 +60,9 @@ func TestHarnessConfigurationFromManifestPreservesExplicitValues(t *testing.T) { if !configuration.ReconcilerEnabled() { t.Fatal("ReconcilerEnabled() = false; disposable harness requires the wipe reconciler") } + if got := configuration.PlanValidity(); got != time.Hour { + t.Errorf("PlanValidity() = %s; want 1h", got) + } if got := configuration.Caps().MaxDiskGiB(); got != 100 { t.Errorf("MaxDiskGiB() = %d; want 100", got) } @@ -196,6 +199,9 @@ func TestHarnessConfigurationRequiresCompleteValidation(t *testing.T) { {name: "wipe belongs to another project", mutate: func(t *testing.T, manifest map[string]any) { nestedMap(t, manifest, "spec", "reconciler")["serviceAccount"] = "ctrldb-test-wipe@foreign-project.iam.gserviceaccount.com" }}, + {name: "VM belongs to another project", mutate: func(t *testing.T, manifest map[string]any) { + nestedMap(t, manifest, "spec", "host")["serviceAccount"] = "ctrldb-test-vm@foreign-project.iam.gserviceaccount.com" + }}, {name: "operator has provider-invalid identity", mutate: func(t *testing.T, manifest map[string]any) { nestedMap(t, manifest, "spec", "testIsolation")["operatorServiceAccount"] = "BAD@example-project.iam.gserviceaccount.com" }}, diff --git a/internal/isolation/bootstrap/canonical.go b/internal/isolation/bootstrap/canonical.go index 679d755..35e5d20 100644 --- a/internal/isolation/bootstrap/canonical.go +++ b/internal/isolation/bootstrap/canonical.go @@ -100,16 +100,19 @@ func validateCompiledPayload(payload compiledPayloadV1) (domain.ExecutionContrac if err != nil || !equalCanonicalValue(expectedResources, payload.DesiredResources) { return domain.ExecutionContract{}, invalidCompiled("desired resources") } - if err := validateLimitsAndPricing(payload.Limits, payload.Pricing, payload.Plan); err != nil { + if err := validateLimitsAndPricing(payload.Limits, payload.Pricing, payload.Plan, payload.Desired); err != nil { return domain.ExecutionContract{}, err } if !slices.Equal(payload.CleanupCapabilities, isolation.InitialCleanupCapabilities()) { return domain.ExecutionContract{}, invalidCompiled("cleanup capabilities") } - if err := validateBinding(payload.Binding, payload.Plan); err != nil { + definitions := stepRegistry(expectedResources) + if err := validatePermissionEvidence(payload.Permissions, definitions, payload.Desired.Account, payload.Desired.Project, payload.Plan.CreatedAt); err != nil { + return domain.ExecutionContract{}, invalidCompiled("permission evidence") + } + if err := validateBinding(payload.Binding, payload.Plan, payload.Permissions.Revision); err != nil { return domain.ExecutionContract{}, err } - definitions := stepRegistry(expectedResources) expectedPlan, contract, err := buildPlanValues( payload.Plan.PlanID, payload.Desired.Project, payload.Plan.Environment, payload.Desired.Account, payload.Plan.CreatedAt, payload.Plan.ExpiresAt, payload.Plan.PolicyHash.Local, payload.Plan.PolicyHash.Approved, @@ -134,6 +137,10 @@ func validateDesiredState(desired HarnessDesiredState, plan domain.Plan) error { !desiredLocationPattern.MatchString(desired.Zone) || !strings.HasPrefix(desired.Zone, desired.Region+"-") { return invalidCompiled("desired provider context") } + if desired.PlanValiditySeconds <= 0 || desired.PlanValiditySeconds > int64((1<<63-1)/time.Second) || + !plan.ExpiresAt.Equal(plan.CreatedAt.Add(time.Duration(desired.PlanValiditySeconds)*time.Second)) { + return invalidCompiled("plan validity") + } prefix, err := netip.ParsePrefix(desired.CIDR) if err != nil || prefix != prefix.Masked() || !prefix.Addr().IsPrivate() { return invalidCompiled("desired CIDR") @@ -160,12 +167,21 @@ func validateDesiredState(desired HarnessDesiredState, plan domain.Plan) error { !strings.HasPrefix(desired.ImageDigest, "sha256:") || !sha256Pattern.MatchString(strings.TrimPrefix(desired.ImageDigest, "sha256:")) { return invalidCompiled("protocol-owned desired state") } + if err := config.ValidateHarnessPrincipalSet( + desired.Project, desired.OperatorPrincipal, desired.DestructivePrincipal, + desired.VMPrincipal, desired.WipePrincipal, desired.CIPrincipal, + ); err != nil { + return invalidCompiled("desired principals") + } return nil } -func validateLimitsAndPricing(limits RunLimits, price PricingEvidence, plan domain.Plan) error { +func validateLimitsAndPricing(limits RunLimits, price PricingEvidence, plan domain.Plan, desired HarnessDesiredState) error { if limits.MaximumMachineType == "" || limits.MaximumMachineType != price.MachineType || + price.Region != desired.Region || price.Zone != desired.Zone || !strings.HasPrefix(price.Zone, price.Region+"-") || limits.MaximumGuestCPUs != price.GuestCPUs || limits.MaximumMemoryMiB != price.MemoryMiB || + limits.MaximumDiskGiB != price.DiskGiB || limits.MaximumInstances != price.Instances || + limits.MaximumLifetimeSec != price.LifetimeSeconds || price.Currency != "USD" || limits.MaximumDiskGiB <= 0 || limits.MaximumInstances <= 0 || limits.MaximumLifetimeSec <= 0 || limits.MaximumCostMicros < 0 || limits.MaximumCostMicros > maximumExactMicros || limits.EstimatedCostMicros != price.EstimatedRunMicros || limits.EstimatedCostMicros < 0 || @@ -179,13 +195,17 @@ func validateLimitsAndPricing(limits RunLimits, price PricingEvidence, plan doma if err != nil || parsedDate.After(plan.CreatedAt) || plan.CreatedAt.Sub(parsedDate) > maximumPricingAge { return invalidCompiled("price table date") } + if revision, err := pricingEvidenceRevision(price); err != nil || revision != price.Revision { + return invalidCompiled("pricing revision") + } return nil } -func validateBinding(binding EnvelopeBinding, plan domain.Plan) error { +func validateBinding(binding EnvelopeBinding, plan domain.Plan, permissionRevision string) error { if binding.WorkflowID != WorkflowID || binding.PlanID != plan.PlanID || binding.PlanHash != plan.PlanHash || binding.Account != plan.Principal || !sha256Pattern.MatchString(binding.ManifestHash) || !sha256Pattern.MatchString(binding.ObservationRevision) || !validUTC(binding.ObservedAt) || + binding.PermissionRevision != permissionRevision || !sha256Pattern.MatchString(binding.PermissionRevision) || !validUTC(binding.ValidUntil) || !binding.ObservedAt.Before(binding.ValidUntil) || plan.CreatedAt.Before(binding.ObservedAt) || !plan.CreatedAt.Before(binding.ValidUntil) || !sha256Pattern.MatchString(binding.BindingSHA256) { @@ -259,6 +279,7 @@ func clonePayload(payload compiledPayloadV1) compiledPayloadV1 { payload.Plan = clonePlan(payload.Plan) payload.Desired = cloneDesired(payload.Desired) payload.DesiredResources = append([]DesiredResource(nil), payload.DesiredResources...) + payload.Permissions = clonePermissionEvidence(payload.Permissions) payload.CleanupCapabilities = append([]isolation.CleanupCapability(nil), payload.CleanupCapabilities...) payload.Intents = cloneIntents(payload.Intents) payload.Risks = cloneRisks(payload.Risks) @@ -306,6 +327,11 @@ func cloneRisks(value RiskSummary) RiskSummary { return value } +func clonePermissionEvidence(value PermissionEvidence) PermissionEvidence { + value.Grants = append([]PermissionGrant(nil), value.Grants...) + return value +} + func invalidCompiled(field string) error { return fmt.Errorf("%w: %s", ErrInvalidCompiledPlan, field) } diff --git a/internal/isolation/bootstrap/canonical_test.go b/internal/isolation/bootstrap/canonical_test.go index ec63171..d3a3013 100644 --- a/internal/isolation/bootstrap/canonical_test.go +++ b/internal/isolation/bootstrap/canonical_test.go @@ -51,6 +51,12 @@ func TestParseCompiledPlanRejectsRehashedSemanticTampering(t *testing.T) { }{ {name: "prefix without reserved value", mutate: func(value *compiledPayloadV1) { value.Desired.NamePrefix = "other-test-" }}, {name: "labels without prefix", mutate: func(value *compiledPayloadV1) { value.Desired.Labels["managed-by"] = "other" }}, + {name: "unsafe CI principal", mutate: func(value *compiledPayloadV1) { value.Desired.CIPrincipal = "allUsers" }}, + {name: "foreign service account", mutate: func(value *compiledPayloadV1) { + value.Desired.VMPrincipal = "ctrldb-test-vm@foreign-project.iam.gserviceaccount.com" + }}, + {name: "duplicate service account", mutate: func(value *compiledPayloadV1) { value.Desired.VMPrincipal = value.Desired.OperatorPrincipal }}, + {name: "plan validity bypass", mutate: func(value *compiledPayloadV1) { value.Desired.PlanValiditySeconds++ }}, {name: "unsupported cleanup kind", mutate: func(value *compiledPayloadV1) { value.CleanupCapabilities[0] = isolation.CleanupCapability("compute.snapshots") }}, @@ -74,6 +80,8 @@ func TestParseCompiledPlanRejectsRehashedSemanticTampering(t *testing.T) { }}, {name: "cost cap bypass", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumCostMicros++ }}, {name: "estimated cost mismatch", mutate: func(value *compiledPayloadV1) { value.Limits.EstimatedCostMicros++ }}, + {name: "pricing cost driver", mutate: func(value *compiledPayloadV1) { value.Pricing.DiskGiB-- }}, + {name: "permission proof", mutate: func(value *compiledPayloadV1) { value.Permissions.Grants[0].Granted = false }}, {name: "machine shape bypass", mutate: func(value *compiledPayloadV1) { value.Limits.MaximumGuestCPUs++ }}, {name: "resource fingerprint", mutate: func(value *compiledPayloadV1) { value.DesiredResources[0].DesiredStateFingerprint = repeatedHex("c") }}, {name: "plan envelope", mutate: func(value *compiledPayloadV1) { value.Binding.PlanHash = repeatedHex("d") }}, diff --git a/internal/isolation/bootstrap/compile.go b/internal/isolation/bootstrap/compile.go index 3d4b5a6..f469415 100644 --- a/internal/isolation/bootstrap/compile.go +++ b/internal/isolation/bootstrap/compile.go @@ -99,6 +99,9 @@ func Compile(request CompileRequest) (CompiledPlan, error) { return CompiledPlan{}, err } definitions := stepRegistry(desiredResources) + if err := validatePermissionEvidence(request.Permissions, definitions, request.Preflight.Account(), request.Configuration.Project(), request.CreatedAt); err != nil { + return CompiledPlan{}, err + } plan, contract, err := buildPlanValues( request.PlanID, request.Configuration.Project(), request.Configuration.Environment(), request.Preflight.Account(), request.CreatedAt, request.ExpiresAt, request.LocalPolicyHash, request.ApprovedPolicyHash, @@ -114,7 +117,7 @@ func Compile(request CompileRequest) (CompiledPlan, error) { intents := buildIntents(definitions, binding.BindingSHA256) payload := compiledPayloadV1{ Plan: plan, Binding: binding, Desired: desired, DesiredResources: desiredResources, - Limits: limits, Pricing: request.Pricing, + Limits: limits, Pricing: request.Pricing, Permissions: clonePermissionEvidence(request.Permissions), CleanupCapabilities: isolation.InitialCleanupCapabilities(), Intents: intents, Risks: buildRiskSummary(limits), } @@ -130,7 +133,9 @@ func validateCompileRequest(request CompileRequest) error { } if !validUTC(request.CreatedAt) || !validUTC(request.ExpiresAt) || !request.ExpiresAt.After(request.CreatedAt) || - request.ExpiresAt.Sub(request.CreatedAt) < minimumPlanValidity { + request.ExpiresAt.Sub(request.CreatedAt) < minimumPlanValidity || + request.Configuration.PlanValidity() <= 0 || + !request.ExpiresAt.Equal(request.CreatedAt.Add(request.Configuration.PlanValidity())) { return invalidCompile("plan time window") } if !sha256Pattern.MatchString(request.LocalPolicyHash) || @@ -176,7 +181,12 @@ func validatePricing(request CompileRequest) error { price := request.Pricing if price.Schema != PricingSchemaV1 || !sha256Pattern.MatchString(price.Revision) || price.MachineType != request.Configuration.Caps().MaxMachineType() || + price.Region != request.Configuration.Region() || price.Zone != request.Configuration.Zone() || price.GuestCPUs <= 0 || price.MemoryMiB <= 0 || price.EstimatedRunMicros < 0 || + price.DiskGiB != request.Configuration.Caps().MaxDiskGiB() || + price.Instances != int64(request.Configuration.Caps().MaxInstances()) || + price.LifetimeSeconds != int64(request.Configuration.Caps().MaxLifetime()/time.Second) || + price.Currency != "USD" || price.EstimatedRunMicros > maximumExactMicros || !validUTC(price.ObservedAt) || !validUTC(price.ValidUntil) || !price.ObservedAt.Before(price.ValidUntil) || request.CreatedAt.Before(price.ObservedAt) || !request.CreatedAt.Before(price.ValidUntil) { @@ -189,6 +199,9 @@ func validatePricing(request CompileRequest) error { if price.EstimatedRunMicros > request.Configuration.Caps().MaxEstimatedCostMicros() { return blocked("run cost cap") } + if revision, err := pricingEvidenceRevision(price); err != nil || revision != price.Revision { + return invalidCompile("pricing revision") + } matches := 0 for _, machine := range request.Preflight.MachineTypes() { if machine.Name == price.MachineType { @@ -205,6 +218,51 @@ func validatePricing(request CompileRequest) error { return nil } +func pricingEvidenceRevision(value PricingEvidence) (string, error) { + value.Revision = "" + return hashJSON(value) +} + +func validatePermissionEvidence( + evidence PermissionEvidence, + definitions []stepDefinition, + account, project string, + at time.Time, +) error { + if evidence.Schema != PermissionEvidenceSchemaV1 || evidence.Account != account || evidence.Project != project || + !validUTC(evidence.ObservedAt) || !validUTC(evidence.ValidUntil) || + !evidence.ObservedAt.Before(evidence.ValidUntil) || at.Before(evidence.ObservedAt) || !at.Before(evidence.ValidUntil) || + !sha256Pattern.MatchString(evidence.Revision) { + return blocked("permission evidence") + } + expected := expectedPermissionGrants(definitions) + if len(evidence.Grants) != len(expected) { + return blocked("permission evidence") + } + for index, grant := range evidence.Grants { + if grant != expected[index] || !grant.Granted { + return blocked("permission evidence") + } + } + copy := clonePermissionEvidence(evidence) + copy.Revision = "" + revision, err := hashJSON(copy) + if err != nil || revision != evidence.Revision { + return blocked("permission evidence") + } + return nil +} + +func expectedPermissionGrants(definitions []stepDefinition) []PermissionGrant { + result := make([]PermissionGrant, 0) + for _, definition := range definitions { + for _, permission := range definition.permissions { + result = append(result, PermissionGrant{StepID: definition.id, Identity: definition.identity, Permission: permission, Granted: true}) + } + } + return result +} + func buildRunLimits(request CompileRequest) (RunLimits, error) { caps := request.Configuration.Caps() lifetime := caps.MaxLifetime() @@ -234,7 +292,8 @@ func desiredState(request CompileRequest) HarnessDesiredState { CIPrincipal: configuration.CIPrincipal(), OperatorRole: operatorRoleName, DestructiveRole: destructiveRoleName, WipeRunJob: configuration.WipeRunJob(), WipeSchedulerJob: configuration.WipeSchedulerJob(), WipeScheduleUTC: wipeScheduleUTC, - ImageDigest: configuration.ImageDigest(), + ImageDigest: configuration.ImageDigest(), + PlanValiditySeconds: int64(configuration.PlanValidity() / time.Second), } } @@ -429,7 +488,7 @@ func buildPlanValues( SuccessCondition: redact.Sanitize(definition.success), FailureBehavior: definition.failure, } } - contract, err := domain.NewExecutionContract(WorkflowID, "audit-retention-lock", definitions[0].id, + contract, err := domain.NewExecutionContract(WorkflowID, "audit-retention-lock", "k1-retention-lock", domain.PointOfNoReturnMutationObserved, contractSteps) if err != nil { return domain.Plan{}, domain.ExecutionContract{}, fmt.Errorf("%w: execution contract", ErrInvalidCompileRequest) @@ -457,7 +516,7 @@ func buildPlanValues( Downtime: domain.PlanDowntime{ExpectedSeconds: 0, Kind: "none"}, Exposure: domain.ExposureNone, Protection: []redact.Text{redact.Sanitize("production resources remain outside the reserved disposable namespace"), redact.Sanitize("permanent control resources are excluded from disposable cleanup")}, Rollback: domain.PlanRollback{Boundary: "audit-retention-lock", Assets: []domain.PlanRecoveryAsset{}}, - PointOfNoReturn: definitions[0].id, PointOfNoReturnTrigger: domain.PointOfNoReturnMutationObserved, + PointOfNoReturn: "k1-retention-lock", PointOfNoReturnTrigger: domain.PointOfNoReturnMutationObserved, Verification: []redact.Text{redact.Sanitize("every desired provider object is re-observed at exact desired state"), redact.Sanitize("T8 completes every TEST-ISO proof before opening test admission")}, } sealed, err := policy.SealPlan(plan) @@ -498,6 +557,7 @@ func planPreconditions() []domain.PlanPrecondition { {"target-identities-absent", "no exact desired provider identity collides"}, {"bucket-conflict-guarded", "M1-05 must treat create-time global bucket-name conflict as a blocking outcome"}, {"capability-set-closed", "mutation and cleanup are limited to the recorded three-kind capability set"}, + {"permissions-proven", "every declared bootstrap permission has fresh exact positive evidence"}, {"pre-t8-admission", "only this approved WF-TEST-01 envelope may mutate before T8"}, } result := make([]domain.PlanPrecondition, len(values)) @@ -510,7 +570,8 @@ func planPreconditions() []domain.PlanPrecondition { func buildEnvelopeBinding(request CompileRequest, plan domain.Plan) (EnvelopeBinding, error) { binding := EnvelopeBinding{WorkflowID: WorkflowID, PlanID: plan.PlanID, PlanHash: plan.PlanHash, Account: request.Preflight.Account(), ManifestHash: request.Configuration.ManifestHash(), ObservationRevision: request.Preflight.Revision(), - ObservedAt: request.Preflight.ObservedAt(), ValidUntil: request.Preflight.ValidUntil()} + PermissionRevision: request.Permissions.Revision, + ObservedAt: request.Preflight.ObservedAt(), ValidUntil: request.Preflight.ValidUntil()} digest, err := hashJSON(binding) if err != nil { return EnvelopeBinding{}, invalidCompile("envelope binding") @@ -544,15 +605,16 @@ func buildRiskSummary(limits RunLimits) RiskSummary { } func stepRegistry(resources []DesiredResource) []stepDefinition { - allPreconditions := []string{"manifest-bound", "provider-context-bound", "observation-fresh", "schemas-complete", "cidr-clear", "api-set-enabled", "no-public-mongodb", "machine-cap-resolved", "cost-cap-respected", "target-identities-absent", "bucket-conflict-guarded", "capability-set-closed", "pre-t8-admission"} + allPreconditions := []string{"manifest-bound", "provider-context-bound", "observation-fresh", "schemas-complete", "cidr-clear", "api-set-enabled", "no-public-mongodb", "machine-cap-resolved", "cost-cap-respected", "target-identities-absent", "bucket-conflict-guarded", "capability-set-closed", "permissions-proven", "pre-t8-admission"} retry3210 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 2, MaxBackoffSeconds: 10} retry3520 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 5, MaxBackoffSeconds: 20} retry5220 := domain.RetryPolicy{MaxAttempts: 5, InitialBackoffSeconds: 2, MaxBackoffSeconds: 20} retry3530 := domain.RetryPolicy{MaxAttempts: 3, InitialBackoffSeconds: 5, MaxBackoffSeconds: 30} once := domain.RetryPolicy{MaxAttempts: 1} return []stepDefinition{ - {id: "k1-audit-bootstrap", kind: IntentAuditBootstrap, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket"}, preconditions: allPreconditions, verification: []string{"envelope hash and server generation match", "retention is locked for 365 days", "archive lifecycle has no delete rule"}, retry: retry3210, timeoutSeconds: 120, cancelSafe: false, compensation: "before retention lock remove only an audit bucket created by this operation; after lock pause and preserve", ponr: PONRAuditRetentionLock, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update", "storage.objects.create", "storage.objects.get"}, summary: "create and verify the audit bootstrap handoff, then lock retention", success: "the exact envelope is durable and the compliant audit retention policy is locked", failure: domain.FailurePause}, - {id: "k2-control-bucket", kind: IntentControlBucket, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k1-audit-bootstrap"}, preconditions: allPreconditions, verification: []string{"control bucket desired state matches exactly"}, retry: retry3210, timeoutSeconds: 60, cancelSafe: true, compensation: "remove only a preexisting-empty control bucket created by this operation before durable state is written", ponr: PONRReversible, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update"}, summary: "create the permanent versioned control bucket", success: "the control bucket has exact UBLA, PAP, versioning, and soft-delete state", failure: domain.FailureRollback}, + {id: "k1-audit-bootstrap", kind: IntentAuditBootstrap, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket"}, preconditions: allPreconditions, verification: []string{"envelope hash and server generation match", "archive lifecycle has no delete rule"}, retry: retry3210, timeoutSeconds: 120, cancelSafe: true, compensation: "before retention lock remove only an audit bucket created by this operation", ponr: PONRReversible, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.objects.create", "storage.objects.get"}, summary: "create and verify the audit bootstrap handoff", success: "the exact envelope is durable and verified before retention is locked", failure: domain.FailurePause}, + {id: "k1-retention-lock", kind: IntentAuditRetention, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket"}, dependencies: []string{"k1-audit-bootstrap"}, preconditions: allPreconditions, verification: []string{"retention is locked for 365 days", "archive lifecycle has no delete rule"}, retry: retry3210, timeoutSeconds: 60, cancelSafe: false, compensation: "after the retention-lock mutation is observed pause and preserve the permanent audit bucket", ponr: PONRAuditRetentionLock, permissions: []string{"storage.buckets.get", "storage.buckets.update"}, summary: "lock the verified audit bucket retention policy", success: "the compliant 365-day audit retention policy is irreversibly locked", failure: domain.FailurePause}, + {id: "k2-control-bucket", kind: IntentControlBucket, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k1-retention-lock"}, preconditions: allPreconditions, verification: []string{"control bucket desired state matches exactly"}, retry: retry3210, timeoutSeconds: 60, cancelSafe: true, compensation: "remove only a preexisting-empty control bucket created by this operation before durable state is written", ponr: PONRReversible, permissions: []string{"storage.buckets.create", "storage.buckets.get", "storage.buckets.update"}, summary: "create the permanent versioned control bucket", success: "the control bucket has exact UBLA, PAP, versioning, and soft-delete state", failure: domain.FailureRollback}, {id: "k3-bucket-iam", kind: IntentBucketIAM, identity: domain.IdentityHuman, resourceIDs: []string{"audit-bucket", "control-bucket"}, dependencies: []string{"k2-control-bucket"}, preconditions: allPreconditions, verification: []string{"bucket IAM policies equal the closed rendered policy"}, retry: retry5220, timeoutSeconds: 120, cancelSafe: true, compensation: "remove only IAM bindings added by this operation", ponr: PONRReversible, permissions: []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}, summary: "apply exact control and audit bucket IAM", success: "bucket IAM equals the closed prefix-scoped policy", failure: domain.FailureRollback}, {id: "k4-seed-control", kind: IntentSeedControl, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k3-bucket-iam"}, preconditions: allPreconditions, verification: []string{"seed objects exist and preexisting generations are unchanged"}, retry: retry3210, timeoutSeconds: 30, cancelSafe: true, compensation: "preserve all create-only seed objects and converge from their exact contents", ponr: PONRReversible, permissions: []string{"storage.objects.create", "storage.objects.get"}, summary: "create the exact control-store seed objects", success: "all required create-only seed objects are present", failure: domain.FailurePause}, {id: "k5-lock-round-trip", kind: IntentLockRoundTrip, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"k4-seed-control"}, preconditions: allPreconditions, verification: []string{"generation-preconditioned acquire and release succeeds"}, retry: once, timeoutSeconds: 30, cancelSafe: true, compensation: "release only the lock generation acquired by this operation", ponr: PONRReversible, permissions: []string{"storage.objects.create", "storage.objects.get", "storage.objects.update"}, summary: "prove the control-store lock round trip", success: "one generation-bound lock is acquired and released", failure: domain.FailurePause}, @@ -562,8 +624,8 @@ func stepRegistry(resources []DesiredResource) []stepDefinition { {id: "t4-firewall", kind: IntentFirewall, identity: domain.IdentityHuman, resourceIDs: []string{"test-iap-firewall", "test-internal-firewall"}, dependencies: []string{"t3-nat"}, preconditions: allPreconditions, verification: []string{"IAP SSH and node-internal MongoDB rules match exactly", "no public or non-test source and target is present"}, retry: retry3520, timeoutSeconds: 60, cancelSafe: true, compensation: "delete only firewall rules recorded as created by this operation", ponr: PONRReversible, permissions: []string{"compute.firewalls.create", "compute.firewalls.get"}, summary: "create the two closed test firewall rules", success: "only the exact IAP SSH and test-node MongoDB rules exist", failure: domain.FailureRollback}, {id: "t5-identities", kind: IntentIdentities, identity: domain.IdentityHuman, resourceIDs: []string{"test-operator-sa", "test-destructive-sa", "test-vm-sa", "test-wipe-sa", "test-operator-role", "test-destructive-role"}, dependencies: []string{"t4-firewall"}, preconditions: allPreconditions, verification: []string{"service accounts have no keys", "role and conditional binding fingerprints match", "expected allows and denies are proven exactly"}, retry: retry5220, timeoutSeconds: 180, cancelSafe: true, compensation: "remove only roles, bindings, and service accounts created by this operation", ponr: PONRReversible, permissions: []string{"iam.roles.create", "iam.roles.get", "iam.roles.update", "iam.serviceAccounts.create", "iam.serviceAccounts.get", "iam.serviceAccounts.setIamPolicy", "resourcemanager.projects.getIamPolicy", "resourcemanager.projects.setIamPolicy"}, summary: "create test identities, closed roles, and conditional bindings", success: "identity desired state and exact permission matrix are proven", failure: domain.FailureRollback}, {id: "t6-control-prefix", kind: IntentControlPrefix, identity: domain.IdentityHuman, resourceIDs: []string{"control-bucket"}, dependencies: []string{"t5-identities"}, preconditions: allPreconditions, verification: []string{"only the test control prefix bindings are present"}, retry: retry5220, timeoutSeconds: 60, cancelSafe: true, compensation: "remove only prefix bindings added by this operation", ponr: PONRReversible, permissions: []string{"storage.buckets.getIamPolicy", "storage.buckets.setIamPolicy"}, summary: "bind test identities to the control-store test prefix", success: "the exact test-prefix IAM bindings are present", failure: domain.FailureRollback}, - {id: "t7-nightly-wipe", kind: IntentNightlyWipe, identity: domain.IdentityHuman, resourceIDs: []string{"test-wipe-job", "test-wipe-scheduler"}, dependencies: []string{"t6-control-prefix"}, preconditions: allPreconditions, verification: []string{"wipe job image and identity match", "scheduler uses the exact Run Jobs v2 OAuth target", "first run reports zero deletions"}, retry: retry3530, timeoutSeconds: 180, cancelSafe: true, compensation: "delete only the job and scheduler recorded as created by this operation", ponr: PONRReversible, permissions: []string{"cloudscheduler.jobs.create", "cloudscheduler.jobs.get", "iam.serviceAccounts.actAs", "run.jobs.create", "run.jobs.get"}, summary: "create and dry-run the immutable nightly wipe job", success: "the pinned wipe job and UTC scheduler exist and the first run deletes nothing", failure: domain.FailureRollback}, - {id: "t8-isolation-gate", kind: IntentIsolationGate, identity: domain.IdentityHuman, resourceIDs: desiredResourceIDs(resources), dependencies: []string{"t7-nightly-wipe"}, preconditions: allPreconditions, verification: []string{"every TEST-ISO proof passes", "harness fingerprints and cleanup capabilities match", "pending and unusable may transition to open and usable"}, retry: once, timeoutSeconds: 900, cancelSafe: true, compensation: "on failure retain pending and unusable state and keep TEST-I, TEST-D, and unrelated mutation blocked", ponr: PONRVerificationOnly, transition: &HarnessTransition{FromBootstrapPhase: "pending", FromTestUsability: "unusable", ToBootstrapPhase: "open", ToTestUsability: "usable"}, permissions: []string{"cloudscheduler.jobs.get", "compute.firewalls.get", "compute.networks.get", "iam.serviceAccounts.get", "resourcemanager.projects.getIamPolicy", "run.jobs.get"}, summary: "run the complete TEST-ISO gate without opening admission early", success: "every T8 proof passes and the proposed state transition is eligible to persist", failure: domain.FailurePause}, + {id: "t7-nightly-wipe", kind: IntentNightlyWipe, identity: domain.IdentityHuman, resourceIDs: []string{"test-wipe-job", "test-wipe-scheduler"}, dependencies: []string{"t6-control-prefix"}, preconditions: allPreconditions, verification: []string{"wipe job image and identity match", "scheduler uses the exact Run Jobs v2 OAuth target", "first run reports zero deletions"}, retry: retry3530, timeoutSeconds: 180, cancelSafe: true, compensation: "delete only the job and scheduler recorded as created by this operation", ponr: PONRReversible, permissions: []string{"cloudscheduler.jobs.create", "cloudscheduler.jobs.get", "iam.serviceAccounts.actAs", "run.jobs.create", "run.jobs.get", "run.jobs.run"}, summary: "create and dry-run the immutable nightly wipe job", success: "the pinned wipe job and UTC scheduler exist and the first run deletes nothing", failure: domain.FailureRollback}, + {id: "t8-isolation-gate", kind: IntentIsolationGate, identity: domain.IdentityHuman, resourceIDs: desiredResourceIDs(resources), dependencies: []string{"t7-nightly-wipe"}, preconditions: allPreconditions, verification: []string{"every TEST-ISO proof passes", "harness fingerprints and cleanup capabilities match", "pending and unusable may transition to open and usable"}, retry: once, timeoutSeconds: 900, cancelSafe: true, compensation: "on failure retain pending and unusable state and keep TEST-I, TEST-D, and unrelated mutation blocked", ponr: PONRVerificationOnly, transition: &HarnessTransition{FromBootstrapPhase: "pending", FromTestUsability: "unusable", ToBootstrapPhase: "open", ToTestUsability: "usable"}, permissions: []string{"cloudscheduler.jobs.get", "compute.firewalls.get", "compute.networks.get", "compute.routers.get", "compute.subnetworks.get", "iam.roles.get", "iam.serviceAccounts.get", "iam.serviceAccounts.getIamPolicy", "resourcemanager.projects.getIamPolicy", "run.jobs.get", "storage.buckets.get", "storage.buckets.getIamPolicy"}, summary: "run the complete TEST-ISO gate without opening admission early", success: "every T8 proof passes and the proposed state transition is eligible to persist", failure: domain.FailurePause}, } } diff --git a/internal/isolation/bootstrap/compile_test.go b/internal/isolation/bootstrap/compile_test.go index 314e93d..646c591 100644 --- a/internal/isolation/bootstrap/compile_test.go +++ b/internal/isolation/bootstrap/compile_test.go @@ -23,7 +23,7 @@ func TestCompileProducesCompleteWFTestPlan(t *testing.T) { compiled := mustCompile(t, validCompileRequest(t)) wantSteps := []string{ - "k1-audit-bootstrap", "k2-control-bucket", "k3-bucket-iam", "k4-seed-control", "k5-lock-round-trip", + "k1-audit-bootstrap", "k1-retention-lock", "k2-control-bucket", "k3-bucket-iam", "k4-seed-control", "k5-lock-round-trip", "t1-network", "t2-subnet", "t3-nat", "t4-firewall", "t5-identities", "t6-control-prefix", "t7-nightly-wipe", "t8-isolation-gate", } @@ -69,7 +69,7 @@ func TestCompileProducesCompleteWFTestPlan(t *testing.T) { if got := compiled.CleanupCapabilities(); !slices.Equal(got, isolation.InitialCleanupCapabilities()) { t.Fatalf("CleanupCapabilities() = %v; want %v", got, isolation.InitialCleanupCapabilities()) } - if compiled.Plan().PointOfNoReturn != "k1-audit-bootstrap" || compiled.Risks().ExpectedDowntimeSeconds != 0 || + if compiled.Plan().PointOfNoReturn != "k1-retention-lock" || compiled.Risks().ExpectedDowntimeSeconds != 0 || compiled.Risks().ProductionExposure != "none" { t.Fatal("compiled review surface lost the retention boundary or safety summary") } @@ -160,7 +160,7 @@ func TestCompileRejectsStaleOrMismatchedInputs(t *testing.T) { }{ {name: "stale observation", mutate: func(value *CompileRequest) { value.CreatedAt = testNow.Add(4 * time.Minute) - value.ExpiresAt = value.CreatedAt.Add(31 * time.Minute) + value.ExpiresAt = value.CreatedAt.Add(value.Configuration.PlanValidity()) }, want: ErrPlanBlocked}, {name: "manifest project", mutate: func(value *CompileRequest) { value.Preflight = validPreflight(t, "other-project", testRegion, testZone, nil, nil, nil) @@ -172,6 +172,7 @@ func TestCompileRejectsStaleOrMismatchedInputs(t *testing.T) { {name: "missing preflight", mutate: func(value *CompileRequest) { value.Preflight = observation.HarnessPreflight{} }, want: ErrPlanBlocked}, {name: "policy mismatch", mutate: func(value *CompileRequest) { value.ApprovedPolicyHash = repeatedHex("c") }, want: ErrPlanBlocked}, {name: "short validity", mutate: func(value *CompileRequest) { value.ExpiresAt = value.CreatedAt.Add(29 * time.Minute) }, want: ErrInvalidCompileRequest}, + {name: "longer than manifest validity", mutate: func(value *CompileRequest) { value.ExpiresAt = value.CreatedAt.Add(61 * time.Minute) }, want: ErrInvalidCompileRequest}, {name: "non UTC creation", mutate: func(value *CompileRequest) { value.CreatedAt = value.CreatedAt.In(time.FixedZone("offset", 3600)) }, want: ErrInvalidCompileRequest}, } for _, test := range tests { @@ -277,6 +278,12 @@ func TestCompileRejectsUnresolvedOrAmbiguousPricing(t *testing.T) { mutate func(*CompileRequest) }{ {name: "unobserved machine", mutate: func(value *CompileRequest) { value.Pricing.MachineType = "n2-standard-2" }}, + {name: "wrong region", mutate: func(value *CompileRequest) { value.Pricing.Region = "us-east1" }}, + {name: "wrong zone", mutate: func(value *CompileRequest) { value.Pricing.Zone = "us-central1-b" }}, + {name: "wrong disk", mutate: func(value *CompileRequest) { value.Pricing.DiskGiB-- }}, + {name: "wrong count", mutate: func(value *CompileRequest) { value.Pricing.Instances-- }}, + {name: "wrong lifetime", mutate: func(value *CompileRequest) { value.Pricing.LifetimeSeconds-- }}, + {name: "wrong currency", mutate: func(value *CompileRequest) { value.Pricing.Currency = "EUR" }}, {name: "numeric mismatch", mutate: func(value *CompileRequest) { value.Pricing.GuestCPUs++ }}, {name: "over cost cap", mutate: func(value *CompileRequest) { value.Pricing.EstimatedRunMicros = 25_000_001 }}, {name: "float precision overflow", mutate: func(value *CompileRequest) { value.Pricing.EstimatedRunMicros = maximumExactMicros + 1 }}, @@ -290,6 +297,7 @@ func TestCompileRejectsUnresolvedOrAmbiguousPricing(t *testing.T) { t.Run(test.name, func(t *testing.T) { request := validCompileRequest(t) test.mutate(&request) + refreshPricingRevision(t, &request.Pricing) if _, err := Compile(request); err == nil { t.Fatal("Compile() succeeded; want fail-closed error") } @@ -318,6 +326,7 @@ func TestCompileAcceptsExactIntegerCostBoundary(t *testing.T) { request := validCompileRequest(t) request.Pricing.EstimatedRunMicros = 25_000_000 + refreshPricingRevision(t, &request.Pricing) compiled := mustCompile(t, request) if compiled.Limits().EstimatedCostMicros != compiled.Limits().MaximumCostMicros { t.Fatal("exact micro-USD cap boundary was not preserved") @@ -360,11 +369,34 @@ func TestCompiledPlanGettersAreDetached(t *testing.T) { capabilities[0] = isolation.CleanupCapability("tampered") plan := compiled.Plan() plan.Steps[0].ID = "tampered" + permissions := compiled.PermissionEvidence() + permissions.Grants[0].Granted = false if !bytes.Equal(original, mustCanonical(t, compiled)) { t.Fatal("detached getter mutation changed compiled plan") } } +func TestStepPermissionsCoverWipeExecutionAndEveryT8Read(t *testing.T) { + t.Parallel() + + compiled := mustCompile(t, validCompileRequest(t)) + byStep := make(map[string][]string) + for _, grant := range compiled.PermissionEvidence().Grants { + byStep[grant.StepID] = append(byStep[grant.StepID], grant.Permission) + } + if !slices.Contains(byStep["t7-nightly-wipe"], "run.jobs.run") { + t.Fatal("T7 omits run.jobs.run required by its first-run verification") + } + wantT8 := []string{ + "cloudscheduler.jobs.get", "compute.firewalls.get", "compute.networks.get", "compute.routers.get", + "compute.subnetworks.get", "iam.roles.get", "iam.serviceAccounts.get", "iam.serviceAccounts.getIamPolicy", + "resourcemanager.projects.getIamPolicy", "run.jobs.get", "storage.buckets.get", "storage.buckets.getIamPolicy", + } + if !slices.Equal(byStep["t8-isolation-gate"], wantT8) { + t.Fatalf("T8 permissions = %v; want %v", byStep["t8-isolation-gate"], wantT8) + } +} + func zeroHarnessConfiguration() config.HarnessConfiguration { return config.HarnessConfiguration{} } func TestStepRegistryHasNoRepresentableTestRunOrUnrelatedIntent(t *testing.T) { @@ -372,7 +404,7 @@ func TestStepRegistryHasNoRepresentableTestRunOrUnrelatedIntent(t *testing.T) { compiled := mustCompile(t, validCompileRequest(t)) allowed := []IntentKind{ - IntentAuditBootstrap, IntentControlBucket, IntentBucketIAM, IntentSeedControl, IntentLockRoundTrip, + IntentAuditBootstrap, IntentAuditRetention, IntentControlBucket, IntentBucketIAM, IntentSeedControl, IntentLockRoundTrip, IntentNetwork, IntentSubnet, IntentNAT, IntentFirewall, IntentIdentities, IntentControlPrefix, IntentNightlyWipe, IntentIsolationGate, } @@ -387,3 +419,53 @@ func TestStepRegistryHasNoRepresentableTestRunOrUnrelatedIntent(t *testing.T) { t.Fatal("compiled cleanup capability set is not the closed initial set") } } + +func TestCompileRequiresExactFreshPermissionEvidence(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + mutate func(*PermissionEvidence) + }{ + {name: "missing grant", mutate: func(value *PermissionEvidence) { value.Grants = value.Grants[1:] }}, + {name: "extra grant", mutate: func(value *PermissionEvidence) { value.Grants = append(value.Grants, value.Grants[0]) }}, + {name: "denied grant", mutate: func(value *PermissionEvidence) { value.Grants[0].Granted = false }}, + {name: "wrong permission", mutate: func(value *PermissionEvidence) { value.Grants[0].Permission = "storage.buckets.delete" }}, + {name: "wrong identity", mutate: func(value *PermissionEvidence) { value.Grants[0].Identity = domain.IdentityOperator }}, + {name: "wrong account", mutate: func(value *PermissionEvidence) { value.Account = "other@example.invalid" }}, + {name: "wrong project", mutate: func(value *PermissionEvidence) { value.Project = "other-project" }}, + {name: "stale", mutate: func(value *PermissionEvidence) { value.ValidUntil = testNow }}, + {name: "unknown schema", mutate: func(value *PermissionEvidence) { value.Schema = "permission-evidence/v2" }}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + request := validCompileRequest(t) + test.mutate(&request.Permissions) + refreshPermissionRevision(t, &request.Permissions) + if _, err := Compile(request); !errors.Is(err, ErrPlanBlocked) { + t.Fatalf("Compile() error = %v; want ErrPlanBlocked", err) + } + }) + } +} + +func TestCompiledPermissionsMatchFreshEvidence(t *testing.T) { + t.Parallel() + + request := validCompileRequest(t) + compiled := mustCompile(t, request) + if !reflect.DeepEqual(compiled.PermissionEvidence(), request.Permissions) { + t.Fatal("compiled permission evidence differs from the exact fresh input") + } + permissions := compiled.Plan().Permissions + if len(permissions) != len(request.Permissions.Grants) { + t.Fatalf("plan permissions = %d; want %d", len(permissions), len(request.Permissions.Grants)) + } + for index, permission := range permissions { + grant := request.Permissions.Grants[index] + if permission.StepID != grant.StepID || permission.Identity != grant.Identity || + permission.Permission != grant.Permission || !permission.Granted { + t.Fatalf("plan permission %d does not match evidence", index) + } + } +} diff --git a/internal/isolation/bootstrap/helpers_test.go b/internal/isolation/bootstrap/helpers_test.go index 48d50d3..fedd3ff 100644 --- a/internal/isolation/bootstrap/helpers_test.go +++ b/internal/isolation/bootstrap/helpers_test.go @@ -26,26 +26,65 @@ var testNow = time.Date(2026, 9, 7, 12, 1, 0, 0, time.UTC) func validCompileRequest(t *testing.T) CompileRequest { t.Helper() - return CompileRequest{ + request := CompileRequest{ Configuration: validHarnessConfiguration(t), Preflight: validPreflight(t, testProject, testRegion, testZone, nil, nil, nil), PlanID: "plan-0123456789abcdef", CreatedAt: testNow, - ExpiresAt: testNow.Add(31 * time.Minute), + ExpiresAt: testNow.Add(time.Hour), LocalPolicyHash: repeatedHex("a"), ApprovedPolicyHash: repeatedHex("a"), Pricing: PricingEvidence{ MachineType: "e2-medium", + Region: testRegion, + Zone: testZone, GuestCPUs: 2, MemoryMiB: 4096, + DiskGiB: 100, + Instances: 3, + LifetimeSeconds: int64((8 * time.Hour) / time.Second), EstimatedRunMicros: 5_000_000, + Currency: "USD", PriceTableDate: "2026-09-07", Schema: PricingSchemaV1, - Revision: repeatedHex("b"), ObservedAt: testNow.Add(-time.Minute), ValidUntil: testNow.Add(4 * time.Minute), }, } + refreshPricingRevision(t, &request.Pricing) + resources, err := buildDesiredResources(desiredState(request)) + if err != nil { + t.Fatalf("buildDesiredResources() unexpected error: %v", err) + } + request.Permissions = PermissionEvidence{ + Account: testAccount, Project: testProject, Schema: PermissionEvidenceSchemaV1, + ObservedAt: testNow.Add(-time.Minute), ValidUntil: testNow.Add(4 * time.Minute), + Grants: expectedPermissionGrants(stepRegistry(resources)), + } + refreshPermissionRevision(t, &request.Permissions) + return request +} + +func refreshPricingRevision(t *testing.T, evidence *PricingEvidence) { + t.Helper() + + revision, err := pricingEvidenceRevision(*evidence) + if err != nil { + t.Fatalf("pricingEvidenceRevision() unexpected error: %v", err) + } + evidence.Revision = revision +} + +func refreshPermissionRevision(t *testing.T, evidence *PermissionEvidence) { + t.Helper() + + copy := clonePermissionEvidence(*evidence) + copy.Revision = "" + revision, err := hashJSON(copy) + if err != nil { + t.Fatalf("hashJSON(permission evidence) unexpected error: %v", err) + } + evidence.Revision = revision } func validHarnessConfiguration(t *testing.T) config.HarnessConfiguration { diff --git a/internal/isolation/bootstrap/types.go b/internal/isolation/bootstrap/types.go index 8576336..20cb9bf 100644 --- a/internal/isolation/bootstrap/types.go +++ b/internal/isolation/bootstrap/types.go @@ -15,8 +15,9 @@ import ( ) const ( - CompiledPlanSchemaV1 = "ctrldb.ctrlboard.dev/wf-test-plan/v1" - WorkflowID = isolation.WFTestWorkflowID + CompiledPlanSchemaV1 = "ctrldb.ctrlboard.dev/wf-test-plan/v1" + PermissionEvidenceSchemaV1 = "ctrldb.ctrlboard.dev/permission-evidence/v1" + WorkflowID = isolation.WFTestWorkflowID ) var ( @@ -36,15 +37,22 @@ type CompileRequest struct { LocalPolicyHash string ApprovedPolicyHash string Pricing PricingEvidence + Permissions PermissionEvidence } // PricingEvidence is a fresh, externally obtained integer-micro-USD estimate. // M1-04 validates and binds it but never performs pricing I/O. type PricingEvidence struct { MachineType string `json:"machineType"` + Region string `json:"region"` + Zone string `json:"zone"` GuestCPUs int64 `json:"guestCpus"` MemoryMiB int64 `json:"memoryMiB"` + DiskGiB int64 `json:"diskGiB"` + Instances int64 `json:"instances"` + LifetimeSeconds int64 `json:"lifetimeSeconds"` EstimatedRunMicros int64 `json:"estimatedRunMicros"` + Currency string `json:"currency"` PriceTableDate string `json:"priceTableDate"` Schema string `json:"schema"` Revision string `json:"revision"` @@ -52,6 +60,27 @@ type PricingEvidence struct { ValidUntil time.Time `json:"validUntil"` } +// PermissionGrant is one exact positive permission observation. Missing, +// denied, duplicated, or additional entries make the evidence unusable. +type PermissionGrant struct { + StepID string `json:"stepId"` + Identity domain.ExecutionIdentity `json:"identity"` + Permission string `json:"permission"` + Granted bool `json:"granted"` +} + +// PermissionEvidence binds the complete pre-mutation grant set to the human, +// project, and freshness window used to compile the plan. +type PermissionEvidence struct { + Account string `json:"account"` + Project string `json:"project"` + Schema string `json:"schema"` + Revision string `json:"revision"` + ObservedAt time.Time `json:"observedAt"` + ValidUntil time.Time `json:"validUntil"` + Grants []PermissionGrant `json:"grants"` +} + // ResourceKind is the closed set of provider objects referenced by M1-04. type ResourceKind string @@ -120,6 +149,7 @@ type HarnessDesiredState struct { WipeSchedulerJob string `json:"wipeSchedulerJob"` WipeScheduleUTC string `json:"wipeScheduleUtc"` ImageDigest string `json:"imageDigest"` + PlanValiditySeconds int64 `json:"planValiditySeconds"` } // RunLimits is the exact future disposable-run ceiling recorded by the plan. @@ -143,6 +173,7 @@ type EnvelopeBinding struct { Account string `json:"account"` ManifestHash string `json:"manifestHash"` ObservationRevision string `json:"observationRevision"` + PermissionRevision string `json:"permissionRevision"` ObservedAt time.Time `json:"observedAt"` ValidUntil time.Time `json:"validUntil"` BindingSHA256 string `json:"bindingSha256"` @@ -153,6 +184,7 @@ type IntentKind string const ( IntentAuditBootstrap IntentKind = "audit-bootstrap" + IntentAuditRetention IntentKind = "audit-retention-lock" IntentControlBucket IntentKind = "control-bucket" IntentBucketIAM IntentKind = "bucket-iam" IntentSeedControl IntentKind = "seed-control" @@ -225,6 +257,7 @@ type compiledPayloadV1 struct { DesiredResources []DesiredResource `json:"desiredResources"` Limits RunLimits `json:"limits"` Pricing PricingEvidence `json:"pricing"` + Permissions PermissionEvidence `json:"permissions"` CleanupCapabilities []isolation.CleanupCapability `json:"cleanupCapabilities"` Intents []StepIntent `json:"intents"` Risks RiskSummary `json:"risks"` @@ -255,6 +288,9 @@ func (value CompiledPlan) DesiredResources() []DesiredResource { } func (value CompiledPlan) Limits() RunLimits { return value.payload.Limits } func (value CompiledPlan) Pricing() PricingEvidence { return value.payload.Pricing } +func (value CompiledPlan) PermissionEvidence() PermissionEvidence { + return clonePermissionEvidence(value.payload.Permissions) +} func (value CompiledPlan) CleanupCapabilities() []isolation.CleanupCapability { return append([]isolation.CleanupCapability(nil), value.payload.CleanupCapabilities...) } From cd38a9e681fc0ddffb7d2aede2e5f60eb56e843e Mon Sep 17 00:00:00 2001 From: Syed <40798652+thelostorbital@users.noreply.github.com> Date: Mon, 7 Sep 2026 08:06:24 +0530 Subject: [PATCH 3/3] fix: keep bootstrap buckets distinct --- internal/config/harness.go | 3 +++ internal/config/harness_test.go | 4 ++++ internal/isolation/bootstrap/canonical.go | 3 +++ internal/isolation/bootstrap/canonical_test.go | 1 + 4 files changed, 11 insertions(+) diff --git a/internal/config/harness.go b/internal/config/harness.go index da81b03..1491cf7 100644 --- a/internal/config/harness.go +++ b/internal/config/harness.go @@ -189,6 +189,9 @@ func HarnessConfigurationFromManifest(document ManifestDocument) (HarnessConfigu return HarnessConfiguration{}, fmt.Errorf("%w: required value is absent", ErrInvalidHarnessConfiguration) } } + if wire.Spec.Control.StateBucket == wire.Spec.Control.AuditBucket { + return HarnessConfiguration{}, fmt.Errorf("%w: control and audit buckets must be distinct", ErrInvalidHarnessConfiguration) + } if wire.Spec.TestIsolation.Caps.MaxDiskGiB <= 0 || wire.Spec.TestIsolation.Caps.MaxInstances <= 0 { return HarnessConfiguration{}, fmt.Errorf("%w: invalid numeric caps", ErrInvalidHarnessConfiguration) } diff --git a/internal/config/harness_test.go b/internal/config/harness_test.go index fe95896..adaa710 100644 --- a/internal/config/harness_test.go +++ b/internal/config/harness_test.go @@ -169,6 +169,10 @@ func TestHarnessConfigurationRequiresCompleteValidation(t *testing.T) { {name: "disabled wipe reconciler", mutate: func(t *testing.T, manifest map[string]any) { nestedMap(t, manifest, "spec", "reconciler")["enabled"] = false }}, + {name: "shared control and audit bucket", mutate: func(t *testing.T, manifest map[string]any) { + control := nestedMap(t, manifest, "spec", "control") + control["auditBucket"] = control["stateBucket"] + }}, {name: "shared operator and destructive identity", mutate: func(t *testing.T, manifest map[string]any) { isolation := nestedMap(t, manifest, "spec", "testIsolation") isolation["destructiveServiceAccount"] = isolation["operatorServiceAccount"] diff --git a/internal/isolation/bootstrap/canonical.go b/internal/isolation/bootstrap/canonical.go index 35e5d20..2722c9e 100644 --- a/internal/isolation/bootstrap/canonical.go +++ b/internal/isolation/bootstrap/canonical.go @@ -161,6 +161,9 @@ func validateDesiredState(desired HarnessDesiredState, plan domain.Plan) error { return invalidCompiled("desired value") } } + if desired.ControlBucket == desired.AuditBucket { + return invalidCompiled("distinct control and audit buckets") + } if desired.IAPFirewall != iapFirewallName || desired.InternalFirewall != internalFirewallName || desired.NodeTag != testNodeTag || desired.OperatorRole != operatorRoleName || desired.DestructiveRole != destructiveRoleName || desired.WipeScheduleUTC != wipeScheduleUTC || diff --git a/internal/isolation/bootstrap/canonical_test.go b/internal/isolation/bootstrap/canonical_test.go index d3a3013..975fec3 100644 --- a/internal/isolation/bootstrap/canonical_test.go +++ b/internal/isolation/bootstrap/canonical_test.go @@ -56,6 +56,7 @@ func TestParseCompiledPlanRejectsRehashedSemanticTampering(t *testing.T) { value.Desired.VMPrincipal = "ctrldb-test-vm@foreign-project.iam.gserviceaccount.com" }}, {name: "duplicate service account", mutate: func(value *compiledPayloadV1) { value.Desired.VMPrincipal = value.Desired.OperatorPrincipal }}, + {name: "shared control and audit bucket", mutate: func(value *compiledPayloadV1) { value.Desired.AuditBucket = value.Desired.ControlBucket }}, {name: "plan validity bypass", mutate: func(value *compiledPayloadV1) { value.Desired.PlanValiditySeconds++ }}, {name: "unsupported cleanup kind", mutate: func(value *compiledPayloadV1) { value.CleanupCapabilities[0] = isolation.CleanupCapability("compute.snapshots")