Remediate the HIGH and security-relevant findings from the 2026-07-11 audit.
H1 — the per-env approval gate is now server-authoritative. The governing
project is resolved from the target node's nearest-claimed ancestor
(`governing_env_policy`/`_tree` + `governing_project_id` +
`ProjectRepository::get_environments_by_id`), independent of the client-supplied
`[project]`. Omitting or spoofing the project block can no longer skip a gate the
owning project established; a to-create group resolves its declared parent's
chain so a fresh subtree node inherits the gate. Fails closed on any read error.
H2 — the API-key prefix slice (`&rest[..8]`) is now the boundary-safe
`rest.get(..8)`, so an attacker-supplied multibyte bearer can't panic the request
task (unauthenticated per-request DoS). Regression test added.
C1 — admin sessions gain an absolute lifetime cap (migration 0070,
`PICLOUD_SESSION_ABSOLUTE_TTL_HOURS`, default 30d): `lookup` filters it, `touch`
clamps the sliding bump to it, so a continuously-used or stolen-but-warm token
self-expires. Mirrors the data-plane app-user cap.
C2 — `Cache-Control: no-store` on the login and API-key-mint responses (the two
that return a raw credential), so a proxy/CDN/browser cache can't retain it.
B8 — file downloads are header-safe: `sanitize_stored_filename` guarantees a
valid `HeaderValue` (no panic on a control-char name) and BOTH the per-app and
group download paths now set attachment + `X-Content-Type-Options: nosniff` +
a restrictive CSP, closing a group-path stored-XSS gap.
Also folds in the server-side plan-warning plumbing (`plan_warnings`,
`PlanResult::warnings`) and the `app_only_reject` message helper that the CLI
plan-preview change builds on, plus operator security notes (reads-open shared-
topic SSE; the `--env` label is advisory, not a boundary).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Shared TOPICS fanned out only to in-cluster trigger handlers; external clients
could not subscribe (per-app topics already can). Add SSE for shared topics.
- RealtimeBroadcaster gains a parallel (group_id, topic) channel map:
subscribe_group / publish_group / drop_group_topic (default no-ops so
NoopRealtimeBroadcaster + test doubles are untouched). InProcessBroadcaster
implements them with a second map; GC + channel_count span both.
- Route GET /realtime/shared/topics/{topic}: Host->app dispatch (as the per-app
route), then RealtimeAuthority::authorize_subscribe_shared resolves the OWNING
GROUP from the app's chain (kind=topic, root segment). Reads-open model — the
resolution IS the authorization, consistent with in-script shared reads; a
foreign-subtree app never resolves (404, the isolation boundary). No principal
machinery needed.
- GroupPubsubServiceImpl::with_realtime bridges a shared-topic publish to the
owning-group channel (best-effort) after the durable trigger fan-out.
- Host wires the broadcaster into the group pubsub service + the collection
resolver into the authority.
Auth-model note: chose reads-open (subtree app's Host is the grant) over
"authenticated principal + GroupKvRead" — it's both simpler and faithful to how
shared-collection reads already work. Pinned by realtime broadcaster group-map
tests, realtime_api shared-route tests (404 + stream), and
group_pubsub_service::publish_bridges_to_the_group_broadcaster. No migration.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-env approval policy was applier-supplied — a hand-crafted request
that omitted `project.environments` was ungated, and flipping a gate to
`confirm = false` in the same request un-gated it. Persist the policy
server-side and enforce against `persisted ∪ declared`.
- Migration 0067: `project_environments (project_id, env_name, confirm)`,
CASCADE on the project. Written declaratively (delete-then-insert) inside
`upsert_project_tx`, same tx as the project row + node claim.
- `ProjectRepository::get_environments_by_slug` (read side) +
`ApplyService::effective_env_policy` union the persisted policy with the
request's declared one (confirm if EITHER says so — monotonic).
- `env_gate_check` now evaluates the effective policy; the three handlers load
it before the gate. `plan.approvals_required` is the effective gated set, so
CI sees a persisted gate even when the manifest omits it.
- The union rule closes both bypasses: an omitted policy still trips a
persisted gate, and an ungating apply must itself pass the gate (the
declarative replace then takes effect next time) — TOCTOU-safe.
Pinned by env_approval::{persisted_policy_gates_a_request_that_omits_it,
flipping_a_gate_to_false_in_the_same_request_still_gates} +
projects_repo::get_environments_by_slug_returns_persisted_policy. Schema golden
reblessed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-env approval gate (`[project.environments]`, `confirm = true`) was
client-side only — the policy was `#[serde(skip_serializing)]`, so a patched or
older CLI applying to a confirm-required env bypassed it entirely. This makes
the gate server-enforced, admin-gated, and audited (the deferred §4.2 piece).
- Wire: `ProjectDecl` gains `environments` (the CLI stops skip_serializing it and
sends the selected `env` + the `--approve` set on the apply request). The
server RE-DERIVES the gate from the request (`env_gate_check`): a
confirm-required env not in `approved_envs` → 409 `ApprovalRequired`.
- Admin gate: an approved override additionally requires `AppAdmin`/`GroupAdmin`
on EVERY declared node — a step up from the editor write caps an ordinary
apply needs (reusing the admin caps per §4.2) — and emits a `tracing::info!`
audit line. Enforced on all three handlers (single app, single group, tree);
the check runs before any tx, so a denial (403) precedes any mutation.
- Token: the env policy folds into the TREE bound-plan token (a policy edit
between plan and apply trips StateMoved). The single-node token is left
unchanged (a bare live-state hash), so plan/apply still match without threading
a project into it.
- CLI: single-node `apply`/`plan` now resolve the GOVERNING project (own block
else nearest ancestor's) so a leaf apply carries the root's policy; `plan`
surfaces `approvals_required`. Client-side `require_env_approval` fast-fail
kept as UX.
Adapts the earlier feat/ownership-and-approval M5 (654e387) to main's DTOs; main
addresses tree group nodes by slug only, so the admin loop mirrors authz_tree
(app: resolve_app_id fail-closed; group: get_by_slug, skip to-create).
Threat-model note (honest scope, in the doc + code comments): this closes the
patched/older-CLI bypass and admin-gates + audits the override, but is NOT a
hermetic boundary against a hand-crafted request that OMITS the policy — the
policy is applier-supplied, not persisted server state. Full closure (persist a
`project_environments` source of truth + a server-determined env) is deferred.
Review (adversarial pass): fixed a MEDIUM (single-node `plan` didn't resolve the
governing project, so a leaf plan under-reported the gate `apply` enforces) and a
LOW (duplicate clippy attr); corrected overstated "can't bypass" wording.
Tests: ProjectDecl gate unit test; env_approval journeys
`server_enforces_the_gate_against_a_non_stock_client` (raw request → 409 without
approval, admin-approved → 200) and `approving_a_gated_apply_requires_admin`
(editor + --approve → 403). 417 lib tests + 33 CLI journeys + workspace clippy
-D warnings + fmt clean. No migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review #4 (LOW, cleanup). `reconcile_group_structure_tx` carried a
`#[allow(clippy::too_many_lines)]` over a ~145-line body doing create +
reparent + attach/RBAC/lock in one loop. Extract the create branch
(`create_group_node_tx`) and the divergence-resolution branch
(`reparent_diverged_group_tx`) into helper methods, threading the shared
attach-ceiling / principal / mode context through a small `ReconcileCtx` — the
loop is now thin enough to drop the allow. Also improve the child-before-parent
error in `resolve_declared_parent` to hint that group nodes must be ordered
parents-first (only a hand-rolled bundle hits it; the CLI already sorts).
No behavior change. Documents the Tier-1 review follow-ups in the design doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`tests/structural_divergence.rs`: a repo that nests a group under a different
parent than the server → `pic plan` shows it `diverged`; a bare apply is
refused (422); `--force-local-structure` reparents to the manifest shape;
`--adopt-server-structure` keeps the server placement. Design doc §6 records M2
shipped, leaving only per-env approval gating deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`tests/group_create.rs`: a nested `[group]` tree with no server groups yet →
`pic apply --dir` creates both (parent from directory nesting), claims them for
the `[project]`, and re-applies as a no-op; a member with only editor (no
group-admin) is refused and leaves nothing behind. Design doc §6 records M1
shipped (parent-by-nesting, Phase-0 create-in-tx, claim, RBAC + attach ceiling)
with reparent/divergence still deferred to M2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The apply_ownership journey gains an attach-ceiling case: a group node below
the attach point applies; the attach point itself and a sibling subtree are
both refused (422, message names the attach point). Design doc §7 + CLAUDE.md
record M2 shipped and re-point 'Next' at M3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end coverage of the M1 ownership claim through the real CLI + server:
claim-on-first-apply + owner column, idempotent re-apply by the owner, a
second project refused (409, naming the owner), --takeover by an admin,
app-inheritance from the nearest claimed ancestor (owning project ok; foreign
or absent project refused), and the capability gate — a member with only an
editor GROUP role can reconcile but is refused --takeover (needs group-admin),
with ownership unchanged after the failed takeover.
- tests/apply_ownership.rs (registered in cli.rs); a grant_group_membership
helper in tests/common/member.rs (group-level, mirroring the app one).
- design doc §7 gains an M1-shipped status note; CLAUDE.md records §7 M1 and
re-points 'Next' at M2 (attach ceiling) + M3 (blast-radius / pic projects ls).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLI journey (shared_queues): authoring + `pic triggers ls --group` shared
column + the two validation rejections (undeclared queue collection; shared
on an app), mirroring the shared_topics/shared_triggers norm; the live
competing-consumer + dispatcher behaviour is pinned by the deterministic
manager-core tests. Docs: CLAUDE.md + design doc §11.6 record D3 as shipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deterministic manager-core test (tests/shared_topics.rs) proves the
namespace boundary both ways: a shared publish fans out only to the
group's shared pubsub trigger (not a per-app or non-shared group
template), and a per-app publish never hits the shared trigger. CLI
journey (shared_topics) covers authoring + `pic triggers ls --group`
shared column + the two validation rejections (undeclared topic; shared
on an app), mirroring the shared_triggers norm. Docs: CLAUDE.md + design
doc §11.6 record D1 + D2 as shipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update the design doc §4.5 and CLAUDE.md current-focus: email joins
cron+queue as a materialized stateful template via the shared-group-secret
model; drop the email deferral and the stale "email rejected on a group"
note; retarget "Next" to multi-node / multi-repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 0063: partial unique index on (app_id, materialized_from) + ON CONFLICT DO
NOTHING in the reconciler, so a materialized copy is idempotent under
concurrent reconciles (two parallel app-creates no longer duplicate a copy).
- stateful_templates journey: a group cron template + a descendant app → a
materialized cron copy in `pic triggers ls --app`; re-apply is a NoOp.
- docs (§4.5 + CLAUDE.md): cron+queue materialization implemented; group EMAIL
templates deferred (per-descendant inbound-secret reseal needs the master key
threaded through the hooks + a secret-ref schema) and cleanly rejected at
apply for now, alongside a `materialized` ls column.
Completes the v1.2 near-term batch (M1-M5, cron+queue); group email is the one
scoped follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `shared` column in `pic triggers ls --group` (TriggerTemplateInfo + DTO +
report + renderer).
- manager-core/tests/shared_triggers.rs: a shared write matches the group's
shared trigger and NOT a same-named per-app trigger; a per-app write matches
the app trigger and NOT the shared one (the `shared` flag is the boundary).
- shared_triggers journey: declarative authoring applies, ls --group shows
shared, an undeclared-collection shared trigger + an app shared trigger are
rejected.
- docs: §11.6 + CLAUDE.md move shared-collection triggers from Deferred to
implemented.
Completes M2. (Pre-existing async-dispatcher timing flakes pass in isolation.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- dangling_suppress_warnings takes an ApplyOwner and walks the group's own
chain (GROUP_CHAIN_LEVELS_CTE) for a group node; runs for both owners.
- GET /groups/{id}/suppressions + `pic suppress ls --group` (client + cmd +
main.rs; --app/--group mutually exclusive).
- suppress journey: a child group declines a parent template for its subtree;
suppress ls --group shows the marker. Docs §4.5 + CLAUDE.md move group-level
suppression from Deferred to implemented.
Completes M1. (An unrelated pre-existing email_inbound dispatch-timing flake
passes in isolation.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tests/sealed.rs: a group declares a sealed [[routes]] template; a descendant
that suppresses it still serves the path, apply warns "sealed — no effect",
routes ls --group shows sealed=true, and sealing an [app] route is rejected.
- docs: §4.5 trust-model callout + CLAUDE.md move `sealed` from Deferred to
implemented — group templates are advisory-by-default *unless* sealed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review findings.
1. Per-app opt-out changes the group-template guarantee from "runs on every
descendant" to "runs unless the descendant declines" — a footgun for
compliance hooks (an audit/security trigger a tenant can silently opt out
of). There is no non-suppressible flag. Document this in design §4.5 +
CLAUDE.md, and record the deferred mitigation: a `sealed`/`mandatory`
group-template marker the trigger anti-join + route filter skip (cheap —
both filters are centralized).
2. The dangling-suppress warning path had no coverage. The suppress journey
now applies a `[suppress] triggers=["does-not-exist"]`, asserts the apply
still succeeds and the report warns "no effect".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Journey tests/suppress.rs: a group declares a `[[routes]]` template; a
descendant app applies `[suppress] routes=["/ghello"]` → stops serving it
(via `routes match`), a sibling still serves it, `pic suppress ls --app`
shows it, re-apply is a NoOp; pruning the block re-inherits. The
trigger-side filter + isolation are pinned at the repo layer by
template_suppression.rs.
- Docs: design §4.5 (per-app opt-out closing the deferred gap for both
templates — coarse-by-reference, inheritance-only, the two consumption
points; group-level suppress still deferred), CLAUDE.md current-focus.
Full journey suite 121/121; workspace tests 34 suites/0 failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two review findings.
1. After R3 every production rebuild path uses `compile_effective_routes`;
`compile_routes` (the old app-only compile) was called only by its own
two unit tests, and its "runs in build_app" doc was stale — so the
lenient-skip + disabled-drop regression guards no longer covered the
live path. Remove `compile_routes` + its re-export, fold the lenient-skip
rationale onto `compile_one`, and port both guards to
`compile_effective_routes` (app-only rows are just depth-0 effective
rows). Add a third guard pinning nearest-owner shadowing at the pure-
compile layer. Fix the stale `compile_routes` mention in apply_service.
2. Document the disabled + inherited semantic: the `enabled` filter runs
before the shadow check, so a disabled own-route does not claim its
binding and an enabled ancestor template at the same path falls through
("disabled = absent", §4.3). Recorded on `compile_effective_routes` and
in design §4.5 — the corollary (can't 404 an inherited route by
disabling a same-path own route) points at the deferred per-app opt-out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Visibility + end-to-end coverage + docs for group route templates.
- ApplyService::route_report(Group) → GET /api/v1/admin/groups/{id}/routes
(viewer-tier GroupScriptsRead), surfacing a group's own route templates
(method, host, path, handler script, dispatch, enabled).
- CLI: `pic routes ls --group <g>` (the script-id positional now conflicts
with --group), client `group_routes_list` + RouteTemplateDto.
- Journey `group_routes.rs`: a group declares a `[[routes]]` template
binding a group handler; a DESCENDANT app serves it (via `routes match`
against the live RouteTable), a SIBLING-subtree app does not, `routes ls
--group` shows it, re-apply is a NoOp. Shadowing + isolation are
additionally pinned at the repo layer by group_route_templates.rs.
- Update the manifest unit test: a [group] now accepts route templates and
rejects only the stateful trigger kinds.
- Docs: design §4.5 (the live route-template model + full-live
invalidation + nearest-wins shadowing + deferrals), CLAUDE.md focus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- apply_service: trigger_report(group) → TriggerTemplateInfo
(kind/target/script/enabled); resolve_inherited_targets_for(Group) now
surfaces the group's OWN endpoint scripts so a template's handler
validates (fixes "binds to unknown script" when the handler is a
pre-existing group script, not declared in the same manifest).
- apply_api: GET /groups/{id}/triggers (GroupScriptsRead).
- CLI: `pic triggers ls --group <g>` (--app/--group mutually exclusive)
+ the client method + DTO.
- tests/group_trigger_templates.rs (manager-core, live DB): the chain
union matches a descendant app's kv insert against the group template
and NOT a sibling subtree — the isolation boundary, deterministic.
- tests/group_triggers.rs (journey): apply a kv template, ls --group
shows it, re-apply NoOp, cron-on-group rejected.
- docs: design §4.5 (live-event-kinds decision + deferrals), CLAUDE.md.
Full journey suite 119/119; workspace tests 0 failures; clippy -D clean;
schema unchanged (blessed in T1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- collections.rs: shared_files_collection_is_read_write_across_the_subtree
— a group declares kv+docs+files in one string-or-table manifest; authed
app A creates a blob via files::shared_collection("assets").create(...),
app B lists + gets the SAME bytes back across the subtree, a
sibling-subtree app gets CollectionNotShared, collections ls shows all
three kinds. Live-verified the bytes land under
files/groups/<group_id>/assets/<id[0:2]>/<id>.
- .gitignore: ignore /crates/picloud-cli/data (the CLI journey harness
spawns the server from that dir; the files journey is the first CLI test
to write blobs).
- docs: design §11.6 (KV+DOCS+FILES shipped; topics/queue still deferred),
sdk-shape (files::shared_collection), CLAUDE.md current-focus.
Full journey suite 117/117; schema blessed (55 migrations); workspace
clippy -D clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
End-to-end docs journey: a group declares a kv AND a docs collection via the
string-or-table manifest; an authenticated app A docs::shared_collection(
"articles").create(#{...}), app B finds it back across the subtree (exercising
the shared find DSL); a sibling-subtree app gets CollectionNotShared;
collections ls shows both kinds; re-apply NoOp. Full journey suite 116/116.
Docs: groups-and-project-tool §11.6 (KV+docs slices shipped; files/topics/queue
deferred), sdk-shape.md (docs::shared_collection + string-or-table), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finish the §11.6 KV slice: declarative authoring, read-only inspection, the
runtime journey, and docs — plus a forced SDK-name fix.
- SDK name: `shared` is a Rhai RESERVED keyword, so `kv::shared(...)` is a parse
error. Renamed the handle constructor to `kv::shared_collection(...)`
(keeps the user's "shared" intent; unambiguous). Updated everywhere.
- CLI manifest: `collections = [...]` on `[group]` only (ManifestApp has no such
field + deny_unknown_fields → an app manifest carrying it is a hard error);
Manifest::collections() accessor; build_bundle emits it.
- plan/apply: a `collection` row group in `pic plan`; created/deleted counts in
the apply summary; collections threaded through PlanDto/NodePlanDto/
ApplyReportDto.
- read-only `pic collections ls --group` → GET /admin/groups/{id}/collections
(viewer+), backed by ApplyService::collection_report + CollectionInfo.
- journey: a group declares `catalog`; app A writes, app B (same subtree) reads
it back; a sibling-subtree app gets CollectionNotShared; `collections ls` +
re-apply NoOp. Full suite 114/114.
- docs: groups-and-project-tool §11.6 (KV-only MVP shipped + deferrals),
sdk-shape.md (the shared-collection handle + trust model), CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the manifest `extension_points` from a top-level key to an `[app]`/
`[group]` node key (`ManifestApp`/`ManifestGroup` + a `Manifest::extension_points()`
accessor). A bare top-level array placed after the node header was silently
re-nested by TOML into that table and dropped; as a node key it parses
unambiguously wherever it sits. build_bundle/pull/init updated.
- Journeys (live server + Postgres): a group default body resolves for an
inheriting app; the app provides its own module and OVERRIDES the EP (the
inverse of the Phase 4b sealed import); `extension-points ls` shows the
provider; a body-less EP with no provider fails `pic plan`.
- Re-bless the schema snapshot (new `extension_points` table).
- Docs: §5.5 marked complete (extension points ✅) + CLAUDE.md current-focus.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two journeys against a live server + Postgres:
- `group_module_is_lexically_sealed_under_inheritance`: a group module +
a group endpoint that imports it, inherited by an app — proves the §5.5
trust boundary (a leaf's same-named module does NOT shadow the inherited
endpoint's import) and app-origin CoW (an app endpoint's import resolves
the app's module).
- `dangling_import_is_rejected_by_plan`: a manifest script importing a
non-existent module is a `pic plan` error.
Docs: mark §5.5 residual resolved + §11 Phase 4b ✅ in the design doc;
update CLAUDE.md current-focus (extension points are the remaining §5.5
piece).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 5 C5. Three `pic ... --dir` tree journeys:
* a group + a nested app apply atomically as one tree (the app's route binds
the group's same-apply `shared` script); the group node's script is
created; re-plan is all-noop (idempotent),
* one invalid node aborts the whole tree — the valid group node's script is
NOT created (true all-or-nothing),
* a `pic groups reparent` between `pic plan --dir` and `pic apply --dir` trips
the bound-plan (StateMoved) check via the tree's structure version.
Docs: `groups-and-project-tool.md` §11.5 status (✅ shipped — single-repo
nested tree apply; multi-repo single-owner + per-env approval gating deferred
per §11.1) + CLAUDE.md current-focus.
Gates: fmt, clippy -D warnings, cargo test --workspace, check-versioning (50
migrations — Phase 5 added none), schema snapshot unchanged, full journey suite
108/108. (Pre-existing flake `queue_e2e::queue_receive_acks_on_success` — a
racy immediate `count==0` after the async ack; flaky at the pre-Phase-5
baseline too, unrelated to this work.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mark §11.4 ✅ shipped with the live-resolution decision (no body
materialization / invalidation fan-out, mirroring Phase 3's config), the
shipped surface, and the deliberate Phase-4b deferrals (group modules + lexical
import resolver, invoke-by-id, live auto-rebinding). Note the sharp edge:
routes.script_id ON DELETE CASCADE means deleting a group script drops
descendant routes. Update the §5.1 materialization box and CLAUDE.md
(current-focus + the data-model invariant now covers group-owned scripts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The groups/project-tool work had no home in the blueprint's version
scheme, two "Phase N" numbering schemes overlapped, and CLAUDE.md's
"current focus" was stale (claimed v1.1.0 SDK foundation; we're at v1.1.9
with the SDK line complete). Reconcile all three:
* blueprint §12 Phase 5 (v1.2, already titled "Advanced Workflows &
Hierarchies") — split into a Hierarchies track (the groups + project
tool initiative, with §11 Phases 1–3 marked shipped) and a Workflows
track, with sequencing flagged as an open call. This gives the work an
explicit version home: the Hierarchies half of v1.2.
* design doc header — Status Draft → Active, "scheduled as the Hierarchies
track of v1.2", and an explicit warning that §11's local Phase 1–6
numbering is distinct from the blueprint product-phase numbering.
* design doc §11.1 — a post-Phase-3 re-sequencing review: resolve Phase 4
scripts LIVE (drop the fan-out-invalidation sub-project, per the Phase 3
result); consider a "Phase 5-lite" (declarative apply of vars/secrets +
app scripts) before the hard group-scripts work; and force the two
deferred decisions (trigger/route templates vs tenant cardinality;
multi-repo ownership vs a single-repo start).
* CLAUDE.md — refresh "current focus" to v1.2 Hierarchies, and correct the
now-broken "every v1.1+ table is app_id NOT NULL" invariant: the new
group-inheritable config tables (vars, secrets) use a polymorphic owner
(group_id XOR app_id), not app_id NOT NULL.
Doc-only; no code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Update §4.3: the fire-time `enabled` re-check is shipped and test-backed on
BOTH async arms (the unified outbox `!resolved.active` gate and the queue
arm's fresh-script nack), with the specific regression tests cited, plus the
same-app backstop now present on every async build path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the implementation outcome back into the project-tool spec (§11) as
the design asks. Records what landed (init/pull/config/plan/apply/prune,
env overlays, enabled lifecycle, trigger name+backfill, content-fingerprint
bound-plan, apply lock) and the deliberate deferrals: tree-structure
version → groups; vars/--explain → Phase 3; trigger upsert-by-name + the
dashboard enabled toggle → tracked follow-ups beyond the §11 bullets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The architecture spec for the full vision — a declarative project tool
(picloud.toml + pull/plan/apply/prune) and a server-side nested-groups
hierarchy with live-resolve inheritance. Reviewed across several passes
for consistency, gaps, and contradictions; resolutions are folded in
beside the topics they settle.
This commit lands the spec only. The first slice of implementation (the
single-app reconcile foundation) follows; groups, env-scoping, and the
`enabled` toggle are later milestones called out in the doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>