From 84c1b58b6655116c171a6f2350c2c6add1b04251 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Tue, 9 Jun 2026 19:57:13 -0500 Subject: [PATCH] =?UTF-8?q?docs:=20fix=20stale=20"fenced/private=20home"?= =?UTF-8?q?=20claims=20=E2=80=94=20default=20homes=20are=20shared?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The auto_own_fenced mechanism (private per-creator home via inherit:false) still exists, but the current default tree sets it NOWHERE — the working/staging/ incoming/reviewing homes are auto_own but UNFENCED, so ancestor grants (project_team: cr at working/) cascade in and they are shared team folders. Code comments (file.go AutoOwnFenced, special.go WriteAutoOwnZddcFenced, ensure.go, fileapi.go) and AGENTS.md (role model + the auto_own_fenced key) still described per-user homes as fenced/private-by-default — a pre-reshape artifact. Correct them: fencing is an opt-in not used by the default tree; the party homes are unfenced/shared. No behavior change (grep finds no auto_own_fenced in internal/zddc/defaults). From the deferred-findings triage. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 8 ++++---- zddc/internal/handler/fileapi.go | 10 ++++++---- zddc/internal/zddc/ensure.go | 20 ++++++++++---------- zddc/internal/zddc/file.go | 15 +++++++++------ zddc/internal/zddc/special.go | 17 +++++++++-------- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 023a864..d46f52e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -526,12 +526,12 @@ The in-flight lifecycle slots form a one-way ratchet: `working/` → `staging/` → `issued/` (WORM) -Each handoff drops `project_team`'s modify rights for the slot they pushed from. At `working/` they have `cr` plus `rwcda` inside their auto-own-fenced `/` home. At `staging/` they have `cr` only (drop files, no modify after). DC takes over with `rwcd` at staging and files to `issued/` via the WORM `cr` grant. Same shape on the inbound side via `incoming/` → `received/` (WORM). +Each handoff drops `project_team`'s modify rights for the slot they pushed from. At `working/` they have `cr` plus `rwcda` inside the `/` folder they create (auto-owned but **unfenced** — `working/` is a shared team space, so peers keep their `cr` there). At `staging/` they have `cr` only (drop files, no modify after). DC takes over with `rwcd` at staging and files to `issued/` via the WORM `cr` grant. Same shape on the inbound side via `incoming/` → `received/` (WORM). Pick a role per persona: -- `document_controller` — per-party records custodian; files into WORM `received/issued`, manages the `working/staging/reviewing` lifecycle, QCs the counterparty's drops in `incoming/`. **NOT a subtree-admin** anywhere — authority comes purely from cascade grants (the role-level `rwcda` written by `auto_own_roles` at each party, plus explicit `rwcd` at `incoming/` and `staging/`). They cannot bypass WORM (only worm-create via the list) or reach inside fenced working homes. -- `project_team` — day-to-day contributor. Reads across the project; ratchets through the in-flight slots. Owns their `archive//working//` home via auto-own with a fenced `.zddc` (`inherit: false`). +- `document_controller` — per-party records custodian; files into WORM `received/issued`, manages the `working/staging/reviewing` lifecycle, QCs the counterparty's drops in `incoming/`. **NOT a subtree-admin** anywhere — authority comes purely from cascade grants (the role-level `rwcda` written by `auto_own_roles` at each party, plus explicit `rwcd` at `incoming/` and `staging/`). They cannot bypass WORM (only worm-create via the list). +- `project_team` — day-to-day contributor. Reads across the project; ratchets through the in-flight slots. Auto-owns (`rwcda`) the `working//` folder they create; it is **unfenced**, so `working/` stays a shared team space (every `project_team` member keeps cascade `cr` there). A per-directory `auto_own_fenced` opt-in (not set in the default tree) would make it private. - `observer` — pure read-only across the project. No auto-own home (the role itself has no `c` anywhere). Intended for auditors, regulators, and external read-only viewers who must not contribute content. **Roles overlap on purpose.** DCs are typically internal employees and ARE in `project_team` (often defined as `*@example.com`). The cascade is "deepest level that has any matching principal wins for that level, with within-level UNION of all matched principals". To prevent a `project_team: cr` grant at the slot from shadowing a DC's role-level `rwcda` inherited from the party folder, the embedded defaults RESTATE `document_controller: rwcda` at every slot that has a project_team-specific grant (`working/`, `staging/`, `reviewing/`). Within-level union → DC gets `rwcda` ∪ `cr` = `rwcda`. Operators adding new slot-level project_team grants in their own `.zddc` files should follow the same pattern. (Internal `observer` users matched by the project_team wildcard would still be lifted to `cr` by the union — observer is intended for EXTERNAL auditors whose emails don't match the wildcard. Deployments with internal observers should use explicit project_team membership instead of a wildcard.) @@ -544,7 +544,7 @@ Pick a role per persona: - `roles: { : { members: [...], reset: ? } }` — members union across the cascade unless `reset: true`. - `admins: [, ...]` — root only; sudo-style elevation per request. - `auto_own: ` — when true, ensure.go writes a `.zddc` granting the creator's email `rwcda` on first mkdir. -- `auto_own_fenced: ` — adds `inherit: false` to the auto-own `.zddc` (private-by-default home). No effect without `auto_own: true`. +- `auto_own_fenced: ` — adds `inherit: false` to the auto-own `.zddc`, making the directory private to its creator (ancestor grants don't cascade in). **Opt-in — not set anywhere in the default tree**, so the default working/staging/incoming/reviewing party homes are unfenced/shared. No effect without `auto_own: true`. - `auto_own_roles: [, ...]` — additional role names that get `rwcda` in the auto-own `.zddc`, alongside the creator's email. Lets the schema express role-level peer authority without `admins:` (which would be subtree-admin and bypass WORM/fences via elevation). - `title:` — read only from the per-project `.zddc`; surfaces on the landing-page picker. diff --git a/zddc/internal/handler/fileapi.go b/zddc/internal/handler/fileapi.go index ef67253..41a3a87 100644 --- a/zddc/internal/handler/fileapi.go +++ b/zddc/internal/handler/fileapi.go @@ -821,10 +821,12 @@ func serveFileMkdir(cfg config.Config, w http.ResponseWriter, r *http.Request) { // - abs's parent is declared auto_own — every child mkdir under // an auto-own folder (working/, staging/, archive//, // archive//incoming/, …) gets the creator's grant. - // The fence (inherit:false) follows abs's own cascade level: - // per-user homes under working/ declare auto_own_fenced, so the - // generated .zddc is private; other auto-own positions are - // unfenced so ancestor grants still cascade through. + // The fence (inherit:false) follows abs's own cascade level via + // AutoOwnFencedAt. It is an opt-in the default tree does not set — + // the working/staging/incoming/reviewing party homes are auto-owned + // but UNFENCED, so ancestor grants (e.g. project_team cr) cascade + // through and they behave as shared team folders. An operator can + // set auto_own_fenced on a position to make it private. if email != "" { if zddc.AutoOwnAt(cfg.Root, abs) || zddc.AutoOwnAt(cfg.Root, filepath.Dir(abs)) { roles := zddc.AutoOwnRolesAt(cfg.Root, abs) diff --git a/zddc/internal/zddc/ensure.go b/zddc/internal/zddc/ensure.go index 168a34e..6d30d23 100644 --- a/zddc/internal/zddc/ensure.go +++ b/zddc/internal/zddc/ensure.go @@ -200,10 +200,11 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil // Determine if this newly-created ancestor is an auto-own // position and whether it should be fenced (inherit: false). - // Resolved via the .zddc cascade — internal/zddc/defaults/ - // carries the canonical "working/staging auto-own + per-user - // homes fenced + incoming auto-own" convention, and any - // on-disk .zddc can override per-directory. + // Resolved via the .zddc cascade — the embedded defaults + // (internal/zddc/defaults/) declare auto_own at the working/ + // staging/ incoming/ reviewing/ homes but do NOT fence + // them (they are shared team folders); an on-disk .zddc can opt + // a directory into fencing per-directory with auto_own_fenced. _ = parentSegs // depth-tracking no longer needed _ = i autoOwn := AutoOwnAt(fsRoot, pathSoFar) @@ -222,12 +223,11 @@ func EnsureCanonicalAncestors(fsRoot, target, principalEmail string, perm fs.Fil // Seed auto-own .zddc on the canonical positions that were freshly // created. Skip if no principal email is available (anonymous or - // system writes). The fenced variant is used at per-user home - // folders under working/ — private by default; owner can later - // edit the .zddc to add collaborators. Role grants (from the - // cascade's auto_own_roles list) are written alongside the - // creator email so role-level peer authority survives without - // needing a subtree-admin grant. + // system writes). The fenced variant (inherit:false, private to the + // creator) is an opt-in the default tree does not use — see + // AutoOwnFencedAt. Role grants (from the cascade's auto_own_roles + // list) are written alongside the creator email so role-level peer + // authority survives without needing a subtree-admin grant. if principalEmail != "" { for _, c := range freshlyCreated { if !c.autoOwn { diff --git a/zddc/internal/zddc/file.go b/zddc/internal/zddc/file.go index e990ef7..caae5b2 100644 --- a/zddc/internal/zddc/file.go +++ b/zddc/internal/zddc/file.go @@ -228,12 +228,15 @@ type ZddcFile struct { // created. Empty (nil) inherits via cascade. AutoOwn *bool `yaml:"auto_own,omitempty" json:"auto_own,omitempty"` - // AutoOwnFenced augments AutoOwn: when true, the generated .zddc - // is written with `inherit: false` so the new directory is - // private to its creator (ancestor ACL grants don't apply). Used - // for per-user home folders under working//. Default - // (nil/false) writes a non-fenced auto-own .zddc — ancestor - // admin grants still apply. + // AutoOwnFenced augments AutoOwn: when true, the generated .zddc is + // written with `inherit: false` so the new directory is private to its + // creator (ancestor ACL grants don't cascade in). It is an OPT-IN an + // operator can set on any auto_own position; the current embedded default + // tree does NOT set it anywhere — the working/ staging/ incoming/ + // reviewing/ homes are auto-owned but UNFENCED, so ancestor grants + // (e.g. `project_team: cr` at working/) still cascade in, making them + // shared team folders rather than private per-user sandboxes. Default + // (nil/false) writes a non-fenced auto-own .zddc. AutoOwnFenced *bool `yaml:"auto_own_fenced,omitempty" json:"auto_own_fenced,omitempty"` // AutoOwnRoles augments AutoOwn with role-level grants: when set, diff --git a/zddc/internal/zddc/special.go b/zddc/internal/zddc/special.go index 651d070..bda780a 100644 --- a/zddc/internal/zddc/special.go +++ b/zddc/internal/zddc/special.go @@ -32,20 +32,21 @@ func WriteAutoOwnZddc(dir, principalEmail string, roles []string) error { } // 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." +// sets `acl.inherit: false` — fencing ancestor cascade grants so the new +// directory is private to its creator. This is the OPT-IN private-home form: +// the current embedded default tree does NOT use it (the working/staging/ +// incoming/reviewing party homes are unfenced and shared — see AutoOwnFenced +// in file.go). An operator opts in by setting auto_own_fenced on a position. // -// 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. +// With the fence, an ancestor grant (e.g. `project_team: cr` at working/) +// does NOT cascade in, so only the creator (and any roles passed below) can +// reach the directory. // // 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. +// inherit:false). Callers using the fenced variant typically pass nil roles. func WriteAutoOwnZddcFenced(dir, principalEmail string, roles []string) error { return writeAutoOwn(dir, principalEmail, true, roles) }