diff --git a/infrastructure/cloud-compose.mdx b/infrastructure/cloud-compose.mdx
index 694cc1d..77a7eff 100644
--- a/infrastructure/cloud-compose.mdx
+++ b/infrastructure/cloud-compose.mdx
@@ -78,17 +78,30 @@ Prefer the module for the provider the caller already selected:
| Entry point | Modular source pattern | Infrastructure boundary | Identity and secrets |
| --- | --- | --- | --- |
-| Google Cloud | `github.com/libops/cloud-compose//providers/gcp?ref=RELEASE_OR_FULL_SHA` | Compute, network, firewall, persistent disks, service accounts, and optional GCP integrations | Can use a consumer-configured Vault GCP IAM role; the operator owns the role, policy, and secret paths |
+| Google Cloud, new deployment | `github.com/libops/cloud-compose//providers/gcp?ref=RELEASE_OR_FULL_SHA` | Compute, network, firewall, persistent disks, service accounts, and optional GCP integrations | Can use a consumer-configured Vault GCP IAM role; the operator owns the role, policy, and secret paths |
+| Google Cloud, 1.x-compatible root | `github.com/libops/cloud-compose?ref=RELEASE_OR_FULL_SHA` | The same GCP boundary while preserving the historical indexed `module.gcp[0]` address | The root accepts only `cloud_provider = "gcp"` and has the same GCP identity boundary |
| DigitalOcean | `github.com/libops/cloud-compose//providers/do?ref=RELEASE_OR_FULL_SHA` | Droplet, network/firewall inputs, and persistent volumes exposed by the module | Consumer-managed provider token and secret authentication |
| Linode | `github.com/libops/cloud-compose//providers/linode?ref=RELEASE_OR_FULL_SHA` | Instance, firewall inputs, and persistent volumes exposed by the module | Consumer-managed provider token and secret authentication |
| Ansible | `ansible/` role from the pinned release | Bootstrap files and packages on an existing Debian or Ubuntu host | The existing-host owner supplies OS access, secrets, network, DNS, firewall, and storage |
| Salt | `salt/` formula from the pinned release | Bootstrap files and packages on an existing Debian or Ubuntu host | The existing-host owner supplies OS access, secrets, network, DNS, firewall, and storage |
-Pin the source to the reviewed release or a full commit. The compatibility dispatcher at the repository root remains available, but a new caller should use one provider entrypoint so Terraform does not load unrelated providers. Do not source production infrastructure from a moving branch.
+Pin the source to the reviewed release or a full commit. Since `1.3.0`, each
+entrypoint statically declares only its target provider. The repository root is
+not an all-provider dispatcher: it is a GCP-only compatibility entrypoint for
+existing GCP state and intentionally has no DigitalOcean or Linode dependency.
+New deployments should select one path under `providers/`. Do not source
+production infrastructure from a moving branch.
Provider entrypoints should normalize the same runtime object before calling provider-specific resources. A template preset supplies repository, plugin, and package defaults only; an explicit caller value wins. Shared host behavior belongs in provider-neutral runtime modules so Google Cloud, DigitalOcean, Linode, Ansible, and Salt do not grow independent application lifecycles. Each adapter must reject an input it cannot support rather than silently ignore it.
-Changing an existing deployment from the root dispatcher to a provider module changes module addresses even when the real infrastructure is identical. Preserve the addresses with release-provided `moved` blocks or an explicit, reviewed state migration. Do not let Terraform destroy and recreate a data-bearing host to make the module path look cleaner.
+An existing GCP deployment can remain on the root source at `1.3.0` or later without
+loading unrelated providers or moving `module.gcp[0]`. Moving it to
+`providers/gcp` is an optional, separate state refactor. Existing DigitalOcean
+and Linode callers must adopt their provider-specific source and place the
+release-documented `moved` block in the caller's root module. In every case,
+back up remote state and require a plan with address moves only—no VM, firewall,
+or durable-volume replacement or deletion. Do not recreate a data-bearing host
+merely to make a module path look cleaner.
## Pin the complete runtime contract
@@ -150,9 +163,9 @@ If the selected release does not contain that foundation module, follow its
release-specific IAM instructions. Do not copy an unpublished foundation
module from a development branch into an otherwise pinned production stack.
-
-In the [July 13, 2026 release-status snapshot](/infrastructure/current-release-status), PPB `0.5.0` exists as a source/GitHub release but its versioned container is not published. The power-management paragraphs below define the contract a selected release must satisfy; they do not make `0.5.0` a consumable image or make the managed shared-router/private-PPB path available.
-
+
+The [July 17, 2026 release-status snapshot](/infrastructure/current-release-status) records cloud-compose `1.5.0` and the signed PPB `0.5.1` image as released self-hosted dependencies. That does not make the separate managed shared-router/private-PPB integration generally available; its API, edge, and end-to-end promotion gates remain independent.
+
The optional Cloud Run power-management ingress reaches the VM's private
application port through [Direct VPC
@@ -166,10 +179,20 @@ Terraform identity needs authority to manage host-project and subnet IAM; the
application Terraform identity needs authority to inspect the selected network
and manage its per-stack firewall rule.
+Do not assign the Direct VPC revision to a [Serverless VPC Access connector's
+subnet](https://cloud.google.com/vpc/docs/serverless-vpc-access#ip_address_ranges).
+A connector requires its own otherwise-unused `/28`, while Direct VPC
+requires capacity in the selected `/26` or larger application subnet. If an
+unrelated workload still needs a connector, give that connector a separate
+subnet and keep it outside this firewall source range unless it belongs in the
+same trust boundary.
+
The word "ingress" here describes the proxy's application role, not a Direct
-VPC ingress feature. Cloud Run services do not support Direct VPC ingress.
-Public requests enter Cloud Run through its configured service/edge ingress;
-Direct VPC egress is only the private hop from that service to the VM.
+VPC ingress feature. Cloud Run services and jobs do not support Direct VPC
+ingress; worker pools have a separate ingress capability that this architecture
+does not use. Public requests enter Cloud Run through its configured
+service/edge ingress; Direct VPC egress is only the private hop from that
+service to the VM.
Cloud-compose configures this Cloud Run attachment with
`vpc_direct_egress = "PRIVATE_RANGES_ONLY"`. Only requests to private
@@ -199,6 +222,12 @@ the same trust boundary. Leave free capacity for `/28` allocation blocks,
roughly two addresses per running service instance, and overlapping revision
retention; an already occupied `/26` is not sufficient.
+In a Shared VPC, also audit hierarchical firewall policies and every existing
+host-project ingress rule that can reach the VM. The narrow rule owned by one
+cloud-compose application state cannot cancel a broader rule owned elsewhere.
+Record the subnet's other consumers and re-audit them before widening either
+the source range or the application port.
+
A release that provides power management must declare bounded power-on, TCP
connection, and Cloud Run request budgets. Check the selected release for the
tested values. The proxy adds no application-level request or response-status
@@ -206,6 +235,15 @@ retry after a connection succeeds; Go may still retry requests it classifies
as replayable when a pooled connection is found stale. Operators and clients
must still tolerate occasional connection resets during network maintenance.
+Cloud-compose `1.5.0` retains the tested direct public PPB path with a 240-second
+power-on budget and a 300-second backend-dial budget inside the 600-second
+Cloud Run request ceiling. Its hosted GCP gate sends one request, verifies the
+VM reaches `RUNNING`, follows a changed ephemeral external address after a
+stop/start, then performs host-level and application checks. Those direct-path
+budgets do not fit behind non-Enterprise Cloudflare's 120-second read limit;
+the managed router uses a separate pre-dispatch retry contract documented in
+[Security and Operations](/platform/security-operations).
+
In releases that expose the hardened power-management contract, enabling it
requires explicit original-client CIDRs in
`gcp.network.power_button_allowed_ips`; cloud-compose does not silently trust
@@ -229,12 +267,16 @@ depth zero rather than trust the incoming `X-Forwarded-For` chain. See [Security
and Operations](/platform/security-operations) for that target managed request
path.
-Google recommends an egress-aware startup probe for ordinary Direct VPC
-services. The power-button proxy cannot use application reachability as its
-startup gate because the application VM is intentionally allowed to be off;
+Google warns that a new Direct VPC instance can take a minute or more to
+establish a connection and recommends an egress-aware startup probe with
+retries for ordinary services. The power-button proxy cannot use application
+reachability as its startup gate because the application VM is intentionally
+allowed to be off;
the proxy has to accept the first request before it can start that VM. Its
-bounded backend dial loop is the retry boundary for this design. A generic
-process-only probe would not prove VPC connectivity.
+bounded backend dial loop is the retry boundary for this design. The selected
+release therefore uses a process-only startup probe to avoid deadlocking the
+wake path. That probe proves only that PPB can accept requests; the hosted
+first-request smoke test is the evidence for Direct VPC connectivity.
Cloud Run can retain addresses for up to 20 minutes after a service revision
scales down. After a service is deleted or moved away, wait one to two hours
@@ -309,6 +351,15 @@ When opting in:
When a selected release advertises verified managed artifacts, it must verify downloaded Docker plugins, sitectl packages, rootfs archives, and optional rollout artifacts before atomic installation. Preserve those checks when replacing an installer or adding another managed artifact.
+Automated Docker cleanup is disabled by default and must remain independent of
+key rotation or application rollout. If an operator enables the dedicated
+cleanup timer, preserve rollback-tagged images: prune old stopped containers,
+unused networks, dangling images, and build cache explicitly, with an age
+filter and a single host lock. Do not restore `docker system prune --all`; it
+can remove the named image retained for a local rollback. The safer policy may
+retain unused named images, so monitor disk use and remove an obsolete named
+release only after its rollback window closes.
+
## Adopting the modular runtime contract
Use the `required_version` declared by the exact module source. A release that adopts the modular runtime contract changes power management and privileged internal services from implicit behavior to explicit opt-in. To preserve the earlier GCP behavior, configure the controls exposed by that release before advancing the module ref:
diff --git a/infrastructure/current-release-status.mdx b/infrastructure/current-release-status.mdx
index aab4365..4d4a510 100644
--- a/infrastructure/current-release-status.mdx
+++ b/infrastructure/current-release-status.mdx
@@ -7,13 +7,14 @@ description: "A dated availability record for the independently released artifac
There is not yet a published, platform-wide known-good release set for the managed shared-router and private-PPB request path. The architecture pages describe the target contract, but they are not evidence that this integration is generally available. Do not change production DNS or infrastructure for that path until a later status record identifies every immutable artifact and its green end-to-end release gate.
-This snapshot was reviewed on **July 13, 2026 at 23:00 UTC**. It is a release record, not a moving "latest" lookup. A later tag does not silently update the compatibility claims on this page.
+This snapshot was reviewed on **July 17, 2026 at 02:44 UTC**. It is a release record, not a moving "latest" lookup. A later tag does not silently update the compatibility claims on this page.
## Status meanings
| Status | Meaning |
| --- | --- |
| Released dependency | The named independent release exists. Consumers must still verify its release notes, immutable identity, and compatibility with their complete deployment. |
+| Published compatibility baseline | The named release records an exact template and package set that passed its release gates. It is not evidence that every provider/application combination or every transitive image was tested together. |
| Released docs set | The exact core and plugin commits were used to generate and validate the published command documentation. |
| Unrecorded | Independently usable artifacts exist, but LibOps has not published an aggregate compatibility record proving one complete cross-repository set. |
| Preview | Code or architecture work exists, but the production integration and rollout gate are incomplete. |
@@ -23,19 +24,23 @@ This snapshot was reviewed on **July 13, 2026 at 23:00 UTC**. It is a release re
| Surface | Dated reference | Status | What this record proves |
| --- | --- | --- | --- |
-| Core `sitectl` and application plugins | [Generated documentation dependency manifest](https://github.com/libops/sitectl-docs/blob/de317cf1af3ade6097fbe37470a78790807ad713/scripts/snippet-dependencies.json) | Released docs set | The documentation was generated from the full core and plugin commits in that manifest. Package versions remain independent. |
-| cloud-compose | [`0.10.3`](https://github.com/libops/cloud-compose/releases/tag/0.10.3) | Released dependency | This tag exists as a stable self-hosted baseline. Only fields and migrations present in that tag are available to its consumers. |
-| Terraform Cloud Run v2 module | [`0.7.0`](https://github.com/libops/terraform-cloudrun-v2/releases/tag/0.7.0) | Released dependency | The reusable module release exists. That does not prove that cloud-compose or the managed control plane has promoted every module capability. |
+| Last published generated `sitectl` documentation set | [Generated documentation dependency manifest](https://github.com/libops/sitectl-docs/blob/3d41c3aa9d7640dd79a6e1d477546990bf75ac5a/scripts/snippet-dependencies.json) | Released docs set | The documentation was generated from the exact coordinated core and application-plugin `v1.0.0` commits in that manifest, with managed-platform `sitectl-libops` independently pinned at `v1.3.0`; its exact-main documentation CI passed. |
+| cloud-compose | [`1.5.0`](https://github.com/libops/cloud-compose/releases/tag/1.5.0) | Released dependency | Provider isolation, the GCP-only compatibility root, compiled CI helper, exact per-run smoke ownership, rollback-preserving Docker cleanup, and coordinated sitectl v1 presets passed Terraform/configuration lint plus hosted GCP, DigitalOcean, Linode, Ansible, Salt, and fresh-install gates. Its rootfs and checksum release assets were also published successfully. |
+| Self-hosted application baseline | [cloud-compose `1.5.0` template presets and released matrix](/templates/compose-projects#released-compatibility-baseline) | Published compatibility baseline | Every preset pins core `sitectl` and its independently released application plugin at `v1.0.0`. Application templates are `v1.0.0`, except ISLE `v1.1.0`; direct image identities are immutable where the template owns them, while explicitly component-controlled Islandora images use the catalog's reviewed `ISLANDORA_TAG=6.3.19` default. The matrix does not claim that all seven applications ran on all five infrastructure adapters in one aggregate test. |
+| ISLE v1 set | [`libops/isle` `v1.1.0`](https://github.com/libops/isle/tree/v1.1.0), [`sitectl-isle` `v1.0.0`](https://github.com/libops/sitectl-isle/releases/tag/v1.0.0), core/Drupal `v1.0.0`, and [cloud-compose `1.5.0`](https://github.com/libops/cloud-compose/releases/tag/1.5.0) | Released dependency | The ISLE template and DigitalOcean catalog smoke passed, the plugin's binary archives, Homebrew formula, Debian packages, and RPM packages are published, and the cloud catalog supplies the minimum supported Islandora image tag without overriding an explicit downstream value. |
+| Terraform Cloud Run v2 module | [`0.8.0`](https://github.com/libops/terraform-cloudrun-v2/releases/tag/0.8.0) | Released dependency | The reusable module release exists. That does not prove that cloud-compose or the managed control plane has promoted every module capability. |
+| PPB support image | [`0.5.1`](https://github.com/libops/ppb/releases/tag/0.5.1) at `sha256:249697fe2ce7e007053af270be2d5cb064ffa545572a035e405e2763298149bc` | Released dependency | GHCR and public GAR expose the same multi-platform manifest. Its keyless signature verified against the pinned shared publisher workflow identity. This proves the support image release, not the unreleased managed router integration. |
| Buildkit images and Compose templates | The exact template checkout and each `tag@sha256:digest` it records | Unrecorded | Templates record immutable direct-image identities, but this snapshot does not contain a generated aggregate of every template commit, Buildkit digest, and hosted smoke-test result. |
The table deliberately does not invent an image digest, template commit, signature identity, or CI result that has not been captured by a release manifest. For self-hosted deployments, build a deployment-specific record from the exact template commit and immutable references you test.
-## Managed integrations not released in this snapshot
+## Integrations not released in this snapshot
| Integration | Status | Gate that remains |
| --- | --- | --- |
-| Versioned PPB support image | Blocked | A [`0.5.0` source/GitHub release](https://github.com/libops/ppb/releases/tag/0.5.0) exists, but its versioned container is **not published**. There is no released PPB `tag@sha256:digest` to consume or verify against the expected publisher identity. The source tag is not a substitute for that container artifact. |
-| Cloudflare and load balancer to shared Cloud Run router to private per-site PPB | Blocked / preview | Publish and verify the exact router and PPB image digests and expected keyless publisher identities; merge and release the API/router integration; pin the dependencies in the managed Terraform roots; pass the private-origin, client-IP, authorization-preservation, timeout, Direct VPC, and rollback canaries. |
+| Cloudflare and load balancer to shared Cloud Run router to private per-site PPB | Blocked / preview | PPB `0.5.1` is available, but the exact router image, API/router integration, managed Terraform pins, private-origin, client-IP, authorization-preservation, split timeout, Direct VPC, and rollback canaries still need promotion as one managed set. |
+| Organization Vault three-image runtime | Blocked / preview | The verified-publisher desired state is merged, but the public GAR writer migration has not passed a successful central Terraform apply and protected-workflow publication canary; the Vault Init GAR upload still fails closed. Apply and verify that central state, publish and verify the independently versioned `vault-server`, `vault-init`, and `vault-proxy` manifests, release the sitectl-admin digest resolver and exact tag-commit/signature gate, pin all three immutable GAR manifests, and pass the API, Terraform, initialization, recovery, and rollback gates. A source release, local candidate, or reserved version is not an aggregate runtime release. |
+| Canonical API image set and production VM resolver | Blocked / preview | Merge the hosted post-CI publisher and `sitectl admin terraform api-compose-images`; publish the exact protected-main run to GHCR plus the appropriate private or public GAR repository; verify every digest's reusable-workflow identity, caller repository/ref/SHA, and caller-workflow annotation; and prove fresh VM bootstrap plus in-place refresh with the four verified private-GAR Compose images, the checkout detached at the publication commit, and legacy boot-disk discovery that fails on ambiguity. |
| Separate request-serving API and `api-worker` Cloud Run services | Preview | Release the managed worker deployment and prove identity bootstrap, database connectivity, readiness, rollout, rollback, and removal of any temporary migration privilege. A process boundary in source or Compose is not proof of this Cloud Run topology. |
| Platform-wide image, template, CLI, plugin, and infrastructure compatibility manifest | Blocked | Generate the aggregate record from release automation and attach hosted CI evidence for the exact references. Until then, each operator owns a deployment-specific record. |
diff --git a/infrastructure/release-compatibility.mdx b/infrastructure/release-compatibility.mdx
index 08d9d55..a9af779 100644
--- a/infrastructure/release-compatibility.mdx
+++ b/infrastructure/release-compatibility.mdx
@@ -22,9 +22,9 @@ Publish and consume changes in this order:
| 3 | Core `sitectl` | CLI and plugin SDK contract tests pass; package artifacts are published |
| 4 | Application plugins | Each plugin is built against the required core; rollout and component tests pass |
| 5 | Dependent plugins | Included plugins are published first; Drupal precedes ISLE when ISLE requires the newer Drupal contract |
-| 6 | Reusable Terraform dependencies and support images | Required module commits are merged; support images such as PPB are released by immutable digest and signed by the expected publisher identity; validation, mock and image tests, and release CI pass |
+| 6 | Reusable Terraform dependencies and support images | Required module commits are merged; support images such as PPB are released by immutable digest and their keyless signatures verify against the expected protected workflow identity; validation, mock and image tests, and release CI pass |
| 7 | `cloud-compose` | The referenced Terraform modules, support-image digests, sitectl package set, and templates are available; provider, bootstrap, and hosted smoke tests pass |
-| 8 | Managed API and control plane | Its pinned infrastructure contract and support images resolve; managed images come from CI-passed commits and have verified signatures; Go, Terraform contract, mock-plan, and hosted managed-path checks pass |
+| 8 | Managed API and control plane | Its pinned infrastructure contract and support images resolve; managed image digests come from the expected CI-passed commits and their signatures verify against the protected publisher identity; Go, Terraform contract, mock-plan, and hosted managed-path checks pass |
| 9 | Public documentation | Every referenced image, package, template, Terraform dependency, cloud-compose tag, and managed integration is live; generated command references are pinned to the released commit set; docs validation and link checks pass |
A layer may be unchanged in a release. The ordering still matters whenever its contract changes.
@@ -34,17 +34,146 @@ dependency. When it adopts a new core SDK helper, update and test it only after
that core release is live, and publish its contributor documentation after the
scaffold branch is merged.
+## Container registry publication contract
+
+LibOps uses GHCR as the general registry copy. Add a GAR copy when Cloud Run or
+another GCP runtime consumes the image. The Buildkit application-image matrix
+is a separate shared publication path: it publishes the same versioned
+multi-platform manifest to Docker Hub and GHCR because Compose templates still
+use the readable `libops/...` Docker Hub names. It does not need GAR merely
+because another LibOps image does.
+
+Support-image and managed-image repositories should be thin callers of the
+pinned reusable `libops/.github` build workflow. They select the Dockerfile,
+context, build arguments, image name, and registry destinations, while the
+reusable workflow owns registry authentication, platform tags, vulnerability
+scanning when requested, digest handoff, multi-platform manifest assembly, and
+publication. In the verified path, each native image is built once, scanned
+before publication, then pushed to GHCR and optional GAR. The workflow records
+each registry's push-reported platform digest and assembles the final manifest
+from digest references, never by rereading mutable platform tags.
+Cross-registry writes cannot be atomic, so callers use immutable commit or
+release tags and safely rerun a failed publication. When signing is enabled,
+the workflow keylessly signs each registry's final digest and immediately
+verifies the signature against the pinned reusable-workflow identity. The
+workflow does not currently create an OCI provenance attestation, so a
+signature proves the configured publisher identity and exact digest, not a
+complete build-material statement. A repository may still coordinate release
+dispatch or run its own application tests; it should not copy the registry
+login and image-publishing implementation.
+
+Public Cloud Run support images use the GitHub workload-identity pool in
+`libops-images` and its shared `github@libops-images` publisher. The publisher
+receives short-lived credentials and GAR-repository-level writer grants; no
+service-account key belongs in GitHub. Treat adding a GitHub repository to that
+publisher as a trust-boundary change: the shared identity can publish to every
+registry granted to that account. A private GCP project may provide its own
+workload-identity provider and publisher account to the same reusable workflow
+instead of creating a repository-local publishing implementation.
+
+The `api/iac/libops-images` root owns the public pool, verified and legacy
+provider conditions, the shared publisher service account, repository
+configuration, and GAR writer grant. A verified publisher contract binds the
+repository, protected-main or explicitly allowed release-tag ref, caller
+workflow, and immutable reusable publisher workflow commit. The legacy
+provider is a temporary repository-only migration path. Inspect the reviewed
+Terraform revision and the applied IAM state for the exact allowlist rather
+than copying one from this page.
+
+A caller repository's `GCLOUD_OIDC_POOL` and `GSA` values only select the
+centrally owned provider and service account; they do not create or own a
+second publisher. LibOps stores those identifiers as Actions secrets for
+configuration distribution convenience, but they are not credentials or a
+security boundary. Apply and review the central IAM change before enabling a
+new public caller. Private API-project GAR publication remains in the API
+project's infrastructure and must not be added to the public publisher's
+authority. A merged Terraform change is desired state, not evidence that the
+central IAM apply completed. Record the successful apply and a
+protected-workflow GAR publication canary separately before relying on a new
+trust condition, writer grant, or caller revocation.
+
+For a verified caller, WIF rejects another repository, ref, caller workflow, or
+reusable-workflow revision before it can impersonate the shared account. That
+does not make the caller workflow's contents immutable: repository write and
+workflow governance, protected main, the reusable workflow's exact-main guard,
+digest pinning, and post-publication Cosign claim verification remain separate
+controls. The legacy provider enforces only its repository allowlist; any
+runnable workflow in one of those repositories that can request
+`id-token: write` can still satisfy that older condition. Manual dispatch or
+tag publication remains possible where a caller exposes it and must be reviewed
+as a privileged publication, not described as something Actions-secret access
+prevented.
+
+Downstream forks do not inherit LibOps publisher authority. Fork owners should
+keep GHCR as the general distribution channel, create their own WIF trust and
+GAR repository only when they deploy to GCP, and pin the reusable workflow to a
+reviewed commit. Pull-request jobs build and test without publisher credentials.
+Protected-branch or release jobs publish, and deployment records consume the
+resulting digest rather than trusting a movable tag.
+
+Do not weaken branch protection when this split changes a historical reusable
+job name. The shared, credential-free `pr-status.yaml` workflow can preserve a
+required `run / merge` check: the caller's `run` job depends directly on every
+real PR lint, test, and native-image scan, runs with `always()`, passes the
+complete `toJSON(needs)` object, and pins the reusable gate by full commit. The
+gate fails for malformed, empty, failed, cancelled, or skipped dependencies and
+has no checkout, package, OIDC, secret, or registry permission. The main-only
+publisher remains a separate caller job.
+
+The target API repository publication gives every built image a
+GHCR copy. Images consumed by LibOps GCP runtimes also receive a GAR copy:
+`api` (also tagged as `dash`), `api-vault-agent`, and `control-plane` use the
+private API-project repository; `terraform-runner`, `gcp-vm-ip-controller`,
+`controller-ingress`, and `site-controller` use shared public GAR; and
+`cloudflare-ingress-router` uses the separate shared-infrastructure GAR
+repository. The API workflow does not publish `api-init` or `vault-server`.
+A successful publication uses one
+`sha-<40-character-commit>-run--attempt-`
+selector across its set, but deployment resolves each name independently and
+passes only digest references.
+
+Before this API publication path can be promoted, the admin plugin must resolve
+every selected API bundle image and the independently released Vault server to
+fail-closed GAR digest references. The VM must record the canonical API
+selector and detach its Compose checkout at the embedded commit before
+starting services. Only an explicitly configured development branch or
+`local` may follow a branch pull. Local Compose build fallbacks remain useful
+for development; they are not a production substitute for this verified source
+and image handoff. Publish the exact commands and current image-to-variable
+mapping in the sitectl documentation only when that resolver is released; an
+architecture page must not act as an unreleased CLI reference.
+
+Keep similarly named images in their actual ownership boundary. The following
+organization Vault runtime is a target promotion contract until the
+[current release status](/infrastructure/current-release-status) records the
+complete released set. It selects three independent images:
+
+- `vault-server` is built by `terraform-vault-cloudrun` from the exact verified
+ upstream Vault source release with a patched Go toolchain. Its protected-main
+ image workflow writes the application-versioned release to GHCR and public
+ GAR; Terraform pins that public GAR digest.
+- `vault-init` and `vault-proxy` are independently versioned public support
+ repositories. Each publishes its reviewed release to GHCR and public GAR
+ through the shared `libops-images` publisher, and `sitectl-admin` resolves
+ each selected semantic release to its own immutable GAR digest.
+
+Do not advance these three inputs under one shared tag. The API repository's
+private `api-vault-agent` image is a separate API-side helper and is not the
+organization Vault server or initializer. Public support images must not borrow
+the private API publisher identity, and private managed images must not expand
+the public publisher's authority.
+
## Managed artifact promotion gates
-A digest fixes image bytes, but it does not prove which repository or workflow produced them. Managed request-path images add a provenance gate to the dependency order:
+A digest fixes image bytes, but it does not by itself prove which repository or workflow produced them. Managed request-path images therefore add a publisher-evidence gate to the dependency order:
1. Pull-request CI builds, tests, and scans the proposed image without production publisher credentials.
-2. A publisher runs only after the repository's required CI succeeds for the exact commit on the protected primary branch. It publishes that commit's image, records its digest, and signs the digest with short-lived workload identity.
-3. Before changing Terraform, the release operator verifies the signature against the expected repository and workflow identity, not merely any valid keyless signature.
+2. Release policy permits stable publication only after the repository's required CI succeeds for the exact commit on the protected primary branch. The publisher builds and scans that exact commit, obtains a short-lived registry credential through Workload Identity Federation, records each published digest, and keylessly signs the final manifest in each registry. For a caller on the `verified-publisher` provider, the WIF condition requires the exact repository, protected-main ref, caller workflow, and pinned reusable publisher revision. A caller still on the temporary legacy provider receives only repository-level WIF enforcement; GitHub workflow guards and post-publication verification must enforce the remaining claims until that caller migrates.
+3. Before changing Terraform, the release operator verifies the successful GitHub run, exact reusable-workflow commit, caller repository, ref, commit SHA, caller-workflow annotation, WIF trust condition, registry digest, and Cosign certificate identity. API-owned images require the `libops/api/.github/workflows/images.yml@refs/heads/main` annotation. Tag-published Vault initializer and proxy images require their `goreleaser.yaml@refs/tags/` annotation; resolve annotated tag objects to the final commit before comparing the signature claim. The separately built Vault server uses its protected-main image workflow instead. The current publisher does not create an OCI provenance attestation, so documentation and promotion records must not claim one.
4. Terraform pins the verified digest. A readable tag may accompany it, but a tag is not the deployed identity.
5. The dependent module or control plane runs validation and its hosted end-to-end path with those exact references before promotion.
-For the managed site path, publish and verify the PPB support image before cloud-compose or the vendored managed runtime points at it. Publish and verify the shared router image before shared edge Terraform pins it. The managed API and control plane move only after both infrastructure dependency sets are available and green. A failed smoke test is fixed at the producing layer and republished; it is not bypassed by pointing consumers at a moving tag.
+For the managed site path, publish and verify the PPB support image before cloud-compose or the vendored managed runtime points at it. Publish and verify the shared router image before shared edge Terraform pins it. Here, verification means both matching the digest to the protected CI run and validating the keyless signature against the pinned reusable publisher identity. The managed API and control plane move only after both infrastructure dependency sets are available and green. A failed smoke test is fixed at the producing layer and republished; it is not bypassed by pointing consumers at a moving tag.
## Images and templates
@@ -69,11 +198,22 @@ A deployment package manifest should record one reviewed version per package:
Use the [sitectl installation guide](https://sitectl.libops.io/install) for current minimum versions and dependency behavior. Use the [contributor release guide](https://sitectl.libops.io/contributing/development) for the current publisher sequence.
+The [released self-hosted application baseline](/templates/compose-projects#released-compatibility-baseline)
+records the exact core, application-plugin, template, and base-image references
+selected by cloud-compose `1.5.0`. The managed-platform `sitectl-libops`
+package and the contributor-only `sitectl-app-tmpl` scaffold are separate
+release surfaces; neither should be inserted into every application preset.
+
+The ISLE v1 set in that catalog uses template `v1.1.0` and core, Drupal, and
+ISLE packages at `v1.0.0`. Its reviewed application environment supplies
+`ISLANDORA_TAG=6.3.19`; an explicit caller value overrides the catalog default
+and becomes part of the caller's compatibility and rollback record.
+
Cloud-compose releases that expose `runtime.sitectl.package_versions` can pin each installed package independently. A legacy shared `version` value is only a fallback for packages without an override. Releases with only one shared version cannot safely represent independently versioned plugins; do not claim reproducible package pins until the selected module actually exposes and records the per-package map.
Production should not follow a package manager's `latest` channel without a reviewed compatibility record.
-The current Linux package publisher rebuilds APT and RPM metadata from the current release artifacts and does not retain older versions in the package repository. A per-package version map is therefore reproducible only while every named package remains downloadable. For a long-lived production rollback, retain a repository snapshot or archive the matching GitHub release packages and `checksums.txt` in controlled storage. A version string without a retained artifact is not a recovery plan.
+The rolling Linux package channel rebuilds APT and RPM metadata from release artifacts. It may retain older packages, but retention is not an immutable API and maintenance may prune them. A per-package version map is therefore reproducible only while every named package remains downloadable. For a long-lived production rollback, retain a repository snapshot or archive the matching GitHub release packages and `checksums.txt` in controlled storage. A version string without a retained artifact is not a recovery plan.
## Compatibility record
@@ -139,10 +279,30 @@ Before applying:
- inspect replacements, deletes, IAM changes, firewall exposure, disk changes, and user-data changes;
- back up state and data independently.
-Moving from a root provider dispatcher to `providers/gcp`, `providers/do`, or `providers/linode` can change Terraform resource addresses even when the real resources stay the same. Use release-provided `moved` blocks or explicit, reviewed `terraform state mv` operations. Never accept destroy-and-recreate for a data-bearing deployment merely to change the module address.
+Cloud-compose `1.3.0` changes the repository root from a multi-provider
+dispatcher to a GCP-only compatibility entrypoint while preserving the
+historical indexed GCP address. Existing GCP callers can stay on that root and
+avoid both unrelated providers and a state move. Moving GCP to `providers/gcp`,
+or moving a 1.x DigitalOcean or Linode caller to its required
+`providers/do` or `providers/linode` entrypoint, changes module addresses even
+when the real resources stay the same. Use the release-documented caller-side
+`moved` blocks or explicit, reviewed `terraform state mv` operations. Never
+accept destroy-and-recreate for a data-bearing deployment merely to change the
+module address.
When a selected modular release changes GCP power management, privileged internal services, or automatic updates to explicit opt-in, review every removed IAM and service resource. Cloud-init and identity changes can replace a VM or boot disk; DigitalOcean and Linode user-data changes can also replace instances. Review persistent-disk survival and public-IP/DNS effects, not only the application diff. Follow the migration checklist in the [cloud-compose deployment boundary](/infrastructure/cloud-compose).
+The managed API root uses the stable `api-production-boot` name for a new boot
+disk. An older imported deployment can still have the hash-derived name created
+by its previous cloud-compose path:
+`api-production-boot-<32-lowercase-hex>`. The supported deployment wrapper
+queries the configured project and zone, passes exactly one stable or legacy
+match through `api_production_boot_disk_name`, and fails if several matches make
+the choice ambiguous. A direct Terraform caller must inspect and pass the exact
+existing name before import. Leave the override empty only when no matching disk
+exists and the stack is new. Reject an unexpected boot-disk or VM replacement;
+changing this input later is a replacement decision, not an in-place rename.
+
The Ansible and Salt adapters are dedicated-host, first-install bootstrap paths. Updating their input files does not by itself prove that the application lifecycle reran; use their explicit force-bootstrap control only in a planned maintenance window. They do not support cloud-compose-managed Vault Agent configuration.
## Rollback record
diff --git a/platform/adoption-model.mdx b/platform/adoption-model.mdx
index 85c7ad9..fa12f25 100644
--- a/platform/adoption-model.mdx
+++ b/platform/adoption-model.mdx
@@ -147,7 +147,16 @@ Managed Terraform keeps long-lived ownership narrow:
| Project | One customer project plus its complete runtime and site infrastructure inventory |
| Edge | Public hostname and edge-routing resources |
-Creation follows foundation, organization, Vault configuration, project, then edge; deletion reverses that order. A site does not receive an independent Terraform state, and one state must not manage a resource owned by another root.
+Creation follows foundation, organization, Vault configuration, project, then
+edge; deletion reverses that order. Deletion is staged and terminal: children
+must be retired before their parent, externally billed project capacity must be
+removed idempotently before the local tombstone commits, and a project is not
+marked decommissioned until its final infrastructure apply succeeds. An
+organization delete does not silently cascade through projects; every retained
+project must already be terminal and decommissioned, with billing in a terminal
+`canceled` or `incomplete_expired` state (or no subscription). A site does not
+receive an independent Terraform state, and one state
+must not manage a resource owned by another root.
The shared-router/private-PPB integration is a target architecture and is not generally released in the [current status snapshot](/infrastructure/current-release-status). The ownership boundary remains valid, but the linked router identity, canonical client-IP, and timeout details must not be treated as the current production topology until their release gate is complete.
diff --git a/platform/automation-backplane.mdx b/platform/automation-backplane.mdx
index d23f8bd..2f5197a 100644
--- a/platform/automation-backplane.mdx
+++ b/platform/automation-backplane.mdx
@@ -8,7 +8,7 @@ LibOps uses GitHub as an important collaboration surface, but the platform shoul
LibOps is building a webhook-driven automation backplane for events that need to trigger work inside the platform: dependency scanning, CI runner orchestration, deployment follow-up, and repository maintenance.
-The separate request-serving API and `api-worker` Cloud Run deployment described below is a **preview target**, not a released managed topology in the [July 13, 2026 status snapshot](/infrastructure/current-release-status). Process separation in source code or a Compose development stack does not prove that the worker service, identity migration, database path, rollout, and rollback have been promoted in production.
+The separate request-serving API and `api-worker` Cloud Run deployment described below is a **preview target**, not a released managed topology in the [July 17, 2026 status snapshot](/infrastructure/current-release-status). Process separation in source code or a Compose development stack does not prove that the worker service, identity migration, database path, rollout, and rollback have been promoted in production.
## GitHub events in LibOps
@@ -68,7 +68,7 @@ Cloud Run service and revision maxima remain one, the service minimum remains on
### Preview worker network and probe invariants
-The worker is private and has no public invoker. It uses Direct VPC **egress** with `PRIVATE_RANGES_ONLY` to reach the API production database on its private address; Cloud Run services do not gain Direct VPC ingress. The attachment stays on the network and regional subnet that route to that database, the subnet is `/26` or larger, and the VM firewall admits the whole ephemeral source subnet only on the database port and only to the database VM target. Public Vault and Google API calls continue over normal Cloud Run egress; changing to `ALL_TRAFFIC` would require a separately designed and tested NAT contract.
+The worker is private and has no public invoker. It uses Direct VPC **egress** with `PRIVATE_RANGES_ONLY` to reach the API production database on its private address; Cloud Run services and jobs do not gain Direct VPC ingress. The attachment stays on the network and regional subnet that route to that database, the subnet is `/26` or larger, and the VM firewall admits the whole ephemeral source subnet only on the database port and only to the database VM target. Public Vault and Google API calls continue over normal Cloud Run egress; changing to `ALL_TRAFFIC` would require a separately designed and tested NAT contract.
The Vault sidecar must complete its first secret render before the worker starts. The worker startup probe calls `/ready`, whose bounded startup budget covers Direct VPC attachment and database retry and then verifies the database, authenticated Vault access, and leader-or-standby coordination state. `/health` remains process-only during a transient dependency outage but fails on terminal leadership loss or drain. These rules specialize the broader [managed Direct VPC contract](/platform/security-operations#private-vm-delivery-on-google-cloud); self-hosted power-management networking has a different startup exception documented in the [cloud-compose boundary](/infrastructure/cloud-compose#google-cloud-prerequisites).
diff --git a/platform/custom-domains.mdx b/platform/custom-domains.mdx
index b642fe5..77921f2 100644
--- a/platform/custom-domains.mdx
+++ b/platform/custom-domains.mdx
@@ -6,7 +6,7 @@ description: "Prepare a public hostname after managed ingress is enabled for a L
The target LibOps managed-ingress service can serve customer-owned domains through a shared Cloudflare path. You add the hostname to the site in LibOps, then point it at the LibOps SaaS fallback hostname only after LibOps confirms that the managed ingress release is enabled for that site.
-The shared router and private site-origin path is not generally available in the [July 13, 2026 release-status snapshot](/infrastructure/current-release-status). Creating a domain record in the dashboard or API does not prove that end-to-end routing is active. Do not change production DNS until LibOps support confirms the current ingress path and fallback hostname for your site.
+The shared router and private site-origin path is not generally available in the [July 17, 2026 release-status snapshot](/infrastructure/current-release-status). Creating a domain record in the dashboard or API does not prove that end-to-end routing is active. Do not change production DNS until LibOps support confirms the current ingress path and fallback hostname for your site.
Do not point customer DNS directly at a site VM, Cloud Run service, or load balancer IP. The public hostname should enter through Cloudflare so edge rules, TLS, rate limits, and the LibOps site router stay in the request path.
diff --git a/platform/managed-platform.mdx b/platform/managed-platform.mdx
index bbfab7f..87a335b 100644
--- a/platform/managed-platform.mdx
+++ b/platform/managed-platform.mdx
@@ -45,7 +45,7 @@ Instead of asking every project to solve basic operations on its own, LibOps giv
Sites run in professionally managed Google Cloud environments.
- Each site runs on its own dedicated virtual machine.
+ Production sites use production-only runtime VMs, while non-production sites use separate runtime VMs. Each role-matched runtime has a seven-site limit.
All application settings and deployment logic live in version control.
@@ -76,7 +76,7 @@ LibOps manages the secure compute environment on Google Cloud. Your team focuses
## Runtime Isolation
-Each production workload runs on a distinct runtime host. Non-production environments for the same project can share a separate runtime host, which keeps previews and staging isolated from production without requiring a new VM for every short-lived environment.
+Production sites run only on production-role runtime hosts, while non-production environments run on separate ephemeral or custom runtime hosts. Default and role-based placement can assign up to seven compatible sites to one runtime before creating a slot-stable overflow runtime. Explicitly selecting a runtime that is already full returns a capacity error instead of silently moving the site.
diff --git a/platform/resource-hierarchy.mdx b/platform/resource-hierarchy.mdx
index 2554560..9470bfa 100644
--- a/platform/resource-hierarchy.mdx
+++ b/platform/resource-hierarchy.mdx
@@ -31,7 +31,7 @@ When access should be limited, attach it at the narrowest level that matches the
## Environments And Runtime Hosts
-A site record represents one running environment for a codebase: production, staging, review, development, or another named environment. A project can have more than one runtime host. In the managed platform, production is isolated on its own runtime, while non-production environments for the same project can share a runtime host.
+A site record represents one running environment for a codebase: production, staging, review, development, or another named environment. A project can have more than one runtime host. Production sites use production-only runtimes, while non-production sites use separate ephemeral or custom runtimes. Default and role-based placement assigns no more than seven compatible sites to one runtime and creates a slot-stable overflow runtime when the current one is full; explicitly selecting a full runtime returns a capacity error.
Terraform and cloud-compose create the runtime host, install the base operating-system dependencies, and place the initial Compose project scaffolding on the VM. They do not manage day-to-day state inside the VM. The site reconciler running on the VM handles operational state such as SSH users, allowed keys, secrets files, firewall rules, and GitHub-triggered deployments.
diff --git a/platform/security-operations.mdx b/platform/security-operations.mdx
index 570dc6a..45d5a56 100644
--- a/platform/security-operations.mdx
+++ b/platform/security-operations.mdx
@@ -10,7 +10,7 @@ For the full self-hosted checklist, including Terraform state, cloud IAM, secret
-The shared Cloud Run router and private per-site PPB path below is a **target managed-platform contract**, not a generally available release in the [July 13, 2026 release-status snapshot](/infrastructure/current-release-status). It must not be used as a production migration or DNS runbook until a later status record names the promoted image digests, publisher identities, infrastructure and API releases, and passing end-to-end canaries.
+The shared Cloud Run router and private per-site PPB path below is a **target managed-platform contract**, not a generally available release in the [July 17, 2026 release-status snapshot](/infrastructure/current-release-status). It must not be used as a production migration or DNS runbook until a later status record names the promoted image digests, publisher identities, infrastructure and API releases, and passing end-to-end canaries.
## Managed-platform defaults
@@ -23,7 +23,7 @@ The shared Cloud Run router and private per-site PPB path below is a **target ma
Managed platform secrets are stored and scoped as platform resources rather than committed to repositories or copied into unmanaged environment files.
- Each managed production site runs on a dedicated virtual machine. Non-production environments use a separate runtime boundary.
+ Production sites use production-only runtime VMs, while non-production sites use separate runtime VMs. A role-matched runtime can host at most seven active sites.
@@ -77,36 +77,178 @@ The router must authenticate to each private PPB with a Google ID token for one
## Private VM delivery on Google Cloud
-In the target path, the site PPB reaches the application's private VM address through [Direct VPC egress](https://cloud.google.com/run/docs/configuring/vpc-direct-vpc) without a Serverless VPC Access connector. The same pattern carries authenticated controller requests to the private rollout endpoint. Cloud Run services do not support Direct VPC ingress: external traffic still enters the declared Cloudflare, load-balancer, and Cloud Run path.
+In the target path, the site PPB reaches the application's private VM address through [Direct VPC egress](https://cloud.google.com/run/docs/configuring/vpc-direct-vpc) without a Serverless VPC Access connector. A separate Direct VPC attachment carries authenticated controller requests to the private rollout endpoint. Cloud Run services and jobs do not support Direct VPC ingress: external traffic still enters the declared Cloudflare, load-balancer, and Cloud Run path. Worker-pool ingress is a separate capability that this design does not use.
The target deployment sets Cloud Run egress to `PRIVATE_RANGES_ONLY`. Only traffic to private destination ranges uses the VPC attachment; other outbound traffic follows normal Cloud Run egress and does not traverse this VPC or its Cloud NAT. Changing the service to `ALL_TRAFFIC` creates a different routing, NAT, cost, and startup-latency contract and requires a separate review and hosted test.
-Direct VPC revision addresses are ephemeral, so the target VM firewall cannot safely allow individual addresses or use a Cloud Run service identity as the ingress source. It must allow the complete regional subnet CIDR on only the required application or controller ports and target the runtime VM service account. Every workload attached to that subnet is consequently inside the source trust zone.
-
-The target managed project allocates a `/20` subnet in each selected region. A self-hosted deployment must use at least `/26`, but that minimum may be too small once existing allocations, Cloud Run's `/28` allocation blocks, roughly two addresses per steady-state service instance, and overlapping revisions are counted. The service, VM, and subnet must use compatible regions, and the network MTU remains Cloud Run's default `1460`.
+Direct VPC revision addresses are ephemeral, so the target VM firewall cannot
+safely allow individual addresses or use a Cloud Run service identity as the
+ingress source. Keep the two trust zones distinct:
+
+- per-site PPB services attach to the runtime-specific serverless `/22`; that
+ whole `/22` may reach only the runtime's declared application ports, with the
+ runtime VM service account as firewall target;
+- controller-ingress attaches to `primary-`; that primary subnet may
+ reach only TCP 8080 on the same VM target, and the rollout endpoint still
+ requires the runtime-audience ID token.
+
+Every workload attached to either subnet is inside that subnet's source trust
+zone. Never authorize the primary subnet to application ports or the PPB `/22`
+to the controller port.
+
+The target managed project gives every runtime a dedicated RFC6598 `/22`. Its
+persisted project-wide slot, from 0 through 255, indexes
+`cidrsubnet("100.64.0.0/10", 12, slot)`; the CIDR therefore does not change when
+a runtime moves regions. Slots are unique within a project and are never
+recycled. The VM and controller primary `/20`s use a separate append-only
+region registry in
+`172.16.0.0/12`; migration discovers and preserves an existing conventional
+primary subnet, including a legacy `10.128.0.0/9` assignment, rather than
+renumbering it because map order changed.
+
+A runtime accepts at most seven site PPB services. With five instances per
+service, steady-state 2X IP consumption rounds each service to a `/28`
+allocation block. An old/new overlap can require two blocks per service, and a
+rapid third revision or rollback can temporarily retain a third. A `/24` has
+only 16 such blocks and can therefore exhaust at the seven-site limit. The
+managed `/22` has 64 blocks: the ordinary two-generation overlap uses at most
+14 and a retained third generation uses at most 21, leaving substantial
+headroom for rollout timing and allocation lag. This is not permission for
+unbounded rapid revision creation; stop a rollout and let retained addresses
+drain if available capacity falls unexpectedly.
+
+Terraform rejects an eighth active site on one runtime. The API returns a
+capacity error when a caller explicitly selects a full runtime; default or
+role-based placement instead creates a slot-stable overflow runtime and assigns
+the new site there. Upgrade migrations also preflight the existing database and
+stop before infrastructure changes if a non-deleted runtime already owns more
+than seven non-deleted sites. Correct that inventory with an application-aware
+move or retire genuinely deleted sites; changing only a foreign key does not
+move application data or traffic. Every assigned active site must also have a
+unique TCP port in the range `1` through `65535` within its runtime.
+
+A self-hosted deployment must use at least `/26`, but that minimum may be too
+small once existing allocations, Cloud Run's `/28` allocation blocks, roughly
+two addresses per steady-state service instance, and overlapping revisions are
+counted. The service, VM, and subnet must use compatible regions, and the
+network MTU remains Cloud Run's default `1460`.
Cloud Run can retain revision addresses for up to 20 minutes after scale-down. After detaching or deleting the last Cloud Run user, allow one to two hours before deleting the subnet; reserved serverless addresses cannot be removed manually.
+The target control plane persists each runtime's region and immutable slot and
+never recycles that network identity. A successful first-phase apply that
+removes every Cloud Run consumer records the detach time; a failed or targeted
+apply does not start the clock. A later complete apply may delete the subnet
+only after the persisted detach is at least two hours old. An active runtime
+with no sites can recreate the same subnet and slot later. Only an explicitly
+deleted runtime records terminal destruction, and that identity cannot be
+reattached. Project deletion uses the same detach, wait, and destroy phases and
+does not decommission the owning project until every runtime subnet has reached
+terminal destruction.
+
+Deletion is a terminal state transition, not an ordinary update. The API and
+retained database guards reject reactivation of a deleted organization,
+project, runtime, or site and reject creating a live child beneath a terminal
+parent. A project cannot be tombstoned while it still owns an active site. When
+the project affects a paid subscription, the Stripe item change and a
+per-project retirement marker must complete idempotently before the local
+tombstone and outbox event commit; a Stripe error fails closed. The final
+successful `decommission=true` project apply records
+`infrastructure_decommissioned_at`.
+
+Organization deletion deliberately does not cascade through projects. It is
+rejected until there are no active projects, every retained deleted project has
+that durable infrastructure-decommissioned timestamp, and the local Stripe
+subscription is absent, canceled, or `incomplete_expired`. Only then may the
+organization's reverse dependency graph remove edge, Vault, organization, and
+foundation state. These preconditions let retries resume a known phase instead
+of skipping the Direct VPC grace period, abandoning billable resources, or
+resurrecting a terminal ancestor.
+
### Cold starts and timeout ownership
-Direct VPC attachment can delay new connections for a minute or more. An ordinary Cloud Run service should use a bounded, retrying startup probe against a representative egress destination before accepting traffic. In the target design, PPB cannot use the sleeping application VM as that probe target: PPB must accept the first request in order to start the VM, so requiring the VM to be reachable at startup would deadlock the wake path.
+Direct VPC attachment can delay new connections for a minute or more. An ordinary Cloud Run service should use a bounded, retrying startup probe against a representative egress destination before accepting traffic. PPB is a deliberate exception: it cannot probe the sleeping application VM because it must accept a request before it can start that VM. Its startup probe is therefore process-only, while release smoke tests prove the real Direct VPC connection and wake path.
+
+Two timeout contracts apply, and they must not be collapsed into one chain. The direct public Cloud Run PPB endpoint can hold one request through the complete tested wake budget:
+
+```text
+PPB power-on budget 240 seconds
+PPB backend-dial budget 300 seconds
+Cloud Run request ceiling 600 seconds
+
+240 + 300 < 600
+```
-The target managed exception uses bounded retries inside that first request. Its contract allocates PPB 45 seconds to power on the VM and 35 seconds to establish the backend connection. PPB must not replay an application request after a connection succeeds. If a controlled failure occurs before dispatch, it may return `503` with `Retry-After`; the client decides whether its operation is safe to retry. Clients must also tolerate occasional connection resets during Cloud Run or network maintenance.
+PPB does not replay an application request after a connection succeeds. The hosted direct-path smoke test sends exactly one request, proves that the VM reaches `RUNNING`, follows any changed ephemeral VM address, and verifies the application through the host after the response.
-Every outer deadline leaves room for the inner operation to finish:
+The non-Enterprise Cloudflare path cannot contain that 540-second worst case. Its separate outer budgets are:
```text
-PPB power on + backend dial 45 + 35 < 100 seconds
-Router lookup + origin proxy 5 + 100 < 110 seconds
-Router Cloud Run request 110 < 120 seconds
-Cloudflare non-Enterprise read ceiling 120 seconds
+Bounded route lookup 5 seconds
+Router origin request 100 seconds
+Router Cloud Run request 110 seconds
+Cloudflare non-Enterprise read limit 120 seconds
+
+5 + 100 < 110 < 120
```
-The target chain is summarized as `45 + 35 < 100 < 110 < 120`, with the separate five-second route lookup also fitting inside the 110-second router request. Its Terraform contract must reject equality or inversion at any boundary. Do not raise the outer value above 120 unless the Cloudflare zone has an Enterprise timeout and every inner deadline and test is changed as one contract.
+The router's bounded route lookup must also fit before its origin deadline. On a long cold start, the managed path must return a controlled, retryable failure before application dispatch while wake-up work can continue; it must not claim that the first Cloudflare request always reaches the application. A response such as `503` with `Retry-After` lets the client decide whether retrying its operation is safe. The router and PPB must never replay a request whose application dispatch might have started. Clients must also tolerate occasional connection resets during Cloud Run or network maintenance.
+
+Terraform must reject equality or inversion within each timeout contract. Do not raise the managed outer value above 120 unless the Cloudflare zone has an Enterprise timeout and every router deadline, failure contract, and hosted test is changed together.
## Vault-backed secrets
-The managed platform can scope secrets at organization, project, and site level. Runtime delivery keeps the values outside the site repository.
+The target managed platform can scope secrets at organization, project, and
+site level. Runtime delivery keeps the values outside the site repository.
+
+### Target organization Vault runtime boundary
+
+
+This is the promotion contract, not a claim that the aggregate managed Vault
+set is released. The [current release status](/infrastructure/current-release-status)
+must name all three image digests, verified signatures, sitectl-admin resolver,
+and green API/Terraform gates before operators treat this path as available.
+
+
+The organization Vault service is a three-image contract with independent
+ownership and release cadence:
+
+| Image | Owner and purpose | Promotion rule |
+| --- | --- | --- |
+| `vault-server` | The `terraform-vault-cloudrun` repository's Cloud Run-compatible Vault server image | Rebuild the exact verified upstream Vault release with the patched toolchain, publish from protected main to GHCR and public GAR, then pin its public GAR digest |
+| `vault-init` | The independent one-shot initializer that seals initialization material into GCS with Cloud KMS | Release independently, publish the reviewed manifest to GHCR and public GAR, and pin the selected semantic release by GAR digest |
+| `vault-proxy` | The independent request boundary that separates explicit public Vault paths from administrator-only paths | Release independently, publish to GHCR and public GAR, and pin its GAR digest separately from both server and initializer |
+
+Do not substitute the API repository's private `api-vault-agent` helper for
+either the Vault server or `vault-init`, and do not advance all three images
+under one shared tag. `sitectl-admin` resolves
+the independently selected initializer and proxy release tags to immutable
+digests before Terraform runs. It follows annotated GitHub tags to the final
+release commit and requires the Cosign caller repository, tag ref, exact
+workflow SHA, and caller-workflow annotation in addition to the pinned shared
+publisher identity. Reconciliation must fail closed when any of the three
+digest-pinned inputs is missing.
+
+The proxy's public route list is an allowlist, not a string prefix list. A
+route is an exact path, a single `*` path segment, or one final `/**` subtree;
+trailing-slash prefixes, partial-segment wildcards, dot segments, and ambiguous
+patterns are rejected. Everything else requires a verified administrator
+identity. The proxy removes its administrator credential and caller-supplied
+forwarding headers before sending a request to Vault, preserves the
+application's Vault authorization header, and returns generic authentication
+or upstream errors without logging bearer-token URLs.
+
+The initializer is a one-shot state transition. Before asking Vault to
+initialize, it verifies that the destination objects are absent and that its
+KMS encrypt/decrypt path works. It also commits and reads back a unique,
+non-secret preflight marker through the same GCS path before making the
+irreversible request; the marker remains because the runtime deliberately has
+no object-delete permission. It uses create-only object writes, retains the
+Vault response until the complete encrypted bundle is durable, and exits
+nonzero on missing, partial, ambiguous, or conflicting state. A response lost
+after Vault commits cannot be reconstructed; the safe behavior is to detect
+that ambiguity, stop, and require recovery rather than overwrite evidence or
+silently report success.
Self-hosted deployments choose and operate their own secret system. GCP deployments can integrate with a consumer-configured Vault GCP IAM role. DigitalOcean, Linode, and on-premises deployments require a consumer-managed authentication method. In every environment:
@@ -145,7 +287,7 @@ Self-hosted teams must define the equivalent separation across the cloud account
## Runtime and data isolation
-Managed production workloads run on distinct hosts. That creates a clear compute boundary, but isolation does not replace application security, dependency updates, least privilege, monitoring, or backups.
+Managed production sites run only on production-role runtime hosts, with at most seven active sites sharing one runtime; non-production sites use separate runtime hosts. That creates a clear production/non-production compute boundary, but isolation does not replace application security, dependency updates, least privilege, monitoring, or backups.
Self-hosted deployments should keep these concerns separate:
diff --git a/templates/compose-projects.mdx b/templates/compose-projects.mdx
index 0edc84b..98eaf53 100644
--- a/templates/compose-projects.mdx
+++ b/templates/compose-projects.mdx
@@ -33,6 +33,46 @@ See [Application Image and Template Contract](/templates/app-support) for the im
The create flow clones the standalone template repository. Plugin repositories carry the template contract and application behavior, not an embedded copy of the Compose project.
+## Released compatibility baseline
+
+Cloud-compose [`1.5.0`](https://github.com/libops/cloud-compose/releases/tag/1.5.0)
+records the following known-good self-hosted preset. Every row uses
+core [`sitectl` `v1.0.0`](https://github.com/libops/sitectl/releases/tag/v1.0.0),
+the matching application-plugin `v1.0.0`, and its recorded template release.
+The base-image identity shown here is the one declared by that template
+release. Inspect the template's Compose file for its other direct image pins
+and declared component-controlled image selectors.
+
+| Application and template | Application plugin package | Template base image |
+| --- | --- | --- |
+| [ArchivesSpace `v1.0.0`](https://github.com/libops/archivesspace/releases/tag/v1.0.0) | [`sitectl-archivesspace` `v1.0.0`](https://github.com/libops/sitectl-archivesspace/releases/tag/v1.0.0) | `libops/archivesspace:4.2.0@sha256:4082d760cac3fa1c73ee74abf16f83e30560f55a91c5eed8770d365b7df411d3` |
+| [Drupal `v1.0.0`](https://github.com/libops/drupal/releases/tag/v1.0.0) | [`sitectl-drupal` `v1.0.0`](https://github.com/libops/sitectl-drupal/releases/tag/v1.0.0) | `libops/drupal:nginx-1.30.3-php84@sha256:be9c3be9728a06c291bb812d6b0c4b014b207dad47d745c9f1d1ee3d9771b9aa` |
+| [Islandora `v1.1.0`](https://github.com/libops/isle/tree/v1.1.0) | [`sitectl-drupal` `v1.0.0`](https://github.com/libops/sitectl-drupal/releases/tag/v1.0.0) and [`sitectl-isle` `v1.0.0`](https://github.com/libops/sitectl-isle/releases/tag/v1.0.0) | `libops/islandora:nginx-1.30.3-php84@sha256:e9235dc3789d6ba8885849c80888b755d9e60c316e5de8d88bcd5e9d214ecd42` |
+| [Open Journal Systems `v1.0.0`](https://github.com/libops/ojs/releases/tag/v1.0.0) | [`sitectl-ojs` `v1.0.0`](https://github.com/libops/sitectl-ojs/releases/tag/v1.0.0) | `libops/ojs:3.5.0-5-php84@sha256:b97670708fdec9cc63b7c5ba9c96b3db547f3ea3e1658ef01bdeba6400aec090` |
+| [Omeka Classic `v1.0.0`](https://github.com/libops/omeka-classic/releases/tag/v1.0.0) | [`sitectl-omeka-classic` `v1.0.0`](https://github.com/libops/sitectl-omeka-classic/releases/tag/v1.0.0) | `libops/omeka-classic:3.2.1-php84@sha256:b668d895e0334c5367991994f0f4e2165fb932a31b89057721c5669d941965aa` |
+| [Omeka S `v1.0.0`](https://github.com/libops/omeka-s/releases/tag/v1.0.0) | [`sitectl-omeka-s` `v1.0.0`](https://github.com/libops/sitectl-omeka-s/releases/tag/v1.0.0) | `libops/omeka-s:4.2.1-php84@sha256:3c9d1a0558b705235104998d260dfe916b156fed21b8c34ca0aa5e9bd33bfbee` |
+| [WordPress `v1.0.0`](https://github.com/libops/wp/releases/tag/v1.0.0) | [`sitectl-wp` `v1.0.0`](https://github.com/libops/sitectl-wp/releases/tag/v1.0.0) | `libops/wp:nginx-1.30.3-php84@sha256:f98f7c1efcb7a424e5f3f5daedfefa23406de42222aaa6c0c4aacfe7875e5922` |
+
+The ISLE preset uses template `v1.1.0` with core `sitectl`, `sitectl-drupal`,
+and `sitectl-isle` at `v1.0.0`. It also supplies `ISLANDORA_TAG=6.3.19` through
+the template catalog so the MergePDF service has its minimum compatible
+upstream image selector. An explicit `runtime.extra_env.ISLANDORA_TAG` value
+wins over that default; record and test any such override as part of the
+deployment compatibility set.
+
+ArchivesSpace, OJS, Omeka Classic, and Omeka S base tags name the bundled
+upstream application release. Drupal, Islandora, and WordPress tags name the
+runtime because their Composer lockfiles in the template own the application
+tree. The tag communicates intent; the digest fixes the bytes.
+
+This baseline proves the reviewed template/package selection and the hosted
+release gates attached to cloud-compose `1.5.0`. It does not claim that every
+application/provider pairing ran as one Cartesian test matrix. Preserve a
+deployment-specific record of the exact template commit, all direct image
+digests, provider lockfile, and the smoke or upgrade path your site actually
+ran. The managed-platform [`sitectl-libops` `v1.3.0`](https://github.com/libops/sitectl-libops/releases/tag/v1.3.0)
+plugin is intentionally outside these self-hosted application presets.
+
## Islandora components
Islandora has architectural choices that do not belong in separate lifecycle plugins:
diff --git a/templates/downstream-forks.mdx b/templates/downstream-forks.mdx
index 7b35bb8..fbcbb48 100644
--- a/templates/downstream-forks.mdx
+++ b/templates/downstream-forks.mdx
@@ -7,6 +7,24 @@ Once an institution forks a LibOps template, that repository becomes the site's
The goal is a fork that remains understandable: local code is explicit, generated state is excluded, production changes are reviewable, and upstream improvements arrive as pull requests instead of server edits.
+## Preserve the creation record
+
+When `sitectl` creates a project, it removes the template's Git history,
+initializes the downstream repository, and writes
+`.libops/template.lock.yaml`. Commit that generated lock with the fork. Its
+versioned envelope records the source template repository and full commit, the
+core and plugin build identities when available, the declared
+component-defaults revision, and the digest of the template contract.
+
+The lock is provenance, not an attestation or an automatic update channel. A
+source template must not ship its own downstream lock, and a fork must not edit
+the lock to make an unreviewed source appear trusted. Use it when comparing a
+future template release and keep the reviewed downstream merge commit as the
+actual adoption record. Component operations may consult the recorded defaults
+revision, but they still change only fields owned by the component contract;
+`sitectl converge` repairs drift in those fields and does not merge a template
+release or overwrite unrelated institutional policy.
+
## What belongs in the fork
| Concern | Supported location |
diff --git a/templates/islandora.mdx b/templates/islandora.mdx
index 1c50a75..c9b60cd 100644
--- a/templates/islandora.mdx
+++ b/templates/islandora.mdx
@@ -48,6 +48,44 @@ Triplet is managed by the ISLE plugin's IIIF components. It is not a separate si
The detailed choices and current flags live in the [ISLE plugin documentation](https://sitectl.libops.io/plugins/isle) and [component guide](https://sitectl.libops.io/components).
+The independently released v1 set pairs
+[`libops/isle` `v1.1.0`](https://github.com/libops/isle/tree/v1.1.0) with core
+`sitectl`, `sitectl-drupal`, and
+[`sitectl-isle`](https://github.com/libops/sitectl-isle/releases/tag/v1.0.0)
+at `v1.0.0`. The template hosted smoke is green, and the ISLE plugin is
+published as GitHub binary archives, a Homebrew formula, and Debian and RPM
+packages. Cloud-compose `1.5.0` records this set as its released ISLE preset
+and supplies `ISLANDORA_TAG=6.3.19` as the reviewed default for
+component-controlled upstream images. A downstream override remains explicit
+and must be included in that deployment's compatibility record.
+
+## Cross-domain feature bundles
+
+Some Islandora features cannot be represented by one Compose service or one
+Drupal module. The ISLE v1 component model treats them as feature bundles: one
+reviewable unit can own a bounded set of Compose fields, Composer requirements,
+and Drupal configuration-sync files. This lets an existing downstream install
+adopt a reusable feature without replacing its whole template.
+
+The first bundles are:
+
+| Bundle | Owned contract | Compatibility preflight |
+| --- | --- | --- |
+| MergePDF | The `mergepdf` Compose service, its required certificate and JWT secrets, the shared Islandora image tag, and the paged-content aggregated-PDF Drupal action | An upstream `islandora/alpaca` image must be `6.3.19` or newer; the plugin also recognizes the reviewed compatible LibOps Alpaca image contract |
+| hOCR search | The two Composer packages, module registration, Solr search configuration, derivative action, IIIF manifest fields, and hOCR search view | An upstream `islandora/solr` image must be `4.2.1` or newer; the plugin also recognizes the reviewed compatible LibOps Solr image contract |
+
+The matching v1 template contract declares both bundles enabled by default.
+That default is creation metadata, not permission to change an existing fork
+without review.
+
+Enabling a bundle preflights all required files, services, secrets, image
+compatibility, and site-specific inputs before writing any file. Disabling it
+removes only fields and files in that bundle's ownership contract; retained
+derivatives or indexed data remain an operator recovery and retention decision.
+After either transition, inspect and commit the complete Compose, Composer, and
+Drupal configuration diff. A bundle does not merge an upstream template or
+overwrite unrelated institutional policy.
+
## Choose an operating path