Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Fixed
body: Allow local Python overrides to exclude exact generated subtrees before source observation, and accept valid Hatchling source distributions whose single root directory is implicit in archive entries.
time: 2026-08-16T05:45:00+08:00
12 changes: 6 additions & 6 deletions docs/.review/APT_PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ artifact: awd-swe-design-review-attestation
schema_version: 2
scope_key: bd8b79396ed3ab3d8e062279e409430bcf54daf5acf561e1b5d63f4302f9edc4
scope: {"kind": "path", "primary_target": "docs/APT_PROVIDER.md", "repository": "/home/omry/dev/reploy", "selector": "docs/APT_PROVIDER.md"}
review_content_identity_sha256: c26624051f5cdf6ce45d2102163b99ebeb7e903c797a5dcc3f87a7c45324c518
target_content_identity_sha256: a0705bd68de31a704ef17ea3a11f01b1528256efcacbf4914703cd8d38614606
review_content_identity_sha256: 511678fea88a75f21a05457423248cce857b925083bfc94a84cbdbf0592714a4
target_content_identity_sha256: afea1556f273827fbbccdad5304e514b2eec0b09c1992a2af9ae47b41e462c0e
baseline_content_identity_sha256: 5ebdc731af21b7e4bb30f9cd9f0f7e2b143dbb29f674164906b448033592a3c6
target_documents: [{"path": "docs/APT_PROVIDER.md", "repository": "/home/omry/dev/reploy", "sha256": "306f7600901de1c0d3655cebb64a242c6b7bc913189bb9d16a1b6b41a5225e5c"}]
target_documents: [{"path": "docs/APT_PROVIDER.md", "repository": "/home/omry/dev/reploy", "sha256": "c88361fc0998dffc41a5718366e0ac6336aea1c3e53a98c25d6f2a42778e9603"}]
baseline_documents: [{"path": "docs/BLUEPRINT_ENVIRONMENT_MODEL.md", "repository": "/home/omry/dev/reploy", "sha256": "a76af85593f36efa35faf406521b62976924ab2c866564ac2455bf368022fb78"}]
document_repository: "/home/omry/dev/reploy"
document_path: "docs/APT_PROVIDER.md"
document_revision_provenance: "e7e4993284202a7953d8db00a908fad6aa0d319d"
document_sha256: 306f7600901de1c0d3655cebb64a242c6b7bc913189bb9d16a1b6b41a5225e5c
document_revision_provenance: "76a73f0a12eb29a1acfe5615a8ebcba94ce347d5"
document_sha256: c88361fc0998dffc41a5718366e0ac6336aea1c3e53a98c25d6f2a42778e9603
verdict: clean
attested_at: 2026-08-21T18:36:43Z
attested_at: 2026-08-21T19:53:33Z
---
<!-- awd-swe-design-review-attestation:v2 -->

