diff --git a/zddc/internal/zddc/defaults.zddc.yaml b/zddc/internal/zddc/defaults.zddc.yaml
index 177887d..7b79559 100644
--- a/zddc/internal/zddc/defaults.zddc.yaml
+++ b/zddc/internal/zddc/defaults.zddc.yaml
@@ -119,13 +119,30 @@ paths:
# 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]
- # First write into incoming/ auto-creates an owner
- # grant so the creator can manage their own drops.
auto_own: true
- # Browse shows a drag-drop overlay here.
drop_target: true
+ acl:
+ permissions:
+ document_controller: rwcd
# received/ and issued/ are WORM (write-once-read-many).
# The `worm:` list marks the zone:
#
diff --git a/zddc/internal/zddc/standardroles_test.go b/zddc/internal/zddc/standardroles_test.go
index 60ac1eb..8ced4ab 100644
--- a/zddc/internal/zddc/standardroles_test.go
+++ b/zddc/internal/zddc/standardroles_test.go
@@ -50,6 +50,8 @@ func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
mustVerbs(filepath.Join(root, "Proj", "random-folder"), "rw")
// archive/: rwc (can create party folders).
mustVerbs(filepath.Join(root, "Proj", "archive"), "rwc")
+ // incoming/: rwcd — the QC + transfer-out workflow needs delete.
+ mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "incoming"), "rwcd")
// received/ (WORM): rw masked to r, plus worm-restored c → "rc".
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "received"), "rc")
mustVerbs(filepath.Join(root, "Proj", "archive", "Acme", "issued"), "rc")
@@ -117,4 +119,10 @@ created_by: alice@example.com
if got := EffectiveVerbs(chain2, alice, ModeDelegated); got.String() != "r" {
t.Errorf("alice in archive/Acme = %q, want r", got.String())
}
+ // Alice CANNOT write to incoming/ — that's the counterparty's drop
+ // zone, QC'd by the document controller. project_team gets read only.
+ chain3, _ := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme", "incoming"))
+ if got := EffectiveVerbs(chain3, alice, ModeDelegated); got.String() != "r" {
+ t.Errorf("alice in incoming/ = %q, want r (no create/write for project_team)", got.String())
+ }
}