ZDDC/zddc/internal/zddc/defaults.zddc.yaml
ZDDC 9552b297e7 fix(project-create): seed role membership only; grant team rwc on mdl/rsk
My earlier create-project flow wrote per-role verb grants (project_team: rwc,
…) at the PROJECT ROOT, which cascaded create/write across the whole project —
wrong. The project root is structurally locked to canonical peers
(rejectProjectRootMkdir), and the embedded defaults already grant each role its
per-FOLDER permissions ("None gets `c` here — create is granted only at the
specific peers below").

Project-create now writes role MEMBERSHIP only (document_controller /
project_team / observer) plus admins + created_by. Membership unions across the
cascade, so naming members at the project root makes the embedded per-peer
grants apply to them. No acl.permissions is seeded (the advanced field is still
an escape hatch). The dialog's "Guests" maps to the defaults' read-only
`observer` role (was a non-existent `guest` role that hooked no grants).

Per decision, MDL & RSK are now collaboratively editable: defaults grant
project_team rwc (create + edit, no delete) at mdl/ and rsk/ alongside
document_controller rwcd — the history: audit on both covers every change.

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

319 lines
14 KiB
YAML

# 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.
#
# 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 (both contribute to the cascade, on-disk wins per-field).
title: "ZDDC"
# Empty acl at this layer — rules come from on-disk .zddc files above.
# A deployment with no on-disk root .zddc grants no access (consistent
# with prior behaviour); operators bootstrap by editing the root file.
acl:
permissions: {}
# ── Standard roles ─────────────────────────────────────────────────────────
#
# Three roles ship empty (no members) — a fresh deployment grants
# nothing until an operator populates them. Membership UNIONS across
# the cascade; use `reset: true` at a subtree to start fresh.
#
# document_controller — owns the committed record and the party
# registry. They:
# - register parties: a party EXISTS iff ssr/<party>.yaml exists,
# and the DC creates it (rwc at ssr/). This is the single
# source of truth for party existence.
# - file write-once into the WORM archive: read + create at
# archive/<party>/received and issued via the worm: list (the
# WORM mask strips w/d/a; create survives only for listed
# principals). archive/ also grants rwc so the DC can create
# party record dirs.
# - rwcda across the live workspaces (incoming/working/staging/
# reviewing), restated per-peer so a DC matched by the
# project_team wildcard keeps full authority via within-level
# union.
# NOT a subtree-admin anywhere — no admins: entry. DCs cannot
# bypass WORM (only worm-create); admin elevation is reserved for
# the root admins: list (the human escape hatch for mis-filed
# documents or recovery).
#
# project_team — everyone working on a project. Read across the
# project, with a one-way ratchet through the live workspaces:
# working/ cr create + read; auto_own gives the creator
# rwcda inside the party folder they make
# staging/ cr drop + read, no modify after the drop
# reviewing/ cr create + read review iterations
# incoming/ r counterparty's drop zone (observe)
# archive/ r the committed record (received/issued), WORM
# ssr/mdl/rsk r registry + registers (the DC maintains them)
# Each handoff drops the role's modify rights for the previous
# stage.
#
# observer — pure read-only across the project; no create anywhere.
# Intended for auditors, regulators, and external read-only
# viewers who must not contribute content.
roles:
document_controller:
members: []
project_team:
members: []
observer:
members: []
# Universal tool baseline. archive (record browser), browse (file
# tree, hosts the in-place markdown editor), and landing (project
# picker) work everywhere. Each peer below adds its own tools
# (transmittal in staging/, tables in mdl/rsk/ssr, etc.). available_tools
# UNIONS across the cascade — leaf restrictions don't drop ancestor
# entries — so this baseline propagates to every descendant.
available_tools: [archive, browse, landing]
# ── The slash / no-slash routing convention ────────────────────────────────
#
# Every directory URL has two forms:
#
# <dir>/ (trailing slash) → `dir_tool` — the directory view
# (defaults to `browse`, the file-tree
# navigator; you rarely set it).
# <dir> (no slash) → `default_tool` — the specialized app
# for this folder (archive, transmittal,
# tables). If a folder declares no
# default_tool, the no-slash form 302s
# to the slash form.
#
# JSON listing requests are unaffected — they always get the raw
# directory listing, so the browse SPA (and any client) can enumerate
# entries regardless of dir_tool/default_tool. Both keys cascade
# leaf→root.
#
# ── Canonical project structure (top-level party peers) ─────────────────────
#
# A project is a top-level directory. Under it sit a FLAT set of
# physical, party-partitioned peers — there are no virtual aggregators:
#
# archive/<party>/{received,issued}/ the committed record. PURE
# WORM (one rule on archive/, no
# exceptions): write/delete
# stripped for all; create only
# for document_controller (the
# worm: list); admins bypass.
# Party record dirs appear on the
# first filing.
# incoming/<party>/ counterparty drop zone
# reviewing/<party>/<tracking>/ we review their submission
# working/<party>/ our drafts (edit-history on)
# staging/<party>/<tracking>/ assemble transmittals
# mdl/<party>/*.yaml master document list (tables)
# rsk/<party>/*.yaml risk register (tables)
# ssr/<party>.yaml submittal status register — AND
# the AUTHORITATIVE PARTY REGISTRY
#
# Party registry: `ssr/<party>.yaml` existence is the SINGLE source of
# truth for "party <party> exists". Creating it (rwc at ssr/, via the
# SSR form) is how a party is born. Every OTHER peer carries
# `party_source: ssr`, so you cannot create <peer>/<party>/… — archive
# filing included — until the ssr row exists; the server 409s otherwise.
# ssr/ itself has no party_source (it is the source).
#
# mdl/ and rsk/ AGGREGATE: the peer root renders ALL parties in one
# table (a $party column derived from the real subdir), <peer>/<party>/
# shows that party's rows. ssr/ aggregates naturally (one flat file per
# party). $party is a real directory level, not a synthesized column.
#
# Mkdir at the project root is restricted to the peer names above plus
# system (_/.-prefixed) names (see handler/fileapi.go). Nothing here
# needs to exist on disk — the cascade resolves behaviour so a fresh
# project lands on usable empty views at every well-known URL. Operators
# override by mirroring this structure in an on-disk .zddc.
paths:
# First segment under root is the project name; "*" matches any.
"*":
# Project-scoped baseline ACL. project_team and observer read across
# the project; document_controller gets read + overwrite-existing.
# None gets `c` here — create is granted only at the specific peers
# below (archive/, ssr/, and the workspaces).
acl:
permissions:
project_team: r
observer: r
document_controller: rw
paths:
# ── The committed record: pure WORM ─────────────────────────
archive:
default_tool: archive
# A record can only be filed for a registered party.
party_source: ssr
# The ONE WORM rule. Cascades to <party>/{received,issued}:
# write/delete stripped for everyone; create survives only for
# document_controller; admins bypass (the escape hatch).
worm: [document_controller]
# rwc so a DC can create party record dirs (WORM masks w/d to
# leave read + write-once-create).
acl:
permissions:
document_controller: rwc
# ── Authoritative party registry + submittal status register ─
ssr:
default_tool: tables
available_tools: [tables]
# NO party_source — ssr/ IS the source of party existence.
# rwc: a DC registers a party by creating ssr/<party>.yaml and
# maintains its status (overwrite). Delete (de-register) is left
# to admins so a party with archived records is never orphaned.
acl:
permissions:
document_controller: rwc
history: true
records:
"*.yaml":
field_defaults:
kind: SSR
locked: [kind]
# ── Inbound workspace: counterparty drop zone ───────────────
incoming:
default_tool: classifier
available_tools: [classifier]
party_source: ssr
# The other party's DC uploads here (a deployment grants them
# cr, e.g. acl: { permissions: { "*@acme.com": cr } } at
# incoming/Acme/.zddc); OUR DC QCs via classifier and moves to
# archive/<party>/received. project_team has read only (observe).
acl:
permissions:
document_controller: rwcd
paths:
"*": # incoming/<party>
auto_own: true
drop_target: true
# ── Inbound workspace: review of their submission ───────────
reviewing:
default_tool: browse
available_tools: [browse]
party_source: ssr
# The Plan-Review composite endpoint scaffolds a folder here per
# submittal under review, with a .zddc carrying received_path
# back to the canonical record in archive/<party>/received.
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # reviewing/<party>
auto_own: true
drop_target: true
# ── Outbound workspace: our drafts (edit-history on) ────────
working:
default_tool: browse
available_tools: [browse, classifier]
party_source: ssr
# Subtree-inheriting: every markdown save under working/ is
# snapshotted to .zddc.d/history/<stem>/ with a server-stamped
# audit line. Reads of recorded history never require this flag.
history: true
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # working/<party> — auto-owned by its creator
auto_own: true
drop_target: true
# ── Outbound workspace: assemble transmittals ───────────────
staging:
default_tool: transmittal
available_tools: [transmittal, classifier]
party_source: ssr
# project_team drops files (cr); after the drop the doc-control
# workflow owns it. DC gets rwcda — `d` for the cut to issued/,
# `a` so Plan Review can write staging/<tracking>/.zddc.
acl:
permissions:
project_team: cr
document_controller: rwcda
paths:
"*": # staging/<party>
auto_own: true
drop_target: true
# ── Master document list (aggregates across parties) ────────
mdl:
default_tool: tables # peer root: all-parties table
available_tools: [tables]
party_source: ssr
history: true
# The deliverables register is collaboratively editable: the DC
# manages it (rwcd) and project_team can create + edit rows (rwc,
# no delete) — every change is captured by the history: audit above,
# so broad write is safe. This project_team: rwc overrides the
# project-level project_team: r (deepest matching level wins).
acl:
permissions:
document_controller: rwcd
project_team: rwc
# field_codes: constrain tracking-number components here (or
# higher in the cascade). Three kinds — enum / pattern / free;
# map-merge across levels. originator is folder-bound (below),
# so it is not listed here. Example:
# field_codes:
# discipline: { kind: enum, codes: { EL: Electrical, ME: Mechanical } }
# sequence: { kind: pattern, pattern: "[0-9]{4}" }
paths:
"*": # mdl/<party>: that party's rows, flat
default_tool: tables
# MDL records: each .yaml is an independent deliverable with
# its own composed tracking number. originator is the party
# folder (the record's own dir, distance 0 above
# mdl/<party>/<file>.yaml) and renders read-only — the folder
# is the single source of truth for the originator code.
#
# To add project-wide components (phase, area, …), override
# filename_format here AND mdl/<party>/{form,table}.yaml.
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}"
# ── Risk register (aggregates across parties) ───────────────
rsk:
default_tool: tables
available_tools: [tables]
party_source: ssr
history: true
# Same as mdl/: DC manages (rwcd), project_team creates + edits rows
# (rwc, no delete); the history: audit covers every change.
acl:
permissions:
document_controller: rwcd
project_team: rwc
paths:
"*": # rsk/<party>
default_tool: tables
# RSK records: each .yaml is a row of a parent rsk-type
# deliverable; the server auto-assigns -{row} within the
# row-scope group on POST-create. originator is folder-bound
# to the party folder, same as MDL.
records:
"*.yaml":
folder_fields:
originator: 0
filename_format: "{originator}-{project}-{discipline}-{type}-{sequence}-{suffix?}-{row}"
field_defaults:
type: RSK
locked: [type]
row_field: row
row_scope_fields: [originator, project, discipline, type, sequence, suffix]