feat(hierarchies): per-env approval-policy gating (M5)
A project's root manifest can mark environments confirm-required; applying to
one needs an explicit `pic apply --dir --env <e> --approve <e>` (a blanket
`--yes` does NOT cover it), the act is admin-gated and audited, and the policy
folds into the bound-plan token. The last unbuilt piece of the project-tool
track (§4.2/§6).
- manifest: `[project]` block → `ManifestProject { environments[{name,confirm}] }`,
valid only on the tree's ROOT manifest (rejected elsewhere by build_tree).
- discover: build_tree emits `project` into the bundle from the root manifest.
- apply_service: `TreeBundle.project` + `ProjectPolicy`; policy folds into the
state_token (a policy edit between plan and apply trips StateMoved); plan
surfaces `approvals_required`; new `ApplyError::ApprovalRequired` → 409.
- apply_api: `enforce_env_approval` (after authz_tree) — refuses a gated env
not in `approved_envs`, and on approval requires admin (AppAdmin/GroupAdmin)
on every declared node + audits. The server re-derives the policy from the
bundle (CLI check is convenience; server is authoritative).
- CLI: `--approve <env>` (repeatable); `resolve_approvals` refuses a gated env
non-interactively, prompts on a TTY (retype the env name); plan renders gated
envs. Single-node `apply --file --env <e>` REFUSES a confirm-required env
(can't carry the admin-gated approval) and directs to `--dir` — closing the
bypass found in review rather than silently skipping the gate.
- approval journey + manifest/ProjectPolicy unit tests. No migration (policy
lives in the manifest, like takeover/blast-radius).
- doc §11 Phase 5 + §12: approval gating shipped; gate is a `--dir` feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1064,9 +1064,18 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
||||
> directory tree of `picloud.toml` manifests (each declaring an `[app]` or `[group]` node) applies as
|
||||
> ONE server-computed plan in ONE Postgres transaction. **Declarative group create/reparent shipped
|
||||
> (M2, 2026-06-26)** — a manifest now owns the tree *shape*, not just node content. **Multi-repo
|
||||
> single-owner / takeover + structural prune shipped (§7, M3, 2026-06-26)** — see §7 below. Still
|
||||
> deferred: **per-env approval-policy gating** (`[project.environments]`); env overlays
|
||||
> (`picloud.<env>.toml`) already exist.
|
||||
> single-owner / takeover + structural prune shipped (§7, M3, 2026-06-26)** — see §7 below.
|
||||
> **Per-env approval-policy gating shipped (`[project.environments]`, M5, 2026-06-28)** — the root
|
||||
> manifest declares which environments are confirm-required; `pic apply --dir --env <e>` to such an
|
||||
> env needs an explicit `--approve <e>` (a blanket `--yes` does NOT cover it), the approval is
|
||||
> admin-gated (the actor needs admin on every declared node, not just write) and audited, and the
|
||||
> policy folds into the bound-plan token. The server re-derives the policy from the bundle — the CLI's
|
||||
> check is convenience, the server is authoritative. Env overlays (`picloud.<env>.toml`) already exist.
|
||||
> Gating is a **`--dir` (project-tree) feature**: single-node `pic apply --file --env <e>` cannot
|
||||
> carry the admin-gated approval, so it **refuses** a confirm-required env and directs the user to
|
||||
> `--dir` (rather than silently bypassing the gate). Like `--takeover`/blast-radius, the policy lives
|
||||
> in the manifest (not persisted server-side), so whoever controls desired state can weaken it — an
|
||||
> accepted trust boundary, not a privilege bug.
|
||||
>
|
||||
> Shipped surface:
|
||||
> - **Engine:** the reconcile engine generalized from app-only to an `ApplyOwner { App | Group }`
|
||||
@@ -1131,3 +1140,7 @@ to take, not yet taken:
|
||||
§11.3. The remaining contract work here is only for script-body inheritance, Phase 4.)*
|
||||
- The **full manifest schema** spelling every block (scripts, routes, the 8 trigger kinds, storage
|
||||
config, env-scoped vars, secret-refs, domains, `[project.environments]` + confirm policy).
|
||||
> `[project]` (M5, root manifest only) shipped: `[[project.environments]]` entries carry
|
||||
> `name` + `confirm` (default `false`). A `confirm = true` env is gated — `pic apply --dir --env <name>`
|
||||
> requires `--approve <name>` (admin-gated + audited; `--yes` is insufficient). The block is rejected on
|
||||
> any non-root manifest. Remaining unspecified: storage config, domains, the full secret-ref shape.
|
||||
|
||||
Reference in New Issue
Block a user