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>
319 lines
14 KiB
YAML
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]
|