diff --git a/crates/manager-core/migrations/0059_sealed_templates.sql b/crates/manager-core/migrations/0059_sealed_templates.sql new file mode 100644 index 0000000..1a062c8 --- /dev/null +++ b/crates/manager-core/migrations/0059_sealed_templates.sql @@ -0,0 +1,19 @@ +-- ยง11 tail (v1.2 Hierarchies): `sealed` (mandatory) group templates. +-- +-- Per-app opt-out (0058_template_suppressions) made an inherited group TRIGGER +-- or ROUTE template advisory-by-default: it runs on a descendant *unless* that +-- descendant declares a `[suppress]` declining it. That is a compliance footgun +-- โ€” a group's audit / security / rate-limit template can be silently opted out +-- of by the very tenant it is meant to police. +-- +-- A `sealed` template closes the gap: the two suppression filters skip a sealed +-- row, so it fires / serves on every descendant regardless of any suppression. +-- The column is only meaningful on a group-owned template (`group_id IS NOT +-- NULL`); an app-owned row is never inherited, so sealing it is meaningless and +-- rejected at apply. Existing rows default to unsealed (the prior behaviour). +-- +-- * triggers.sealed โ€” a sealed group trigger template is never suppressible. +-- * routes.sealed โ€” a sealed group route template is never suppressible. + +ALTER TABLE triggers ADD COLUMN sealed BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE routes ADD COLUMN sealed BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/manager-core/tests/expected_schema.txt b/crates/manager-core/tests/expected_schema.txt index 0d1a31e..64a16da 100644 --- a/crates/manager-core/tests/expected_schema.txt +++ b/crates/manager-core/tests/expected_schema.txt @@ -342,6 +342,7 @@ table: routes dispatch_mode: text NOT NULL default='sync'::text enabled: boolean NOT NULL default=true group_id: uuid NULL + sealed: boolean NOT NULL default=false table: script_imports app_id: uuid NOT NULL @@ -406,6 +407,7 @@ table: triggers updated_at: timestamp with time zone NOT NULL default=now() name: text NOT NULL default=(gen_random_uuid())::text group_id: uuid NULL + sealed: boolean NOT NULL default=false table: vars id: uuid NOT NULL default=gen_random_uuid() @@ -931,3 +933,4 @@ constraints on vars: 0056: group triggers 0057: group routes 0058: template suppressions + 0059: sealed templates