From d1d30075b35c4b63739e93eca20bdac06b553d1b Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 12:59:33 +0500 Subject: [PATCH 1/2] fix(governance): honor operation-local estate selection Signed-off-by: rldyourmnd --- CHANGELOG.md | 3 + core/app/github_governance_operations.go | 2 +- core/app/github_governance_operations_test.go | 61 +++++++++++++++++++ core/app/github_readonly.go | 2 +- core/app/services.go | 9 ++- core/context/resolver.go | 16 +++++ docs/runbooks/github-ruleset-reconcile.md | 3 + 7 files changed, 93 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8519a..49efba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ Versioning. ## [Unreleased] +- Honor explicit estate selection for governance policy compilation and local + comparison, retaining authority checks and the same root during apply. + - Select continuous development for the GDS repository itself and document explicit ruleset removal plus cautious readback after ambiguous write errors. Generic policy defaults remain opt-in for other consumers. diff --git a/core/app/github_governance_operations.go b/core/app/github_governance_operations.go index f16ae51..a00ac56 100644 --- a/core/app/github_governance_operations.go +++ b/core/app/github_governance_operations.go @@ -350,7 +350,7 @@ func (services *Services) githubGovernanceContext( }) return githubGovernanceOperationContext{}, &envelope } - estateRoot, anchor, findings := services.policyInputs(ctx, path) + estateRoot, anchor, findings := services.policyInputsWithEstateRoot(ctx, path, options.EstateRoot) if len(findings) != 0 { envelope := domain.NewEnvelope(command, classifyFindings(findings), nil, findings...) return githubGovernanceOperationContext{}, &envelope diff --git a/core/app/github_governance_operations_test.go b/core/app/github_governance_operations_test.go index 5bfbbaa..f8b0abc 100644 --- a/core/app/github_governance_operations_test.go +++ b/core/app/github_governance_operations_test.go @@ -138,3 +138,64 @@ func appGovernanceOperationServer(t *testing.T) *httptest.Server { } })) } + +func TestGovernanceExplicitEstateOverridesEnvironmentForPolicy(t *testing.T) { + root := appTestRepositoryRoot(t) + registered := os.Getenv("GDS_ESTATE_ROOT") + explicit := t.TempDir() + for _, directory := range []string{".gds", "estate", "policies"} { + if err := copyAppTestTree(registered, explicit, directory); err != nil { + t.Fatal(err) + } + } + // Only the explicit estate contains the repository's selected profile. + if err := os.Remove(filepath.Join(registered, "policies", "stacks", "continuous-development.yaml")); err != nil { + t.Fatal(err) + } + runtimePath := appTestRuntimeConfig(t, root) + services, err := NewServices(DefaultClock) + if err != nil { + t.Fatal(err) + } + server := appGovernanceOperationServer(t) + client := server.Client() + client.Timeout = 5 * time.Second + services.GitHubRuntimeBuildOptions = githubruntime.BuildOptions{ + BaseURL: server.URL + "/", HTTPClient: client, AllowInsecureLoopback: true, + } + options := GitHubGovernanceOperationOptions{ + GitHubGovernanceOptions: GitHubGovernanceOptions{ + GitHubReadOptions: GitHubReadOptions{ + EstateRoot: explicit, RuntimeConfig: runtimePath, + InstallationID: "installation:github-opennetwork", + }, + Owner: "NDDev-OpenNetwork", Repository: "github-device-sync", CompareLocal: true, + }, + } + current, failure := services.githubGovernanceContext(context.Background(), root, options, "test") + if failure != nil { + t.Fatalf("explicit policy root was not honored: %#v", failure) + } + if current.estateRoot != explicit || current.observer(services).estateRoot != explicit { + t.Fatal("operation and precondition observer did not bind the selected estate") + } + read := services.GitHubGovernance(context.Background(), root, options.GitHubGovernanceOptions) + if read.ExitClass != domain.ExitSuccess { + t.Fatalf("read-only comparison selected a different policy root: %#v", read) + } + if os.Getenv("GDS_ESTATE_ROOT") != registered { + t.Fatal("operation override changed process environment") + } + options.EstateRoot = "" + _, failure = services.githubGovernanceContext(context.Background(), root, options, "test") + if failure == nil || !appHasFinding(*failure, "GDS_POLICY_PROFILE_MISSING") { + t.Fatalf("default estate should still lack the selected profile: %#v", failure) + } + for _, invalid := range []string{filepath.Join(explicit, "missing"), root} { + options.EstateRoot = invalid + _, failure = services.githubGovernanceContext(context.Background(), root, options, "test") + if failure == nil || failure.Mutation.Attempted { + t.Fatalf("unverified estate must fail before mutation: %#v", failure) + } + } +} diff --git a/core/app/github_readonly.go b/core/app/github_readonly.go index 2c06793..7cd6612 100644 --- a/core/app/github_readonly.go +++ b/core/app/github_readonly.go @@ -157,7 +157,7 @@ func (services *Services) GitHubGovernance( Status: "observed-only", Counts: map[string]int{}, Fields: []governance.FieldResult{}, } if options.CompareLocal { - estateRoot, anchor, findings := services.policyInputs(ctx, path) + estateRoot, anchor, findings := services.policyInputsWithEstateRoot(ctx, path, options.EstateRoot) if len(findings) != 0 { return domain.NewEnvelope(command, classifyFindings(findings), nil, findings...) } diff --git a/core/app/services.go b/core/app/services.go index e5025bf..4fd09d3 100644 --- a/core/app/services.go +++ b/core/app/services.go @@ -385,7 +385,14 @@ func (services *Services) policyInputs( ctx context.Context, path string, ) (string, domain.RepositoryAnchor, []domain.Finding) { - outcome := services.Context.Resolve(ctx, path) + return services.policyInputsWithEstateRoot(ctx, path, "") +} + +func (services *Services) policyInputsWithEstateRoot( + ctx context.Context, + path, estateRoot string, +) (string, domain.RepositoryAnchor, []domain.Finding) { + outcome := services.Context.ResolveWithEstateRoot(ctx, path, estateRoot) if outcome.Context.Workspace.GitWorktreeRoot == "" { return "", domain.RepositoryAnchor{}, outcome.Findings } diff --git a/core/context/resolver.go b/core/context/resolver.go index ad4fe52..93a3619 100644 --- a/core/context/resolver.go +++ b/core/context/resolver.go @@ -134,6 +134,22 @@ func NewResolver( } } +// ResolveWithEstateRoot applies an operation-local estate override through the +// same authority checks as GDS_ESTATE_ROOT, without changing process state. +func (resolver *Resolver) ResolveWithEstateRoot(ctx context.Context, path, estateRoot string) Outcome { + if estateRoot == "" { + return resolver.Resolve(ctx, path) + } + selected := *resolver + selected.getenv = func(key string) string { + if key == "GDS_ESTATE_ROOT" { + return estateRoot + } + return resolver.getenv(key) + } + return selected.Resolve(ctx, path) +} + func (resolver *Resolver) Resolve(ctx context.Context, path string) Outcome { resolvedPath, err := resolveDirectory(path) if err != nil { diff --git a/docs/runbooks/github-ruleset-reconcile.md b/docs/runbooks/github-ruleset-reconcile.md index b578801..b1e3c2b 100644 --- a/docs/runbooks/github-ruleset-reconcile.md +++ b/docs/runbooks/github-ruleset-reconcile.md @@ -47,6 +47,9 @@ lost this way reports nothing; the only evidence is its absence. ## Preconditions +- `--estate-root` selects the verified control-plane used for both provider + inventory and canonical policy. It overrides environment/registered estate + selection for this operation; the precondition observer retains that root. - The mutation runtime must be configured. `gds context --json` reporting `capabilities.mutations.runtime: configuration-required` means the GitHub App credential does not resolve on this device, and the apply will fail *after* its From 7ab5cac5cbf11ba4047909be7a175a77d9d56444 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Mon, 7 Sep 2026 13:02:47 +0500 Subject: [PATCH 2/2] chore(gds): refresh provenance for estate selection fix Signed-off-by: rldyourmnd --- .gds/bundle.lock.yaml | 10 +++++----- .github/workflows/gds-ci.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index 7b7207d..1a77711 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -5,14 +5,14 @@ bundle: version: "0.8.0-dev" release_sequence: 0 channel: "development" - source_tree_digest: "sha256:14e3b6a5e784f371cbdd35b9cae16751d187312bddadc65f7a8a2dc4fd9229ff" - digest: "sha256:13511083d417c02909b0c2c749b9f854326d80d878de0bd1c04a8a2564516df7" + source_tree_digest: "sha256:c69222d87a7cd8ddd65a6ffd4266a4d92ad42106a382463cd9c7809f096a2b01" + digest: "sha256:5956a186ebc3080d372497c7c95a7bf39653ab4357df9f223cd2792beb05633d" projection: - input_digest: "sha256:e72c1044b1575802633810e196ff230c0fcdad2e9aedce8931dcf9d659a86c44" - output_digest: "sha256:19e6a3ce782a399ada46509398ad1aae99c9404405049a83aa284b780eec9acc" + input_digest: "sha256:110b7813f13be2c091b16e1a8c5ed12199172480d2497bec68e8bfbc6fcf96e6" + output_digest: "sha256:af2fa4f5d4f89208e410e9f0055cd7393f09eeae69941847a7b230a39d54fa63" files: - path: ".gds/compiled-policy.json" digest: "sha256:807282f820294914e1c7e6ad1bf27c54a799d56305c58630254ab50ab286f379" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:91d5089db35ac77e3863844f2d2133f5e3cd30a9b82aecff28d048c84b932950" + digest: "sha256:71910a8ffb233f1cb9071658f6e32595921ab7404bbc829aff10f379c309e5f5" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 1283455..27ce375 100644 --- a/.github/workflows/gds-ci.yml +++ b/.github/workflows/gds-ci.yml @@ -1,8 +1,8 @@ # GENERATED FILE - DO NOT EDIT DIRECTLY # generator: gds # bundle: 0.8.0-dev -# source-tree-digest: sha256:14e3b6a5e784f371cbdd35b9cae16751d187312bddadc65f7a8a2dc4fd9229ff -# input-digest: sha256:e72c1044b1575802633810e196ff230c0fcdad2e9aedce8931dcf9d659a86c44 +# source-tree-digest: sha256:c69222d87a7cd8ddd65a6ffd4266a4d92ad42106a382463cd9c7809f096a2b01 +# input-digest: sha256:110b7813f13be2c091b16e1a8c5ed12199172480d2497bec68e8bfbc6fcf96e6 # output-digest: sha256:a911ec3d1d5728bbe37fec78e04cc8452b9dbb1e8b394d0b8cace067804eff9a # edit-source: # - .gds/repository.yaml