docs(design): record Phase 3 shipped + the live-resolution decision
Phase 3 (group-inherited config) is implemented, so fold the outcome back into the design doc (per its own "before it drifts" note) — and flag the one deliberate divergence from the plan. §5.1 / §11.3 / §12 prescribed a *materialized* per-app effective view with a generation counter and fan-out invalidation. We shipped **live, resolve-on-read inheritance** instead: one recursive CTE per `vars::get`/`secrets::get`/`config/effective`, no cache. The org tree is small and there was no pre-existing config cache to fight, so this is cheap and sidesteps §10's unsolved invalidation SLA. Records, mirroring the Phase 1/2 status blocks: * a "Status (Phase 3): ✅ shipped" block in §11.3 with the full surface (0048/0049 schema incl. `apps.environment`, the §3 resolver, vars SDK + CRUD, group-secret AAD + inherited read, the two secret gates, masked `config/effective`, `--explain`) and the live-vs-materialized rationale; * §5.1 runtime-model bullet annotated as the deferred (Phase-4 script inheritance) design, not Phase-3 runtime; * §10 "invalidation SLA" and "inherited-membership revocation lag" marked moot/resolved — no cache means immediate propagation; * §12 resolver contract + the header framing updated to live resolution. Doc-only; no code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,8 +10,10 @@
|
|||||||
> without duplication.
|
> without duplication.
|
||||||
>
|
>
|
||||||
> **Blueprint impact:** this reverses the §11.5 *snapshot-copy, not live-link* stance — top-down
|
> **Blueprint impact:** this reverses the §11.5 *snapshot-copy, not live-link* stance — top-down
|
||||||
> hierarchical inheritance (group → app) is now in scope, implemented as a *materialized, auto-
|
> hierarchical inheritance (group → app) is now in scope. *Originally* designed as a *materialized,
|
||||||
> invalidated* view (§5.1). Fold the outcome back into the blueprint before it drifts.
|
> auto-invalidated* view (§5.1); **Phase 3 shipped it as live, resolve-on-read inheritance** (one
|
||||||
|
> recursive CTE per call, no cache), with materialization deferred to a future optimization (see the
|
||||||
|
> Phase 3 status note in §11.3). Fold the outcome back into the blueprint before it drifts.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -425,6 +427,15 @@ Two distinct constraints:
|
|||||||
- **Runtime model: a materialized effective view + versioned cache (not per-request live-resolve).**
|
- **Runtime model: a materialized effective view + versioned cache (not per-request live-resolve).**
|
||||||
An earlier draft said "live-resolve," which is underspecified and would fight the existing cache.
|
An earlier draft said "live-resolve," which is underspecified and would fight the existing cache.
|
||||||
The real model:
|
The real model:
|
||||||
|
|
||||||
|
> **⚠️ Superseded for config by what Phase 3 actually shipped (§11.3): live resolution.** For
|
||||||
|
> **vars + secrets**, PiCloud resolves on-demand via one recursive CTE per call — *no* materialized
|
||||||
|
> view, generation counter, or invalidation fan-out. There was no pre-existing config cache to
|
||||||
|
> "fight" (config is net-new, §3), and the org tree is small, so resolve-on-read is cheap and
|
||||||
|
> sidesteps the invalidation SLA below. The materialized model in this bullet stands only as the
|
||||||
|
> **deferred plan for *script-body* inheritance (Phase 4)** — where a hot per-request path may
|
||||||
|
> justify a cache — and as a future optimization for config if a deep tree ever demands it. Read the
|
||||||
|
> rest of §5.1 as "the materialization design, if/when we need it," not as Phase 3's runtime.
|
||||||
- manager-core **resolves-at-write into a materialized per-app effective view** (§3 rule applied:
|
- manager-core **resolves-at-write into a materialized per-app effective view** (§3 rule applied:
|
||||||
sparse merge, env filter, proximity, CoW, `enabled`).
|
sparse merge, env filter, proximity, CoW, `enabled`).
|
||||||
- The orchestrator/executor serve from that view, **keyed by `app_id` + a generation/version**. The
|
- The orchestrator/executor serve from that view, **keyed by `app_id` + a generation/version**. The
|
||||||
@@ -800,7 +811,10 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
|||||||
|
|
||||||
- **Effective-view invalidation SLA (§5.1)** — the security-staleness guarantee at fan-out to many
|
- **Effective-view invalidation SLA (§5.1)** — the security-staleness guarantee at fan-out to many
|
||||||
descendants is unsolved; synchronous-for-security + eventual-elsewhere is the proposed shape, not a
|
descendants is unsolved; synchronous-for-security + eventual-elsewhere is the proposed shape, not a
|
||||||
proven one. Highest-risk open item.
|
proven one. *(Was the highest-risk open item.)* **Moot for config as of Phase 3:** vars/secrets ship
|
||||||
|
with **live resolution** (no cache → no staleness window; a disable/rotation is visible on the next
|
||||||
|
call). The SLA only re-arises if **script-body** materialization is built (Phase 4) or if config is
|
||||||
|
ever materialized for performance; until then there is nothing to invalidate.
|
||||||
- **Conflict bit vs. operational lock (§4.2)** — *decided:* phase 1 ships the `enabled` / secret-
|
- **Conflict bit vs. operational lock (§4.2)** — *decided:* phase 1 ships the `enabled` / secret-
|
||||||
reference **conflict bit**; the operational-lock flag is the documented fallback if the bit proves
|
reference **conflict bit**; the operational-lock flag is the documented fallback if the bit proves
|
||||||
too heavy. (Was listed as undecided; resolved here to match §11 phase 1.)
|
too heavy. (Was listed as undecided; resolved here to match §11 phase 1.)
|
||||||
@@ -810,8 +824,11 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
|||||||
precondition to adopting the rule.
|
precondition to adopting the rule.
|
||||||
- **Inherited-membership revocation lag (§5.3)** — revoking a group admin must drop implicit admin on
|
- **Inherited-membership revocation lag (§5.3)** — revoking a group admin must drop implicit admin on
|
||||||
every descendant app, but §5.1's synchronous-invalidation subset covers only `enabled`/secrets, not
|
every descendant app, but §5.1's synchronous-invalidation subset covers only `enabled`/secrets, not
|
||||||
**role revocation** — leaving an unbounded window. New residual risk; should get the same
|
**role revocation** — leaving an unbounded window. ~~New residual risk; should get the same
|
||||||
synchronous-for-security bound, lands with phase 3's authz.
|
synchronous-for-security bound, lands with phase 3's authz.~~ **Resolved — never materialized:**
|
||||||
|
Phase 2 RBAC and Phase 3 config both resolve roles/config **live** (no inherited-role or
|
||||||
|
effective-view cache), so a revoked group grant is gone on the next request. The lag only exists if a
|
||||||
|
cache is introduced.
|
||||||
- **External execution cancel (§4.2 kill-switch)** — the executor has **no external-cancel path
|
- **External execution cancel (§4.2 kill-switch)** — the executor has **no external-cancel path
|
||||||
today** (`spawn_blocking` + self-checked deadline, verified); the kill-switch is a net-new capability
|
today** (`spawn_blocking` + self-checked deadline, verified); the kill-switch is a net-new capability
|
||||||
(a cancel flag polled in `on_progress`), deferred past phase 1. Until it exists, the strongest stop
|
(a cancel flag polled in `on_progress`), deferred past phase 1. Until it exists, the strongest stop
|
||||||
@@ -879,6 +896,46 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
|||||||
tables + polymorphic owner; the group-secret AAD scheme (§5.3 caveat); masked group secrets; the
|
tables + polymorphic owner; the group-secret AAD scheme (§5.3 caveat); masked group secrets; the
|
||||||
effective-view resolver + materialization + invalidation; **`config --effective --explain`** (hard
|
effective-view resolver + materialization + invalidation; **`config --effective --explain`** (hard
|
||||||
requirement, since multi-level resolution first ships here).
|
requirement, since multi-level resolution first ships here).
|
||||||
|
|
||||||
|
> **Status (Phase 3): ✅ shipped — with one deliberate architecture change: LIVE resolution, not
|
||||||
|
> materialization.** §5.1 / this bullet / §12 prescribed a *materialized* per-app effective view
|
||||||
|
> with a generation counter and fan-out invalidation. We shipped **live, on-demand resolution
|
||||||
|
> instead**: one depth-bounded recursive CTE (`config_resolver::CHAIN_LEVELS_CTE`, walking
|
||||||
|
> `apps.group_id → groups.parent_id → root`) runs per `vars::get`/`secrets::get`/`config/effective`,
|
||||||
|
> and the §3 merge/proximity/tombstone semantics run in a pure, unit-tested `resolve()` pass on the
|
||||||
|
> fetched rows. No materialized store, no generation, no invalidation protocol. Rationale: the group
|
||||||
|
> tree is small org structure, resolve-on-read is cheap, and it sidesteps §10's unsolved
|
||||||
|
> fan-out-invalidation SLA entirely. **Consequence:** §10's "effective-view invalidation SLA" and
|
||||||
|
> "inherited-membership revocation lag" are **moot for config** — with no cache, a security disable
|
||||||
|
> or secret rotation propagates immediately (same posture as Phase 2's live RBAC). Materialization
|
||||||
|
> (and a per-execution memo keyed by `cx.execution_id`) is re-scoped to a **future optimization**,
|
||||||
|
> to revisit only if a deep tree + hot path ever makes live resolution measurably costly.
|
||||||
|
>
|
||||||
|
> Shipped surface:
|
||||||
|
> - **Schema:** `0048_vars.sql` (`vars` polymorphic owner [`group_id` XOR `app_id`],
|
||||||
|
> `environment_scope`, `is_tombstone`, partial-unique indexes) + **`apps.environment TEXT NULL`**
|
||||||
|
> (the explicit realization of "an environment is an app", §2 — set at app-create, read by the
|
||||||
|
> resolver, never entering `SdkCallCx`). `0049_group_secrets.sql` reshapes the live `secrets`
|
||||||
|
> table to the **same** polymorphic-owner + `environment_scope` contract (PK → two partial-unique
|
||||||
|
> indexes); existing app rows backfill to `app_id` + scope `'*'` and decrypt byte-for-byte (the v1
|
||||||
|
> AAD excludes the scope).
|
||||||
|
> - **§3 resolver:** env-filter in SQL (`scope IN ('*', app_env)`), then proximity-first + per-key
|
||||||
|
> deep-merge / replace / tombstone in Rust. A same-level `@E` value **suppresses** the same-level
|
||||||
|
> `*` (env is eligibility, not a merge tier — §3.1), incl. the map-vs-map case.
|
||||||
|
> - **Vars:** `VarsService` + `vars::get`/`vars::all` SDK + `App/GroupVars{Read,Write}` caps +
|
||||||
|
> `{apps,groups}/{id}/vars` admin CRUD + `pic vars`.
|
||||||
|
> - **Group secrets:** owner-discriminated AAD (`secret:group:{group_id}:{name}`; app AAD
|
||||||
|
> unchanged); inherited `secrets::get` (resolves app-own + ancestor-group, decrypts under the
|
||||||
|
> resolved owner's AAD, anchored to `cx.app_id`); the **two orthogonal gates** (§5.3): runtime
|
||||||
|
> injection (no human-read check) vs. the group-gated value endpoint
|
||||||
|
> `GET /groups/{id}/secrets/{name}/value` (`GroupSecretsRead` = group_admin). `GroupSecretsWrite`
|
||||||
|
> sits at `script:write` scope (matches its editor role tier).
|
||||||
|
> - **Effective view (masked):** `GET /apps/{id}/config/effective` → resolved vars (with
|
||||||
|
> provenance) + secrets masked to `{status, owner, scope}` (value never returned). `pic config
|
||||||
|
> --effective [--explain]` renders it.
|
||||||
|
>
|
||||||
|
> Deferred (unchanged): group-*owned scripts/modules* (Phase 4), the manifest `[vars]`/`[secrets]`
|
||||||
|
> apply reconciliation (Phase 5 — Phase 3 ships CRUD API + CLI), and materialization (above).
|
||||||
4. **Group-inherited scripts/modules.** CoW overrides; the **scope-aware module/import resolver +
|
4. **Group-inherited scripts/modules.** CoW overrides; the **scope-aware module/import resolver +
|
||||||
extension points** (§5.5); cache-invalidation fan-out hardening; versioning/pinning if needed.
|
extension points** (§5.5); cache-invalidation fan-out hardening; versioning/pinning if needed.
|
||||||
5. **Project tool maps onto groups.** Nested manifests, attach point, single-owner, server-computed
|
5. **Project tool maps onto groups.** Nested manifests, attach point, single-owner, server-computed
|
||||||
@@ -893,7 +950,9 @@ Resolved items now live inline next to their topic. What genuinely remains:
|
|||||||
|
|
||||||
- The **apply bundle / plan artifact / change-report** wire contract (what the CLI ships, what the
|
- The **apply bundle / plan artifact / change-report** wire contract (what the CLI ships, what the
|
||||||
server persists and returns), including the conflict and blast-radius shapes.
|
server persists and returns), including the conflict and blast-radius shapes.
|
||||||
- The **effective-view resolver** (the read primitive) — the §3 rule made executable, plus the
|
- The **effective-view resolver** (the read primitive) — the §3 rule made executable. *(Phase 3:
|
||||||
materialization + invalidation protocol (§5.1).
|
shipped as a **live** recursive-CTE resolver, `config_resolver`/`secrets_repo::resolve`; the
|
||||||
|
materialization + invalidation protocol of §5.1 was **not** built — see the Phase 3 status note in
|
||||||
|
§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
|
- 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).
|
config, env-scoped vars, secret-refs, domains, `[project.environments]` + confirm policy).
|
||||||
|
|||||||
Reference in New Issue
Block a user