Expand Down
10 changes: 5 additions & 5 deletions docs/.review/APT_PROVIDER_DETAIL_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ artifact: awd-swe-design-review-attestation
schema_version: 2
scope_key: dd04dd2c2041a5035ffd15d245763f28fd28d9c4d7c4e83ca3a0cb45f092a863
scope: {"kind": "path", "primary_target": "docs/APT_PROVIDER_DETAIL_DESIGN.md", "repository": "/home/omry/dev/reploy", "selector": "docs/APT_PROVIDER_DETAIL_DESIGN.md"}
review_content_identity_sha256: 9abe59a5b4ab45fd8f3f65067668d4b60afbdaad33edf0d1df55627f95bde03f
review_content_identity_sha256: ef68f99f94daca214accc166ce2d067709592ceb81f0e969d26649e62401d4aa
target_content_identity_sha256: 8ca38bf2c857f3348d4b10f7f1913216b933ceeeb89ac60a2c5e5a0719df66e5
baseline_content_identity_sha256: 4167f13aee3b0099cecdf2cc4a96b95d5398547062959eef534c81d2df4da7a5
baseline_content_identity_sha256: 4ec50b29f2bad58befc4c584ae0b97f96118aab39c2afec1fa87fccc3b3bc5ae
target_documents: [{"path": "docs/APT_PROVIDER_DETAIL_DESIGN.md", "repository": "/home/omry/dev/reploy", "sha256": "db0abda0347988abf6dce4a54cdbd830cd84295c8da826c53ef2e4a8eda077f1"}]
baseline_documents: [{"path": "docs/APT_PROVIDER.md", "repository": "/home/omry/dev/reploy", "sha256": "306f7600901de1c0d3655cebb64a242c6b7bc913189bb9d16a1b6b41a5225e5c"}, {"path": "docs/BLUEPRINT_ENVIRONMENT_MODEL.md", "repository": "/home/omry/dev/reploy", "sha256": "a76af85593f36efa35faf406521b62976924ab2c866564ac2455bf368022fb78"}]
baseline_documents: [{"path": "docs/APT_PROVIDER.md", "repository": "/home/omry/dev/reploy", "sha256": "c88361fc0998dffc41a5718366e0ac6336aea1c3e53a98c25d6f2a42778e9603"}, {"path": "docs/BLUEPRINT_ENVIRONMENT_MODEL.md", "repository": "/home/omry/dev/reploy", "sha256": "a76af85593f36efa35faf406521b62976924ab2c866564ac2455bf368022fb78"}]
document_repository: "/home/omry/dev/reploy"
document_path: "docs/APT_PROVIDER_DETAIL_DESIGN.md"
document_revision_provenance: "e7e4993284202a7953d8db00a908fad6aa0d319d"
document_revision_provenance: "76a73f0a12eb29a1acfe5615a8ebcba94ce347d5"
document_sha256: db0abda0347988abf6dce4a54cdbd830cd84295c8da826c53ef2e4a8eda077f1
verdict: clean
attested_at: 2026-08-21T18:42:20Z
attested_at: 2026-08-21T19:53:33Z
---
<!-- awd-swe-design-review-attestation:v2 -->

Expand Down
16 changes: 10 additions & 6 deletions docs/APT_PROVIDER.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,16 @@ match.

When a staging package override selects local Python source, its filesystem
path is only a local build input. Reploy observes an immutable path-free input
digest while withholding VCS metadata, then lets the declared Python build
backend define the package boundary by producing an sdist. The backend sees
ordinary generated directories and caches; Reploy does not decide package
contents with a generic ignore list. Reploy validates and retains exactly one
closed `.tar.gz` sdist, securely extracts that retained artifact, and builds
the wheel only from the extraction.
digest while withholding VCS metadata and any exact relative subtrees listed
by the selected override's optional `exclude` array, then lets the declared
Python build backend define the package boundary by producing an sdist.
Exclusions are literal forward-slash paths, not glob or ignore-file patterns;
they are applied before Reploy reads metadata beneath the selected path and are
part of source-input identity. Selected FIFOs and other unsupported special
files remain errors. Apart from these explicit input exclusions, the backend
sees ordinary generated directories and caches. Reploy validates and retains
exactly one closed `.tar.gz` sdist, securely extracts that retained artifact,
and builds the wheel only from the extraction.

A selected snapshot may contain project-owned `.reploy.yaml` build metadata.
The initial strict recipe declares either `pep517` or `setuptools-legacy` and
Expand Down
20 changes: 12 additions & 8 deletions docs/REDESIGN_EVALUATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ explicit opt-in later.
**Approved implementation contract:**

- Reploy observes and copies an immutable projection containing every selected
local-source entry except VCS metadata. Generated environments, caches, and
other development files remain visible to the declared build backend; the
backend alone decides whether they enter the sdist.
local-source entry except VCS metadata and exact subtrees explicitly excluded
by the local override. Exclusions are literal input-selection paths, are
applied before entry metadata is read, and participate in source identity.
Other generated environments, caches, and development files remain visible
to the declared build backend; the backend alone decides whether they enter
the sdist.
- Every explicit image build with a selected local Python project performs the
complete source projection, sdist build, validation, and wheel build before
deciding whether later provider layers or the final environment image are
Expand Down Expand Up @@ -721,11 +724,12 @@ the source manifest:
- The virtual environment's Python symlink resolves to the system interpreter,
correctly triggering the escape check.

