diff --git a/.gds/bundle.lock.yaml b/.gds/bundle.lock.yaml index abe1a65..9d65406 100644 --- a/.gds/bundle.lock.yaml +++ b/.gds/bundle.lock.yaml @@ -4,14 +4,14 @@ schema_version: 1 bundle: version: "0.9.7-dev" release_sequence: 0 - source_tree_digest: "sha256:ba259a2260cfc55a79b76c69ea895ab446ea9ae6fe2892a016688713f903e162" - digest: "sha256:9b2dd4f5e413021932c2454c7e482fd790482ec9246e5adaf783dc92780672d9" + source_tree_digest: "sha256:ff0bdb5a3aecc647e785a571e5b08f4d47afdf4b9865139b63670f7636ed0df0" + digest: "sha256:83833e3c808531d699accaa7bde3e31289a631da93553c4fae5d08d9ffe902cd" projection: - input_digest: "sha256:4145cec9f3f37a5c3930907936c97ebab0dd5d3e96a3ee480220aa32255c2ffe" - output_digest: "sha256:2572b8c1c82ccf29b5f7950c788f29cc5504bebf64b141e6b60b743d65366717" + input_digest: "sha256:847d847bf309749351054160ec089ef7f7d22a1969c26a3eb82ca69fa58beb23" + output_digest: "sha256:469c17732e4509ca38c6f2903b590cc0e8670502f965ebf50b4b40833e3f2cb4" files: - path: ".gds/compiled-policy.json" digest: "sha256:9f498788bdc34e52a0ab793c536e0e6a7b360c2e1a20446cbf03ed51986cdc6f" - path: ".github/workflows/gds-ci.yml" - digest: "sha256:439aac0176476d26063ff3799233a79a1032b905b737aa1b1db56d12f5c9b658" + digest: "sha256:75d8ce3e084bc5b3f1b33ce920b245d635915810cb97fb9887db0eba168d5f7a" diff --git a/.github/workflows/gds-ci.yml b/.github/workflows/gds-ci.yml index 7b34393..c326ce5 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.9.7-dev -# source-tree-digest: sha256:ba259a2260cfc55a79b76c69ea895ab446ea9ae6fe2892a016688713f903e162 -# input-digest: sha256:4145cec9f3f37a5c3930907936c97ebab0dd5d3e96a3ee480220aa32255c2ffe +# source-tree-digest: sha256:ff0bdb5a3aecc647e785a571e5b08f4d47afdf4b9865139b63670f7636ed0df0 +# input-digest: sha256:847d847bf309749351054160ec089ef7f7d22a1969c26a3eb82ca69fa58beb23 # output-digest: sha256:4ef1ee2fcc42927eaedef9c85f7b421f87e5cc75ff7055ef520216a00f7d4b74 # edit-source: # - .gds/repository.yaml diff --git a/core/app/projection_operations.go b/core/app/projection_operations.go index 6920730..b16ab7a 100644 --- a/core/app/projection_operations.go +++ b/core/app/projection_operations.go @@ -260,10 +260,10 @@ func (services *Services) projectionOperationContext( return projectionContext{}, []domain.Finding{dependencyFinding(path, infoErr)} } anchor, findings = manifest.NewLoader(services.Schemas).LoadRepository(repositoryInfo.WorktreeRoot) - if len(findings) == 0 && !isPublicModuleProjection(anchor) { + if len(findings) == 0 && anchor.Classification.VisibilityContract != "public" { findings = []domain.Finding{{ Code: "GDS_PROJECTION_RELEASE_TARGET_INVALID", Severity: domain.SeverityHigh, - Message: "Released projection sources are accepted only for public modules.", + Message: "Released projection sources are accepted only for public repositories.", }} } var releasedManifest bundle.Manifest diff --git a/core/app/released_project_projection_test.go b/core/app/released_project_projection_test.go new file mode 100644 index 0000000..be0cb7d --- /dev/null +++ b/core/app/released_project_projection_test.go @@ -0,0 +1,105 @@ +package app + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestReleasedPublicProjectRequiresArtifactWithoutFallingBackToEstate(t *testing.T) { + root := releasedProjectFixture(t, "public") + services, err := NewServices(DefaultClock) + if err != nil { + t.Fatal(err) + } + archive := filepath.Join(t.TempDir(), "missing-release.tar.gz") + _, findings := services.projectionOperationContext(context.Background(), root, ProjectionSourceOptions{ + BundleArchive: archive, ReleaseEnvelope: archive + ".json", + }) + if len(findings) != 1 || findings[0].Code != "GDS_LOCAL_OPERATION_NOT_PROVEN" || + findings[0].Evidence["path"] != archive { + t.Fatalf("public project did not require its exact released artifact: %#v", findings) + } + if _, err := os.Stat(filepath.Join(root, ".gds", "compiled-policy.json")); !os.IsNotExist(err) { + t.Fatalf("artifact failure wrote a projection: %v", err) + } +} + +func TestReleasedPrivateProjectCannotDetachFromEstatePolicy(t *testing.T) { + root := releasedProjectFixture(t, "private") + services, err := NewServices(DefaultClock) + if err != nil { + t.Fatal(err) + } + _, findings := services.projectionOperationContext(context.Background(), root, ProjectionSourceOptions{ + BundleArchive: "untrusted.tar.gz", ReleaseEnvelope: "untrusted.json", + }) + if len(findings) != 1 || findings[0].Code != "GDS_PROJECTION_RELEASE_TARGET_INVALID" { + t.Fatalf("private project could bypass canonical estate policy: %#v", findings) + } +} + +func releasedProjectFixture(t *testing.T, visibility string) string { + t.Helper() + root := t.TempDir() + if err := os.Mkdir(filepath.Join(root, ".gds"), 0o755); err != nil { + t.Fatal(err) + } + anchor := `schema_version: 1 +repository: + id: repo_01JEXAMPZ00000000000000001 + display_name: example-project + roles: [project] + lifecycle: active +provider: + type: github + installation: installation:example + repository_id: 1234 + owner: example-owner + name: example-project +classification: + portfolios: [portfolio:example] + visibility_contract: VISIBILITY + data_classification: VISIBILITY +policy: + profiles: [repository-default] + rollout_ring: standard +git: + default_branch: main + integration: pull-request + branch_model: task-branches + handoff_pr: preferred + cleanup: merged-only +verification: + commands: + test: [git diff --check] + required: [test] +agent: + context_profile: project-default + generated_agents: false + serena: + enabled: false + provenance_required: false +release: + mode: none +` + if err := os.WriteFile(filepath.Join(root, ".gds", "repository.yaml"), + []byte(strings.ReplaceAll(anchor, "VISIBILITY", visibility)), 0o644); err != nil { + t.Fatal(err) + } + for _, args := range [][]string{ + {"init", "--quiet", "--initial-branch=main"}, + {"add", ".gds/repository.yaml"}, + {"-c", "user.name=Example", "-c", "user.email=example@example.test", "-c", "commit.gpgsign=false", + "commit", "--quiet", "-m", "fixture"}, + } { + command := exec.Command("git", append([]string{"-C", root}, args...)...) + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("fixture Git %v: %v: %s", args, err, output) + } + } + return root +} diff --git a/docs/contracts/cli-v1.md b/docs/contracts/cli-v1.md index 2da249a..15df868 100644 --- a/docs/contracts/cli-v1.md +++ b/docs/contracts/cli-v1.md @@ -143,7 +143,8 @@ with leaf provenance. It does not write the compiled document. ### `gds generate repository` `--bundle-archive` and `--release-envelope` select an immutable released -projection source for a standalone public module. They are an inseparable pair +projection source for a public repository, including a project without the +`module` role. They are an inseparable pair and apply equally to candidate, check, plan, apply and verify modes. The command never fetches a mutable URL and never treats an unverified extracted directory as authority. diff --git a/docs/contracts/projections-v1.md b/docs/contracts/projections-v1.md index 182d47d..5049efb 100644 --- a/docs/contracts/projections-v1.md +++ b/docs/contracts/projections-v1.md @@ -71,7 +71,8 @@ Development locks use sequence `0`. A released lock requires a positive sequence and attestation identity digest. The development lock is test evidence, not a released immutable bundle. -Standalone public modules consume released policy without copying its source +Public repositories, including ordinary projects and standalone modules, +consume released policy without copying its source tree into every repository: ```bash