You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds project-scoped policy rule authoring so section heads can author non-prod policy for their own projects without holding the global policy.edit permission. Platform retains full control over global rules + production policy + the priority band 9000+; scoped users can only CRUD their project's non-prod rules below the band.
Same pattern as EPIC Q (#99 — scoped provider connection bindings) extended to a different permission family.
Why
Today policy.edit is a single global permission. Every holder can create, edit, or delete any policy rule, affecting any project's request-to-workflow resolution. With the section-head model (EPIC L Slice 2 + EPIC N + EPIC Q), platform should let section heads author rules for their own projects without giving them the ability to change tenant-b's approval workflow.
Locked design decisions
Q1 — project_id column on policy_rules; NULL = platform-owned (admin only)
Q2 — new policy.author permission; policy.edit unchanged
Q3 — scoped rules non-prod-only by construction (DB CHECK + service)
Q4 — DB CHECK enforces env_kind=non_prod OR env_id present; service
validates env_id → env.kind = non_prod AND env_id in project
Q5 — 8 stable error codes (renamed for consistency with EPIC Q)
Q6 — policy.denied_out_of_scope audit event; NO policy_rule_id
Q7 — separate Create/Update/Delete methods (admin) vs
*ForScopedAuthor (scoped); not unified by a callerScope arg
Q8 — 6 resolver tests + 1 DB defense-in-depth test + scoped env_id
to prod env service test
Q9 — UpdateScopedPolicyInput selector: nil preserves;
explicit {} REJECTED for scoped authors
Q10 — scoped GET list shows inherited platform rules with badge +
sanitized projection (no raw selector values)
Q11 — 4 Prometheus counters (created / updated / deleted / denied)
with low-cardinality labels; NO ids on labels
Q12 — policy_scope_too_broad.reason carries 4 variants
Q13 — sidebar entry gated by policy.author; auto-route on single
project coverage
Q14 — guided form only for scoped authors; no raw JSON
Q15 — empty-state admin shortcut visible only to policy.edit holders
Q16 — extend docs/operations/policy-templates.md (NOT a new page)
Q17 — 5-slice PR breakdown (no worker slice)
Mid-pass corrections (each baked into the locked spec):
§2 — resolver must filter by policy_rules.project_id (not just authorization boundary — rule-matching boundary too). Request without project_id in scope considers ONLY platform/global rules.
§3 — empty selector {} REJECTED for scoped authors (not just preserved on update). An empty selector is effectively broad/global and could match prod.
§4 — inherited platform projection sanitized server-side. Scoped users see selector_keys + name + priority + workflow_name + enabled + is_platform_inherited=true; never raw selector values.
§4 — routes must be behind authenticated session middleware. Service runs the team-aware coverage inline; auth identity must already be in context.
§5 — policy errors in their OWN module (src/api/policyErrors.ts or src/api/projectPolicyRules.ts), NOT mixed into providerConnections.ts.
§5 — policy.edit does NOT imply policy.author (NEVER server-side, NEVER as a UI shortcut). The two permissions stay distinct everywhere.
Summary
Adds project-scoped policy rule authoring so section heads can author non-prod policy for their own projects without holding the global
policy.editpermission. Platform retains full control over global rules + production policy + the priority band 9000+; scoped users can only CRUD their project's non-prod rules below the band.Same pattern as EPIC Q (#99 — scoped provider connection bindings) extended to a different permission family.
Why
Today
policy.editis a single global permission. Every holder can create, edit, or delete any policy rule, affecting any project's request-to-workflow resolution. With the section-head model (EPIC L Slice 2 + EPIC N + EPIC Q), platform should let section heads author rules for their own projects without giving them the ability to changetenant-b's approval workflow.Locked design decisions
Mid-pass corrections (each baked into the locked spec):
policy_rules.project_id(not just authorization boundary — rule-matching boundary too). Request withoutproject_idin scope considers ONLY platform/global rules.{}REJECTED for scoped authors (not just preserved on update). An empty selector is effectively broad/global and could match prod.selector_keys+name+priority+workflow_name+enabled+is_platform_inherited=true; never raw selector values.src/api/policyErrors.tsorsrc/api/projectPolicyRules.ts), NOT mixed intoproviderConnections.ts.policy.editdoes NOT implypolicy.author(NEVER server-side, NEVER as a UI shortcut). The two permissions stay distinct everywhere.workflow_definitions.scoped_policy_authorableflag tracked as follow-up [Step 4] Scaffold Fiber API + health/ready/metrics + middleware placeholders #1.Hardcoded constants:
Scoped
policy.authorrules must use priority< 9000; platformpolicy.editrules use>= 9000for guaranteed precedence.Schema (migrations 0033 + 0034)
Sub-issues
/projects/:id/policies+ author drawer + capability helpersR-follow-ups (NOT v1 blockers)
workflow_definitions.scoped_policy_authorableflag (p2-high, scheduled soon after R)PlatformReservedPriority(p2)policy_rules.is_defaultschema column + reservedis_default_not_allowed_for_scopeerror code (p3)Stable error codes (8)
Audit events
policy.create/policy.update/policy.delete— selector KEYS only, NEVER valuespolicy.denied_out_of_scope— does NOT includepolicy_rule_id(gate-order protection)Observability
4 Prometheus counters with the LOW-CARDINALITY LOCK (no ids on labels):
scope ∈ {platform, project}.reasonfixed 8-element closed set.Resolver test plan
6 tests pinned for R1:
Plus the service-level test: scoped author cannot create a selector with
environment_idpointing to a prod env.Dependencies
Definition of done