Slice 12 replaced that generic snapshot boundary with a complete provisional
input (excluding VCS metadata), followed by backend-defined sdist creation and
Reploy-owned sdist validation. Generated development state may be visible to
the backend but does not enter the retained artifact unless the project's
packaging metadata selects it.
Slice 12 replaced that generic snapshot boundary with a provisional input,
followed by backend-defined sdist creation and Reploy-owned sdist validation.
The input excludes VCS metadata and may now exclude exact developer-declared
subtrees. Other generated development state may be visible to the backend but
does not enter the retained artifact unless the project's packaging metadata
selects it.

That historical diagnostic also exposed excessive internal context and used
the ambiguous phrase `workspace root`. Slice 12 now reports source-input,
Expand Down
95 changes: 88 additions & 7 deletions internal/deploy/package_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"unicode"
Expand Down Expand Up @@ -43,8 +45,13 @@ type BaseImageOverrideV1 struct {
// PackageOverrideChoiceV1 selects exactly one local source or exact upstream
// version. A mapping never requests installation by itself.
type PackageOverrideChoiceV1 struct {
Path string `yaml:"path,omitempty"`
Version string `yaml:"version,omitempty"`
Path string `yaml:"path,omitempty"`
Version string `yaml:"version,omitempty"`
Exclude []string `yaml:"exclude,omitempty"`
}

func (choice PackageOverrideChoiceV1) Empty() bool {
return choice.Path == "" && choice.Version == "" && len(choice.Exclude) == 0
}

// ResolvedPackageOverridesV1 contains interpolated, normalized lookup keys and
Expand All @@ -59,6 +66,7 @@ type ResolvedPackageOverridesV1 struct {
type ResolvedPackageOverrideChoiceV1 struct {
Path string
Version string
Exclude []string
}

// PackageOverrideIntentV1 is the path-free build input retained in the lock.
Expand All @@ -78,10 +86,11 @@ type PackageAdditionIntentV1 struct {
}

type PackageOverrideIntentChoiceV1 struct {
Provider string `json:"provider"`
Package string `json:"package"`
Kind string `json:"kind"`
Version string `json:"version"`
Provider string `json:"provider"`
Package string `json:"package"`
Kind string `json:"kind"`
Version string `json:"version"`
Exclude []string `json:"exclude,omitempty"`
}

func EmptyPackageOverridesV1(environmentID string) PackageOverridesV1 {
Expand Down Expand Up @@ -118,13 +127,32 @@ func EncodePackageOverridesV1(overrides PackageOverridesV1) ([]byte, error) {
if err := ValidatePackageOverridesV1(overrides); err != nil {
return nil, err
}
overrides = canonicalPackageOverridesV1(overrides)
content, err := yaml.Marshal(overrides)
if err != nil {
return nil, fmt.Errorf("encode package overrides: %w", err)
}
return content, nil
}

// canonicalPackageOverridesV1 returns an encoding copy whose semantically
// unordered exclusion lists use their stable lexical order. Validation must
// run first; copying the nested mappings avoids mutating caller-owned values.
func canonicalPackageOverridesV1(overrides PackageOverridesV1) PackageOverridesV1 {
canonicalOverrides := make(map[string]map[string]PackageOverrideChoiceV1, len(overrides.Environment.PackageOverrides))
for provider, packages := range overrides.Environment.PackageOverrides {
canonicalPackages := make(map[string]PackageOverrideChoiceV1, len(packages))
for packageID, choice := range packages {
exclusions, _ := NormalizePackageOverrideExclusionsV1(choice.Exclude)
choice.Exclude = exclusions
canonicalPackages[packageID] = choice
}
canonicalOverrides[provider] = canonicalPackages
}
overrides.Environment.PackageOverrides = canonicalOverrides
return overrides
}

func ValidatePackageOverridesV1(overrides PackageOverridesV1) error {
environment := overrides.Environment
if err := blueprint.ValidateEnvironmentID("package overrides environment.id", environment.ID); err != nil {
Expand Down Expand Up @@ -202,11 +230,46 @@ func ValidatePackageOverridesV1(overrides PackageOverridesV1) error {
if version != "" && (strings.HasPrefix(version, "-") || containsControl(version)) {
return fmt.Errorf("package override %s.%s version must be plain version text", provider, packageID)
}
exclusions, err := NormalizePackageOverrideExclusionsV1(choice.Exclude)
Comment thread
omry marked this conversation as resolved.
if err != nil {
return fmt.Errorf("package override %s.%s exclude: %w", provider, packageID, err)
}
if len(exclusions) != 0 && pathValue == "" {
return fmt.Errorf("package override %s.%s exclude requires a local path", provider, packageID)
}
}
}
return nil
}

// NormalizePackageOverrideExclusionsV1 validates exact source-relative paths
// and returns their stable lexical order. Entries select the named path and
// its descendants; they are deliberately not glob or ignore-file patterns.
func NormalizePackageOverrideExclusionsV1(exclusions []string) ([]string, error) {
normalized := append([]string{}, exclusions...)
for index, exclusion := range normalized {
if exclusion == "" || strings.TrimSpace(exclusion) != exclusion ||
!utf8.ValidString(exclusion) || containsControl(exclusion) ||
path.IsAbs(exclusion) || path.Clean(exclusion) != exclusion ||
strings.ContainsAny(exclusion, `\:*?[]`) || exclusion == "." ||
exclusion == ".." || strings.HasPrefix(exclusion, "../") {
return nil, fmt.Errorf("entry %d must be a canonical relative path using forward slashes", index)
}
for _, component := range strings.Split(exclusion, "/") {
if component == "" || component == "." || component == ".." {
return nil, fmt.Errorf("entry %d must be a canonical relative path using forward slashes", index)
}
}
}
sort.Strings(normalized)
for index := 1; index < len(normalized); index++ {
if normalized[index-1] == normalized[index] {
return nil, fmt.Errorf("contains duplicate path %q", normalized[index])
}
}
return normalized, nil
}

// NormalizePackageAdditionV1 validates a provider-native development package
// addition without translating its package name. The os provider currently
// selects the Debian/Ubuntu APT implementation at build time.
Expand Down Expand Up @@ -355,7 +418,14 @@ func ResolvePackageOverridesV1(
}
owners[normalized] = packageID

resolvedChoice := ResolvedPackageOverrideChoiceV1{Version: choice.Version}
exclusions, err := NormalizePackageOverrideExclusionsV1(choice.Exclude)
if err != nil {
return ResolvedPackageOverridesV1{}, fmt.Errorf("package override %s.%s exclude: %w", provider, packageID, err)
}
resolvedChoice := ResolvedPackageOverrideChoiceV1{
Version: choice.Version,
Exclude: exclusions,
}
if choice.Path != "" {
interpolated, err := blueprint.ResolveEnvironmentVariableString(choice.Path, variables)
if err != nil {
Expand Down Expand Up @@ -420,6 +490,7 @@ func (overrides ResolvedPackageOverridesV1) Intent() (PackageOverrideIntentV1, e
switch {
case choice.Path != "" && choice.Version == "":
item.Kind = "local"
item.Exclude = append([]string{}, choice.Exclude...)
case choice.Path == "" && choice.Version != "":
item.Kind = "version"
item.Version = choice.Version
Expand Down Expand Up @@ -486,10 +557,20 @@ func ValidatePackageOverrideIntentV1(intent PackageOverrideIntentV1) error {
if choice.Version != "" {
return fmt.Errorf("local package override intent %s.%s must not contain a version", choice.Provider, choice.Package)
}
exclusions, err := NormalizePackageOverrideExclusionsV1(choice.Exclude)
if err != nil {
return fmt.Errorf("local package override intent %s.%s exclude: %w", choice.Provider, choice.Package, err)
}
if !slices.Equal(exclusions, choice.Exclude) {
return fmt.Errorf("local package override intent %s.%s exclusions must be unique and sorted", choice.Provider, choice.Package)
}
case "version":
if choice.Version == "" || strings.HasPrefix(choice.Version, "-") || containsControl(choice.Version) {
return fmt.Errorf("version package override intent %s.%s must contain plain version text", choice.Provider, choice.Package)
}
if len(choice.Exclude) != 0 {
return fmt.Errorf("version package override intent %s.%s must not contain exclusions", choice.Provider, choice.Package)
}
default:
return fmt.Errorf("package override intent %s.%s has unsupported kind %q", choice.Provider, choice.Package, choice.Kind)
}
Expand Down
22 changes: 18 additions & 4 deletions internal/deploy/package_overrides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ func TestPackageOverridesRoundTripAndResolve(t *testing.T) {
},
PackageOverrides: map[string]map[string]PackageOverrideChoiceV1{
"python": {
"Demo_Pkg": {Path: "{{ workspace_root }}/demo"},
"other": {Version: "2.4.0"},
"Demo_Pkg": {
Path: "{{ workspace_root }}/demo",
Exclude: []string{"recordings/.omegaflow", ".venv"},
},
"other": {Version: "2.4.0"},
},
},
}}
Expand All @@ -51,6 +54,9 @@ func TestPackageOverridesRoundTripAndResolve(t *testing.T) {
if got := resolved.Providers["python"]["demo-pkg"].Path; got != filepath.Join(dir, "workspace", "demo") {
t.Fatalf("resolved local path = %q", got)
}
if got := resolved.Providers["python"]["demo-pkg"].Exclude; len(got) != 2 || got[0] != ".venv" || got[1] != "recordings/.omegaflow" {
t.Fatalf("resolved exclusions = %#v", got)
}
if got := resolved.Providers["python"]["other"].Version; got != "2.4.0" {
t.Fatalf("resolved version = %q", got)
}
Expand All @@ -62,8 +68,11 @@ func TestPackageOverridesRoundTripAndResolve(t *testing.T) {
t.Fatal(err)
}
if len(intent.Choices) != 2 ||
intent.Choices[0] != (PackageOverrideIntentChoiceV1{Provider: "python", Package: "demo-pkg", Kind: "local"}) ||
intent.Choices[1] != (PackageOverrideIntentChoiceV1{Provider: "python", Package: "other", Kind: "version", Version: "2.4.0"}) {
intent.Choices[0].Provider != "python" || intent.Choices[0].Package != "demo-pkg" ||
intent.Choices[0].Kind != "local" || len(intent.Choices[0].Exclude) != 2 ||
intent.Choices[0].Exclude[0] != ".venv" || intent.Choices[0].Exclude[1] != "recordings/.omegaflow" ||
intent.Choices[1].Provider != "python" || intent.Choices[1].Package != "other" ||
intent.Choices[1].Kind != "version" || intent.Choices[1].Version != "2.4.0" {
t.Fatalf("intent = %#v", intent)
}
if len(intent.Additions) != 1 ||
Expand Down Expand Up @@ -237,6 +246,11 @@ func TestPackageOverridesRejectInvalidShape(t *testing.T) {
{name: "multiple documents", yaml: "environment:\n id: demo\n package_overrides: {}\n---\n{}\n", want: "multiple YAML documents"},
{name: "both choices", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {path: ../demo, version: 1.0}\n", want: "exactly one"},
{name: "neither choice", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {}\n", want: "exactly one"},
{name: "exclude on version", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {version: 1.0, exclude: [recordings]}\n", want: "requires a local path"},
{name: "absolute exclude", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {path: ../demo, exclude: [/recordings]}\n", want: "canonical relative path"},
{name: "escaping exclude", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {path: ../demo, exclude: [../recordings]}\n", want: "canonical relative path"},
{name: "glob exclude", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {path: ../demo, exclude: ['recordings/*']}\n", want: "canonical relative path"},
{name: "duplicate exclude", yaml: "environment:\n id: demo\n package_overrides:\n python:\n demo: {path: ../demo, exclude: [recordings, recordings]}\n", want: "duplicate path"},
{name: "missing mappings", yaml: "environment:\n id: demo\n", want: "package_overrides must use a mapping"},
{name: "variable cycle", yaml: "environment:\n id: demo\n vars: {a: '{{ b }}', b: '{{ a }}'}\n package_overrides: {}\n", want: "cycle"},
{name: "unsupported addition provider", yaml: "environment:\n id: demo\n package_additions: {apt: [default-jre-headless]}\n package_overrides: {}\n", want: "use os"},
Expand Down
5 changes: 3 additions & 2 deletions internal/deploy/validated_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ func PackageOverridesDigestV1(overrides PackageOverridesV1) (canonical.Digest, e
return "", err
}
// EncodePackageOverridesV1 normalizes YAML map ordering, scalar spelling,
// and omitted optional fields. Hash that normalized representation so every
// value accepted by the sidecar schema has a stable identity.
// exclusion ordering, and omitted optional fields. Hash that normalized
// representation so every value accepted by the sidecar schema has a stable
// identity.
return canonical.Sum("package-overrides", "package-overrides-v1", string(content))
}

Expand Down
Loading
Loading