feat(hierarchies): cross-repo template expansion (M7)

Route/trigger templates declared at a group now fan out to EVERY descendant
app in the DB subtree, not just the app nodes present in the current
`pic apply --dir`. A template change at group N reaches subtree(N) across
repos, and a removed/disabled template reaps its expansions on those
descendants too.

- group_repo: recursive `descendant_app_ids` (+ `_tx`) — inverse of the
  ancestor chain CTE, app_id-ordered, depth-bounded.
- apply_service: Phase B2 expands templates into descendants of every in-tree
  group not already handled in Phase B. All descendant locks are taken up
  front in the single sorted batch (their union is invariant under reparenting
  in-tree groups), so there is no out-of-order mid-tx locking / deadlock.
  Per-recipient write caps (AppWriteRoute / AppManageTriggers /
  AppSecretsRead-for-email) are required AUTHORITATIVELY IN-TX against the
  post-reparent, already-locked app — checked set == written set, no TOCTOU.
  expand_*_templates_tx are now self-contained (load hand-declared identities
  from the tx), so collisions are caught on descendants too. Blast radius now
  counts the true DB subtree. Descendant expansion errors are tagged with the
  app slug (e.g. an {env} template on a NULL-environment descendant names it).
- apply_api: the pre-tx descendant authz pass is removed in favour of the
  in-tx gate (sound via the hierarchy-aware effective_app_role).
- templates journey: out-of-tree descendant at depth 1 AND 2 receives the
  expansion and is reaped on template removal.
- doc §4.5: strike the in-apply-only scope limitation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-28 18:38:33 +02:00
parent 7c17d6e363
commit c04c684a2e
5 changed files with 392 additions and 55 deletions

View File

@@ -428,6 +428,13 @@ async fn authz_tree(
}
}
}
// NOTE (§4.5, M7): templates also fan out to descendant apps NOT declared in
// this bundle (the cross-repo subtree). Those per-recipient write caps are
// gated AUTHORITATIVELY IN-TRANSACTION in `apply_tree` Phase B2 — against the
// post-reparent app set, each app already locked — so the checked set is by
// construction the written set (no pre-tx TOCTOU, and a reparent that moves
// apps under a templated group in the same apply can't slip the gate).
Ok(())
}