ZDDC/zddc/internal/zddc/special.go
ZDDC 1e0e403f1e feat(zddc): retire defaults.zddc.yaml; .zddc.zip is the policy carrier (phase 6)
Completes the migration. The embedded per-depth tree (internal/zddc/defaults/)
is now the sole source of the shipped baseline; defaults.zddc.yaml is deleted.

  - EmbeddedDefaults() assembles the tree (no yaml). show-defaults now emits a
    .zddc.zip (per-depth, "*" wildcard members) via EmbeddedDefaultsZip() —
    operators redirect it to <ROOT>/.zddc.zip (or any directory) and edit/add/
    delete individual members.
  - Dropped EmbeddedDefaultsBytes; reworked the dumpable test to validate the
    emitted zip; removed the now-redundant tree-vs-yaml oracle (the Layer-2
    matrix is the ongoing behavioral guarantee, and it stays green).
  - Swept stale "defaults.zddc.yaml" comment references to the embedded tree.
  - GRAMMAR.md §1/§6 updated: .zddc.zip is a policy bundle mountable at ANY
    directory (subtree mount; inherit:false + acl.inherit:false = island); the
    shipped baseline is the embedded bundle at the root.

Net of the 6-phase migration: policy is per-depth .zddc files in a .zddc.zip
that an operator can drop at any level to override the cascade; the engine
(Assemble + the unchanged walker) enforces it. Full Go suite + matrix green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 11:35:21 -05:00

103 lines
3.9 KiB
Go

package zddc
import (
"os"
"strings"
)
// The .zddc cascade is the authority for canonical-folder behaviour;
// see internal/zddc/defaults/ for the conventions and lookups.go for the
// helpers consumers call (DefaultToolAt, AutoOwnAt, VirtualAt,
// IsDeclaredPath, ChildrenDeclaredAt, AvailableToolsAt).
// WriteAutoOwnZddc serialises a creator-grant .zddc into dir, granting
// principalEmail rwcda and recording it in CreatedBy. Each role name
// in roles also receives rwcda — gives the schema a way to declare
// "this folder is creator-owned AND any member of these roles has full
// authority" without using subtree-admin (which would bypass WORM /
// fences via elevation). Used by the file API's mkdir post-hook (and
// by EnsureCanonicalAncestors) to seed ownership when a new auto-own
// folder is materialised. Pass nil/empty roles for the legacy
// creator-only behavior.
//
// The grants are identical to what an operator would write by hand —
// direct email pattern + bare role names, "rwcda" verb set — so the
// creator can later edit the file normally to narrow or extend them.
//
// Atomic: marshals via the same yaml encoder ParseFile reads
// (round-trip guaranteed) and writes via zddc.WriteFile (which
// performs an atomic temp-write + rename via zddc.WriteAtomic).
func WriteAutoOwnZddc(dir, principalEmail string, roles []string) error {
return writeAutoOwn(dir, principalEmail, false, roles)
}
// WriteAutoOwnZddcFenced is the same as WriteAutoOwnZddc but additionally
// sets `acl.inherit: false` — fencing ancestor cascade grants. Used at
// per-user home folders under working/ where the convention is "private
// by default; owner edits the file to add collaborators."
//
// Without the fence, an ancestor `*: r` (e.g. a project-root grant for
// authenticated users) would let any user read every other user's
// working subfolder via cascade — defeating the per-user sandbox.
//
// roles is the same as for WriteAutoOwnZddc — listed roles get rwcda
// alongside the creator, and like the creator grant they're INSIDE
// the fence (only resolvable if the role is defined at this level or
// in chain.Embedded, since ancestor role definitions are hidden by
// inherit:false). Typically callers using the fenced variant pass nil
// roles — per-user homes don't need peer authority.
func WriteAutoOwnZddcFenced(dir, principalEmail string, roles []string) error {
return writeAutoOwn(dir, principalEmail, true, roles)
}
func writeAutoOwn(dir, principalEmail string, fenced bool, roles []string) error {
perms := map[string]string{principalEmail: "rwcda"}
for _, role := range roles {
if role == "" || role == principalEmail {
continue // skip empty / collision with the creator entry
}
perms[role] = "rwcda"
}
rules := ACLRules{Permissions: perms}
if fenced {
f := false
rules.Inherit = &f
}
zf := ZddcFile{
ACL: rules,
CreatedBy: principalEmail,
}
return WriteFile(dir, zf)
}
// ResolveCanonical returns the on-disk name of the canonical folder
// 'logical' (lowercase) inside parentDir, or "" if no case variant
// exists. Caller decides whether to MkdirAll(parentDir+"/"+logical)
// when "" is returned.
//
// 'logical' is matched case-insensitively against entries returned by
// os.ReadDir(parentDir). The first matching directory entry wins (if
// an operator created both Working/ and working/ on a case-sensitive
// filesystem, the order is filesystem-dependent — that's an unsupported
// state we don't try to recover from).
//
// Returns "" with no error if parentDir doesn't exist or has no match.
func ResolveCanonical(parentDir, logical string) (string, error) {
entries, err := os.ReadDir(parentDir)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
for _, e := range entries {
if !e.IsDir() {
continue
}
if strings.EqualFold(e.Name(), logical) {
return e.Name(), nil
}
}
return "", nil
}