ZDDC/zddc/internal/zddc/defaults.zddc.yaml
ZDDC 7fbe7867fd feat(zddc): defaults — browse hosts the markdown editor for working/+reviewing/
Flip default_tool from `mdedit` to `browse` (which now ships a Toast UI
markdown editor plugin in its preview pane) at:

  • paths."*".paths.working
  • paths."*".paths.working.paths."*"   (per-user homes)
  • paths."*".paths.reviewing

available_tools at those levels drops `mdedit` and adds `browse` next
to `classifier`. Operator overrides per .zddc cascade still work; only
the embedded baseline changes.

Test fixtures updated:

  • lookups_test.go     — DefaultToolAt assertions for working/+reviewing/
  • availability_test.go — AppAvailableAt + DefaultAppAt for working/+
                           reviewing/+per-user home
  • main_test.go        — dispatch route asserts "ZDDC Browse" (was "ZDDC
                          Markdown"); Apps cascade fixture swaps mdedit
                          for browse so the live route fetches the right
                          embedded HTML
2026-05-13 10:34:06 -05:00

241 lines
12 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 ─────────────────────────────────────────────────────────
#
# Two roles ship empty (no members) — a fresh deployment grants
# nothing until an operator populates them. They're referenced by the
# project-scoped grants in paths: below.
#
# Role membership UNIONS across the cascade: an on-disk .zddc that
# defines `project_team` again with one extra member ADDS that member
# to the inherited role. To start fresh at a subtree (e.g. a project
# wanting its own team independent of a deployment-wide default), use
# `reset: true` on the role at that level — ancestor definitions above
# the reset are then excluded.
#
# document_controller — the people who file into archive/<party>/
# received/ and issued/ (WORM zones). They get read+write-once-
# create there (via the worm: lists below) and read/write
# elsewhere in a project, plus subtree-admin of working/ and
# staging/ so they can stand up new top-level folders and manage
# user/staging subtrees. They are NOT subtree-admin of archive/,
# so the WORM constraint still binds them in received/issued.
#
# project_team — everyone working on a project. Read-only across
# the project. Their own working/<email>/ home and anything they
# create under incoming/ get a creator-owned auto-own .zddc
# (rwcda) which wins via deepest-match, so "read-only except
# what I own" falls out of the cascade with no special rule.
roles:
document_controller:
members: []
project_team:
members: []
# Universal tool baseline. archive (record browser), browse (file
# tree, hosts the in-place markdown editor), and landing (project
# picker) work everywhere. Each canonical folder below adds its own
# context-specific tools (transmittal in staging/, etc.). The cascade
# unions available_tools across all levels — 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, each served by a configurable
# tool:
#
# <dir>/ (trailing slash) → `dir_tool` — the directory view.
# Defaults to `browse` (file-tree
# navigator). This is the site-wide
# default; you rarely set it.
# <dir> (no slash) → `default_tool` — the "specialized
# app" for this folder (e.g. archive,
# transmittal, tables). If a folder
# declares no default_tool, the no-
# slash form just 302s to the slash
# form, so you land on `dir_tool`.
#
# JSON listing requests are unaffected by either key — they always get
# the raw directory listing, so the browse SPA (and any other client)
# can enumerate entries no matter what dir_tool/default_tool are.
#
# Both keys cascade leaf→root: a parent's default_tool applies to
# descendants unless a deeper level overrides it (browse set on
# working/ reaches working/alice/notes/ for free). The keys below set
# default_tool on the canonical folders; dir_tool is left unset
# everywhere, so the slash form is always `browse`.
#
# ── Canonical project structure ────────────────────────────────────────────
#
# Every ZDDC project lives at a top-level directory. Under it the
# convention is four canonical folders: archive (formal record),
# working (in-progress workspace), staging (outbound prep), reviewing
# (purely virtual aggregator). Under archive/<party>/ the convention
# is four more: mdl (deliverables list), incoming (counterparty drop
# zone), received (immutable submittals), issued (immutable responses).
#
# All of this is expressed via the recursive paths: schema. None of
# the directories need to exist on disk — the cascade walker resolves
# behaviour from this declaration, so a fresh project lands on
# usable empty views at every well-known URL.
#
# Operators override any of this by mirroring the structure in an
# on-disk .zddc and changing what they need; on-disk values win.
paths:
# First segment under root is the project name; "*" matches any.
"*":
# Project-scoped baseline ACL. project_team gets read across the
# project; document_controller gets read + overwrite-existing
# (so people can ask them to fix a stuck file). Neither gets
# `c` (create) at this level — that's granted only at the
# specific spots below (archive/, working/, staging/), so the
# doc controller can't make arbitrary folders. Grants here cap
# at deeper levels per deepest-match-wins, except where a deeper
# .zddc restates a fuller grant for the same principal.
acl:
permissions:
project_team: r
document_controller: rw
paths:
archive:
default_tool: archive
# The doc controller can create party subfolders here
# (archive/<party>/). Restate the full grant — deepest-match
# is per-principal replacement, so we re-list rw + add c.
acl:
permissions:
document_controller: rwc
paths:
# Second segment under archive/ is the party name.
"*":
# When the doc controller creates a party folder, an
# auto-own .zddc grants them rwcda there (UNFENCED — so
# the project-level project_team:r still cascades through
# to received/issued). That lets them set up the
# counterparty's own .zddc afterward.
auto_own: true
paths:
mdl:
default_tool: tables
available_tools: [tables]
# The mdl folder is virtual by convention — the
# tables tool serves it from the embedded default
# spec even when the on-disk folder doesn't exist.
virtual: true
incoming:
# incoming/ is the COUNTERPARTY's drop zone. The flow:
# 1. the other party's document controller uploads
# files here (a deployment grants them cr at this
# path, e.g. acl: { permissions: { "*@acme.com": cr } }
# at archive/Acme/incoming/.zddc — or they mkdir a
# dated subfolder under incoming/ and own it via
# auto_own)
# 2. OUR document controller QCs them via classifier
# (rename in place) and moves them to received/
# (which needs delete here + worm-create there),
# ideally returning a signed transmittal in issued/
#
# The normal project_team has only read here (inherited
# from the project level — they have no c/w) so they can
# see what's been dropped but not touch it. The
# document_controller grant restates rwcd so the QC +
# transfer-out workflow has the delete bit it needs.
default_tool: classifier
available_tools: [classifier]
auto_own: true
drop_target: true
acl:
permissions:
document_controller: rwcd
# received/ and issued/ are WORM (write-once-read-many).
# The `worm:` list marks the zone:
#
# - write (w) and delete (d) are stripped for EVERYONE
# - create (c) is stripped for everyone EXCEPT the
# principals listed — they get read + write-once-
# create ("cr")
# - read for non-listed principals is whatever the
# normal cascade ACL granted; the WORM list does not
# itself confer read to outsiders
# - admins (root / subtree) bypass entirely — the
# human escape hatch for mis-filed documents
#
# The baseline is an empty list: WORM zone, no
# create-capable principals — filing is locked until a
# deployment names a document controller, e.g.
#
# worm: ["doc-control@example.com"]
#
# at received/ (or issued/, or archive/<party>/, or
# wherever scopes it right). worm: lists UNION across the
# cascade, so a deeper .zddc adds more controllers.
received:
default_tool: archive
# document_controller may file write-once into the
# WORM zone. Their project-level rw is masked here
# to r; worm: restores write-once-create.
worm: [document_controller]
issued:
default_tool: archive
worm: [document_controller]
working:
default_tool: browse
available_tools: [browse, classifier]
# working/ auto-owns the first creator + the per-user homes
# below.
auto_own: true
drop_target: true
# Doc controller is subtree-admin of working/ — full create
# + manage, including taking over a fenced per-user home if a
# user leaves. (Scoped here, not at the project root, so the
# WORM constraint in archive/<party>/received|issued still
# binds them.)
admins: [document_controller]
paths:
"*": # per-user home dir
default_tool: browse
available_tools: [browse, classifier]
auto_own: true
# Per-user home is private by default: the generated
# auto-own .zddc carries inherit:false so ancestor ACL
# grants don't reach inside. The user can edit the file
# to grant collaborators access.
auto_own_fenced: true
drop_target: true
staging:
default_tool: transmittal
available_tools: [transmittal, classifier]
auto_own: true
drop_target: true
# Doc controller is subtree-admin of staging/ too — same
# rationale as working/.
admins: [document_controller]
reviewing:
default_tool: browse
available_tools: [browse]
# reviewing/ is purely virtual — the aggregator handler
# synthesises listings from received/ ↔ staging/ ↔ issued/.
virtual: true