From d84c1908f64263f5a0003181d94da2279b919299 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Mon, 11 May 2026 14:46:51 -0500 Subject: [PATCH] =?UTF-8?q?feat(zddc):=20Phase=201=20=E2=80=94=20embedded?= =?UTF-8?q?=20defaults.zddc=20+=20inherit=20+=20show-defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of the .zddc-first-configuration rollout: pure plumbing that makes the future move-everything-out-of-Go work mechanically possible without changing any current behaviour. New pieces: 1. zddc/internal/zddc/defaults.zddc.yaml — a real YAML file in the repo. Single source of truth for the baked-in baseline; intentionally minimal in Phase 1 (just title + empty acl) so existing deployments stay bit-identical until Phase 2 starts populating the schema. 2. //go:embed (defaults.go) bakes the bytes into the binary so shipped deployments don't need the file. Operators who want a starting point export with: zddc-server show-defaults > /var/lib/zddc/root/.zddc 3. PolicyChain gains an Embedded ZddcFile field. EffectivePolicy layers in the embedded defaults as a baseline below the on-disk chain. Consumers that want the full effective view consult both; existing consumers that only read chain.Levels keep working bit-identically (the new field is additive). 4. New top-level `inherit:` key on ZddcFile. Default true. Set `inherit: false` on any on-disk .zddc to zero out chain.Embedded — the operator owns every rule from that level outward. Useful at the on-disk root to fully reject the embedded defaults; useful at deeper levels for sandbox subtrees. 5. `zddc-server show-defaults` (also accepts --show-defaults) subcommand dumps the embedded bytes to stdout — same shape as --print-rego. No flag plumbing needed beyond the existing args walk. 6. Tests: parse-roundtrip on the embedded file, presence in chain by default, inherit:false drops it, explicit inherit:true is a no-op versus the default. Phase 2 (next): add a `paths:` recursive map + `default_tool:` / `auto_own:` / `virtual:` keys, populate defaults.zddc.yaml with the canonical ZDDC convention, and migrate apps.DefaultAppAt / AutoOwnCanonicalNames / VirtualOnlyCanonicalNames to cascade lookups. Co-Authored-By: Claude Opus 4.7 (1M context) --- zddc/cmd/zddc-server/main.go | 10 +++ zddc/internal/handler/tables.html | 2 +- zddc/internal/zddc/cascade.go | 26 +++++++ zddc/internal/zddc/defaults.go | 40 +++++++++++ zddc/internal/zddc/defaults.zddc.yaml | 29 ++++++++ zddc/internal/zddc/defaults_test.go | 100 ++++++++++++++++++++++++++ zddc/internal/zddc/file.go | 24 +++++++ 7 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 zddc/internal/zddc/defaults.go create mode 100644 zddc/internal/zddc/defaults.zddc.yaml create mode 100644 zddc/internal/zddc/defaults_test.go diff --git a/zddc/cmd/zddc-server/main.go b/zddc/cmd/zddc-server/main.go index 75ee696..7b2be11 100644 --- a/zddc/cmd/zddc-server/main.go +++ b/zddc/cmd/zddc-server/main.go @@ -50,6 +50,16 @@ func main() { case "--print-rego=federal": fmt.Print(policy.FederalRego) return + case "show-defaults", "--show-defaults": + // Dump the embedded baseline .zddc to stdout. Pipe into a + // real file (e.g. $ZDDC_ROOT/.zddc) to start from the + // shipped defaults and edit; the on-disk copy then + // participates in the cascade alongside the embedded + // layer (both contribute; child wins). To ignore the + // embedded layer entirely after exporting, set + // `inherit: false` at the top of the exported file. + _, _ = os.Stdout.Write(zddc.EmbeddedDefaultsBytes()) + return } } diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html index 5bc53c2..f70194f 100644 --- a/zddc/internal/handler/tables.html +++ b/zddc/internal/handler/tables.html @@ -1300,7 +1300,7 @@ body.help-open .app-header {
ZDDC Table - v0.0.17-alpha · 2026-05-11 18:30:39 · ab44d75-dirty + v0.0.17-alpha · 2026-05-11 19:40:57 · 4af0d8c-dirty
diff --git a/zddc/internal/zddc/cascade.go b/zddc/internal/zddc/cascade.go index d7623cd..5319433 100644 --- a/zddc/internal/zddc/cascade.go +++ b/zddc/internal/zddc/cascade.go @@ -8,9 +8,21 @@ import ( ) // PolicyChain represents a chain of .zddc files from root to leaf. +// +// Embedded sits BELOW Levels[0] (the on-disk root .zddc): it's the +// baked-in defaults that ship with the binary, used as a baseline +// when an on-disk .zddc doesn't specify a rule. Consumers that want +// the full effective view should consult Levels then fall back to +// Embedded for unresolved lookups. +// +// Inherit:false on any level in Levels (or at deeper levels of a +// future paths: walker) zeroes out Embedded for that policy chain — +// the operator has taken full responsibility for spelling out every +// rule from scratch. type PolicyChain struct { Levels []ZddcFile // ordered root (index 0) → leaf (last index) HasAnyFile bool // true if at least one .zddc file exists in the chain + Embedded ZddcFile // baked-in defaults; zero ZddcFile{} if any level set inherit:false } // VisibleStart returns the lowest level index visible to evaluation at @@ -111,6 +123,20 @@ func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) { } } + // Layer in the embedded defaults as the bottom of the cascade + // (used as fallback by consumers that consult Embedded). If any + // on-disk level set top-level inherit:false, the embedded layer + // is dropped — the operator owns every rule. + if embedded, err := EmbeddedDefaults(); err == nil { + chain.Embedded = embedded + for _, lvl := range chain.Levels { + if lvl.Inherit != nil && !*lvl.Inherit { + chain.Embedded = ZddcFile{} + break + } + } + } + policyCache.Store(cacheKey, chain) return chain, nil } diff --git a/zddc/internal/zddc/defaults.go b/zddc/internal/zddc/defaults.go new file mode 100644 index 0000000..1b1a201 --- /dev/null +++ b/zddc/internal/zddc/defaults.go @@ -0,0 +1,40 @@ +package zddc + +import ( + _ "embed" + "sync" +) + +// defaultsBytes is the embedded baseline .zddc — see defaults.zddc.yaml +// for the source-of-truth and a description of its role in the cascade. +// +//go:embed defaults.zddc.yaml +var defaultsBytes []byte + +// EmbeddedDefaultsBytes returns the raw embedded defaults YAML. +// +// Surface: the show-defaults CLI subcommand dumps these bytes to +// stdout so operators can copy them into /.zddc and edit. +func EmbeddedDefaultsBytes() []byte { + out := make([]byte, len(defaultsBytes)) + copy(out, defaultsBytes) + return out +} + +var ( + embeddedDefaultsOnce sync.Once + embeddedDefaults ZddcFile + embeddedDefaultsErr error +) + +// EmbeddedDefaults returns the parsed embedded defaults ZddcFile, +// memoised. Parse errors surface on the first call and are sticky. +// +// The cascade walker (EffectivePolicy) consults this as the bottom- +// most level unless an on-disk .zddc up the chain sets `inherit: false`. +func EmbeddedDefaults() (ZddcFile, error) { + embeddedDefaultsOnce.Do(func() { + embeddedDefaults, embeddedDefaultsErr = parseBytes(defaultsBytes) + }) + return embeddedDefaults, embeddedDefaultsErr +} diff --git a/zddc/internal/zddc/defaults.zddc.yaml b/zddc/internal/zddc/defaults.zddc.yaml new file mode 100644 index 0000000..5993bb9 --- /dev/null +++ b/zddc/internal/zddc/defaults.zddc.yaml @@ -0,0 +1,29 @@ +# defaults.zddc — embedded baseline configuration for every ZDDC +# deployment. Baked into the binary via //go:embed in defaults.go, +# loaded as the bottom-most level of the cascade. Operators override +# at the on-disk root /.zddc (or any deeper level); to ignore this +# file entirely, set `inherit: false` on an on-disk .zddc. +# +# Phase 1 of the .zddc-first-config rollout. Future phases will move +# the hardcoded canonical-folder behaviour (ProjectRootFolders, +# PartyFolders, apps.DefaultAppAt, etc.) into this file via a new +# `paths:` recursive map + a few new per-directory keys (default_tool, +# auto_own, virtual). For now this file is intentionally minimal — +# the plumbing exists, the schema doesn't. +# +# Read-only at runtime; the binary does not write to its embedded +# copy. To export an editable copy for an operator: +# +# zddc-server show-defaults > /var/lib/zddc/root/.zddc +# +# That places this file at the on-disk root, where the operator can +# edit it freely. The new file then takes the place of the embedded +# one (no double-counting — both contribute to the cascade, leaf wins). + +title: "ZDDC" + +# Phase 1: empty acl + empty admins, equivalent to "the embedded +# layer grants nothing; rules come from on-disk .zddc files above". +# This preserves bit-identical behaviour for existing deployments. +acl: + permissions: {} diff --git a/zddc/internal/zddc/defaults_test.go b/zddc/internal/zddc/defaults_test.go new file mode 100644 index 0000000..4ab97c9 --- /dev/null +++ b/zddc/internal/zddc/defaults_test.go @@ -0,0 +1,100 @@ +package zddc + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestEmbeddedDefaultsParse — the shipped defaults.zddc.yaml must +// parse cleanly into a ZddcFile. Regression guard against accidental +// YAML syntax errors in the source-of-truth file. +func TestEmbeddedDefaultsParse(t *testing.T) { + zf, err := EmbeddedDefaults() + if err != nil { + t.Fatalf("EmbeddedDefaults: %v", err) + } + if zf.Title == "" { + t.Errorf("embedded defaults have no title") + } +} + +// TestEmbeddedDefaultsBytesDumpable — the bytes used by the show- +// defaults CLI must be non-empty and start with a comment so an +// operator pasting them into a real file sees the header. +func TestEmbeddedDefaultsBytesDumpable(t *testing.T) { + got := EmbeddedDefaultsBytes() + if len(got) == 0 { + t.Fatal("EmbeddedDefaultsBytes returned empty slice") + } + if !strings.HasPrefix(strings.TrimLeft(string(got), " \t"), "#") { + t.Errorf("expected leading comment, got: %q", string(got[:60])) + } +} + +// TestCascadeIncludesEmbeddedByDefault — a fresh deployment with no +// on-disk .zddc still gets the embedded defaults reachable via +// chain.Embedded. +func TestCascadeIncludesEmbeddedByDefault(t *testing.T) { + resetCache() + root := t.TempDir() + leaf := filepath.Join(root, "Proj") + if err := mkdirAll(leaf); err != nil { + t.Fatal(err) + } + chain, err := EffectivePolicy(root, leaf) + if err != nil { + t.Fatal(err) + } + if chain.Embedded.Title == "" { + t.Errorf("chain.Embedded.Title empty, want defaults title to populate") + } +} + +// TestCascadeInheritFalseDropsEmbedded — when an on-disk .zddc sets +// top-level `inherit: false`, the embedded layer is zeroed out. +func TestCascadeInheritFalseDropsEmbedded(t *testing.T) { + resetCache() + root := t.TempDir() + writeZddc(t, root, "title: 'op-managed'\ninherit: false\n") + leaf := filepath.Join(root, "Proj") + if err := mkdirAll(leaf); err != nil { + t.Fatal(err) + } + chain, err := EffectivePolicy(root, leaf) + if err != nil { + t.Fatal(err) + } + if chain.Embedded.Title != "" { + t.Errorf("chain.Embedded.Title = %q, want empty (inherit:false should drop embedded)", + chain.Embedded.Title) + } + // On-disk level still present. + if got := chain.Levels[0].Title; got != "op-managed" { + t.Errorf("Levels[0].Title = %q, want %q", got, "op-managed") + } +} + +// TestCascadeInheritTrueExplicitKeepsEmbedded — `inherit: true` +// explicitly is the same as omitting it (default behaviour). +func TestCascadeInheritTrueExplicitKeepsEmbedded(t *testing.T) { + resetCache() + root := t.TempDir() + writeZddc(t, root, "title: 'op-managed'\ninherit: true\n") + leaf := filepath.Join(root, "Proj") + if err := mkdirAll(leaf); err != nil { + t.Fatal(err) + } + chain, err := EffectivePolicy(root, leaf) + if err != nil { + t.Fatal(err) + } + if chain.Embedded.Title == "" { + t.Errorf("chain.Embedded.Title empty, want defaults to remain since inherit: true is the default") + } +} + +func mkdirAll(p string) error { + return os.MkdirAll(p, 0o755) +} diff --git a/zddc/internal/zddc/file.go b/zddc/internal/zddc/file.go index ba3aa72..ee00e61 100644 --- a/zddc/internal/zddc/file.go +++ b/zddc/internal/zddc/file.go @@ -154,6 +154,22 @@ type ZddcFile struct { // it. The auto-generated .zddc grants the creator's email directly via // ACL.Permissions, the same way operators grant access to anyone else. CreatedBy string `yaml:"created_by,omitempty" json:"created_by,omitempty"` + + // Inherit controls whether the cascade walker descends below this + // .zddc into lower (ancestor) levels — including the embedded + // defaults that sit at the bottom. Defaults to true (cascade walks + // to the root + embedded layer). + // + // Set to false on a .zddc to stop the descent: that level becomes + // the bottom of the effective cascade. Use this at the on-disk + // root /.zddc to fully ignore the embedded defaults (the operator + // takes full responsibility for spelling out every rule from + // scratch). Useful at deeper levels too — e.g. a sandbox subtree + // that wants none of the project's policy. + // + // Pointer so an unset value (nil) is distinguishable from explicit + // false. nil == defaults to true. + Inherit *bool `yaml:"inherit,omitempty" json:"inherit,omitempty"` } // ParseFile reads and parses a .zddc YAML file. @@ -166,7 +182,15 @@ func ParseFile(path string) (ZddcFile, error) { if err != nil { return ZddcFile{}, err } + return parseBytes(data) +} +// parseBytes is the shared YAML→ZddcFile path used by ParseFile and +// EmbeddedDefaults. Returns a zero ZddcFile if data is empty. +func parseBytes(data []byte) (ZddcFile, error) { + if len(data) == 0 { + return ZddcFile{}, nil + } var zf ZddcFile if err := yaml.Unmarshal(data, &zf); err != nil { return ZddcFile{}, err