Commit Graph

2180 Commits

Author SHA1 Message Date
Anso c43d589031 docs: add private registry fleet delivery tutorial
Walks through storing one GHCR credential on the hub, deploying a private image to a proxy node and a Pilot node that hold no credentials, reading the seal pin, and placing the image with a Git-managed Blueprint through the registry preflight.
2026-10-24 22:11:34 -04:00
Anso 4f013338d1 docs: add provider hook tutorial for GitHub and GitLab pushes 2026-10-24 20:42:49 -04:00
Anso fad509a6c5 docs: add repository secrets tutorial with SOPS and age
Walks a Postgres stack from a plaintext env file in Git to an encrypted,
policy-enforced, deployed one: generate an identity, add its recipient plus
a recovery key, encrypt the env file, require SOPS encryption, deploy, and
back up the identity. Links the feature page and the Git source tutorial both
ways.
2026-10-24 17:05:20 -04:00
Anso f22d72c5d4 docs: add GitOps portfolio sweep tutorial
Walks the portfolio top to bottom: read the verdict, clear the attention
queue in place with Retry, Resume, and Review update, open an application,
and filter the list. Adds the page to the Deploy and automate group and
links it from the portfolio feature page.
2026-10-23 21:21:04 -04:00
Anso 1cd62ddc9b docs: add Git-managed Blueprint rollout tutorial
Walks through converting a Blueprint onto a Git source, approving placement, authorizing a rollout, letting a failing health check pause it, rolling the node back, and shipping the fix. Adds 17 screenshots, a nav entry in the Deploy & automate group, and cross-links from the two neighbouring tutorials.
2026-10-23 19:24:35 -04:00
Anso d07cfc34d1 docs: v1 docs refresh (batch 10) (#2182)
* docs(tutorials): refresh the create and approve a Blueprint walkthrough

Walk the 0.98.0 Blueprint editor, rollout preview, and detail sheet end to end and update the steps to match: the empty-catalog button, the staged label tags, the GitOps authority and evidence block, the approval state shown in the sheet, the audit rows each action leaves, and the real port-conflict recovery. Permissions are described as the stack and node permissions they are, and all seven screenshots are retaken.

* docs(tutorials): refresh the connect a Git source walkthrough

Walk the 0.98.0 Git Source sheet from an unlinked stack to an applied commit and update the steps to match: the toolbar and Anatomy entry points, the Custom CA field, the four apply behaviors, the first-pull Add row in the change plan, the Accepted answer and stage row, and the exact ref error. Add the rule that a linked stack must be detached before its repository or ref changes. All six screenshots are retaken.

* docs(tutorials): refresh the catch and fix drift walkthrough

Walk a real image drift through detection, re-check, redeploy, and dismiss on 0.98.0 and update the text to match: the redeploy confirmation's consequence label, the health gate entry on the Activity timeline, and the Dismiss toast with Undo. Dismissing and restoring a finding is noted as needing the same stack permission as redeploying. All seven screenshots are retaken.

* docs(tutorials): refresh the move a Blueprint to a new node walkthrough

Walk a real migration between a retiring proxy node and the hub on 0.98.0 and update the tutorial to match: the Placement control tab, the toasts on pin and cordon, Cordon in the node details sheet, the Needs confirmation status block before the rollout is confirmed, the transient Completion unknown state after it, and the audit rows including the stack the apply creates. All eleven screenshots are retaken.

* docs(tutorials): refresh the missing required variable guardrail walkthrough

Walk the guardrail on 0.98.0 from a clean deploy to a refused Save & Deploy and Update and back to a healthy update, and update the steps to match: the toast and the Update failed chip that carry the message, the readiness dialog wording, the Environment tab's missing count, the Deploy Guardrails section as it reads now, and the activity entries. Adds the screenshots for the blocked Save & Deploy and the failure chip, and retakes the rest.

* docs(tutorials): refresh the deploy from CI webhook walkthrough

Run the webhook end to end on 0.98.0, including a signed trigger and the newline-signed request the troubleshooting section describes, and update the text to match: the Action list, including Git source sync with a pointer to the Webhooks page, the new form, callout, and Recent executions states, and the uptime and log wording on the redeployed stack. All five screenshots are retaken with the trigger URL and secret replaced by generic values.

* docs(tutorials): refresh the enroll a remote node walkthrough

Enroll a real Pilot agent on 0.98.0 and update the steps to match: the Pilot enrollment dialog and its Done button, the tunnel states after the agent starts, the switcher and Fleet cards beside the nodes already enrolled, the pencil that opens Edit node, and the hub-only view list, which now includes GitOps. The example compose directory is changed to a dedicated media path, and all six screenshots are retaken with host addresses, seal pins, and the token replaced by generic values.

* docs(tutorials): refresh the Custom OIDC single sign-on walkthrough

Run the whole flow on 0.98.0 against a real Keycloak 26 over a private CA and update the text to match: the login button is labelled with the Display Name rather than a fixed phrase, the Active badge and header counts after Save, the stored-secret state after Save, the Log Out path, what happens when the browser is already signed in at the identity provider, and the full list of fields to leave blank. All six screenshots are retaken with the issuer shown as a generic host.

* docs(tutorials): refresh the auto-patch with a label walkthrough

Run the label-driven update schedule twice on 0.98.0 and update the text to match: the Labels submenu states, the Current matches panel that lists every node, the run time and per-node details in the Execution history, the second "all images up to date" run, and the node's Updates chip as the proof the pending update cleared. The Update board example is replaced because other stacks on a real fleet keep it from ever being empty. All eight screenshots are retaken.
2026-10-23 18:29:04 -04:00
Anso 26f77cac96 docs: v1 docs refresh (batch 9) (#2181)
* docs(gitops): complete the overview's source states, placement limits, and control locations

List every source state the app can show, with the order in which the most
serious one wins. Add the pin-driven and unreadable-evidence cases to what
the placement policy hands back to an operator, and name the controls where
each policy is set.

* docs(git-sources): document the polling controls, Manual behavior, and pending-update indicators

State the per-node polling interval and its range, that a Manual source is
never polled or retried but still stages a plan when a webhook delivers a
commit, and where the pending-update icon and button lead. Add common setups
and what Git sources do not do, and correct the create form's alt text.

* docs(git-authentication): cover the Git error cases and current Bitbucket credentials

Add the failures a Git source can report that were missing from
troubleshooting (embedded credentials, SSH without a deploy key, blocked
hosts, rate limits, certificate expiry and hostname mismatch, network
errors), state that the token is only given to the repository's host, and
replace the deprecated Bitbucket app password guidance.

* docs(git-webhooks): state the GitHub content type, remote-node failures, and limits

A form-encoded GitHub delivery fails verification and answers 404, so say to
set the content type to JSON. Add the 500 a delivery gets when the owning
node is unreachable, correct how the hub forwards to a remote node, and list
what provider hooks do not cover.

* docs(git-secrets): document linking an encrypted repository and where encrypted values work

A repository that is already encrypted cannot be linked, because the identity
belongs to a linked source. Encrypted values belong in env files and
file-backed secrets, not in image references or published ports, which
Sencho resolves before decrypting.

* docs(blueprints): correct permissions, remote deploys, and roadmap wording

Describe the permissions Blueprint actions need instead of an admin-only
rule, say that a remote node runs the deploy itself with its own policy and
scanner state, and list all three drift notifications. Remove the repeated
marker, policy gate, and snapshot explanations, the raw apply request
format, and statements about future features.

* docs(git-managed-blueprints): add who can take each step and the limits

List the permission each authority step and rollout control needs, say that
webhooks do not fetch a Blueprint's source so Pull source and polling bring in
new commits, and gather the constraints that were scattered through the page.

* docs(gitops-portfolio): name the row menu actions and state what the portfolio does not do

List the exact row menu items for Direct and Blueprint applications, and
add the limits: hub only, navigation from row menus, and how unreachable
nodes appear.

* docs(gitops-drift): describe the drift row as it appears and add a screenshot

The row shows the class, the deriving component, evidence age, reason, and
expected against observed; the resolving action lives on the application's
status. Name the retry-interval setting, drop rollout states Sencho never
reports, and replace a hedge about which classes are reported.

* docs(git-transport): refresh the live attestations against the 0.98.0 release

Re-run all 21 live attestations (hosted public repositories, the direct
proxy and Pilot node paths, and disposable Gitea and Forgejo instances over
HTTPS with a private CA and over SSH with a deploy key) against the 0.98.0
image, record the new date, commit, and image digest, and regenerate the
matrix page. Describe unlisted hosts without claiming more than was tested,
and add the procedure the attestation record refers to.
2026-10-23 12:51:36 -04:00
sencho-quartermaster[bot] 27495c56b4 chore(main): release 0.98.0 (#1812)
* chore(main): release 0.98.0

* chore: credit external contributors in changelog

---------

Co-authored-by: sencho-quartermaster[bot] <275163604+sencho-quartermaster[bot]@users.noreply.github.com>
v0.98.0
2026-10-23 11:40:14 -04:00
dependabot[bot] 737b0e1779 chore(deps): bump the all-npm-backend group in /backend with 7 updates (#2162)
Bumps the all-npm-backend group in /backend with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [undici](https://github.com/nodejs/undici) | `8.11.2` | `8.12.2` |
| [zod](https://github.com/colinhacks/zod) | `4.6.6` | `4.6.7` |
| [eslint](https://github.com/eslint/eslint) | `10.12.0` | `10.13.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.71.1` | `8.71.2` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.5` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1149.0` | `3.1152.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1149.0` | `3.1152.0` |


Updates `undici` from 8.11.2 to 8.12.2
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v8.11.2...v8.12.2)

Updates `zod` from 4.6.6 to 4.6.7
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.6.6...v4.6.7)

Updates `eslint` from 10.12.0 to 10.13.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.12.0...v10.13.0)

Updates `typescript-eslint` from 8.71.1 to 8.71.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.2/packages/typescript-eslint)

Updates `vitest` from 5.0.3 to 5.0.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.5/packages/vitest)

Updates `@aws-sdk/client-ecr` from 3.1149.0 to 3.1152.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1152.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1149.0 to 3.1152.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1152.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.12.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: zod
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: eslint
  dependency-version: 10.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: typescript-eslint
  dependency-version: 8.71.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: vitest
  dependency-version: 5.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1152.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1152.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-23 11:06:17 -04:00
Anso 75fd4b48f1 docs(security): cover the trusted proxy setting, hub-only tokens and audited GitOps actions (#2180)
The security reference now says per-IP limits use the forwarded client address
only from listed proxies and adds the proxy setting to the hardening checklist.
The configuration page states the consequence for sign-in when the proxy is not
listed, the API token page says token management is never forwarded to a remote
node, and the audit log page lists GitOps decisions and finding dismissals.
2026-10-23 11:00:49 -04:00
Anso b5a2f3aab6 docs(settings): match the Appearance, Stacks and update-check controls to the UI (#2179)
The settings reference now names the controls as the Appearance section shows
them (sidebar and Anatomy panel width, quick links, navigation alignment, the
reset buttons, and what a new account starts with), lists the two rollback
retention settings under Deploy Guardrails, describes the image update check
schedule as the single field it is, and covers the session policy under Users.
The Channels table names the Edit Payload toggle, and Mute Rules says what
deleting a stack does to its rules.
2026-10-23 11:00:19 -04:00
Anso 77afe18832 docs(stacks): give the service URL pencil its own section (#2178)
The paragraph on the Service URL pencil ran to about 3,000 characters inside
Controlling a single service. It now sits under its own heading, with the
conditions that refuse setting or clearing the URL as two short lists. The
content is unchanged.
2026-10-23 11:00:00 -04:00
Anso beeb1b477f docs(fleet): document the GitOps card chip and list every node capability (#2177)
The Fleet node card carries a GitOps chip when applications on the node need
attention, and Node details carries Cordon and Uncordon in its toolbar. The
Node Compatibility capability table now lists all advertised capabilities, and
the Map failure example uses the reasons the tab actually shows.
2026-10-23 10:59:25 -04:00
Anso ebb6d5e4c8 docs(recovery): name what refuses a recovery capture and how to fix each cause (#2176)
Deploys, updates and Git applies stop with "Rollback coverage is unavailable"
when a recovery generation cannot be captured safely. The Atomic Deployments
page now lists which inputs refuse a capture and which are recorded as not
captured, with each message and its fix, and Troubleshooting and Recovery point
to it. The health-gate page names the failure label shown in the recovery
panel, and Recovery describes what Rollback restores today.
2026-10-23 10:59:03 -04:00
Anso 68145590d5 chore(release): bump the OpenAPI version with each release (#2175)
docs/openapi.yaml printed 0.25.3 in the API reference. Annotate info.version
for release-please and register the file as an extra file so the Release PR
keeps it in step with the package version.
2026-10-23 09:58:11 -04:00
Anso 1b4a343435 ci: validate the docs build and links on pull requests (#2174)
A docs page that Mintlify cannot parse is dropped from the site while every
other page keeps serving, so a broken page can ship unnoticed (one stayed
live for weeks). The new Docs Check workflow runs mint validate and mint
broken-links on pull requests that touch docs. The CLI version is pinned,
and the job has read-only permissions.
2026-10-23 08:10:55 -04:00
Anso 99ca3664a5 docs: describe current behavior without contrast to the past (#2173)
Six sentences across the Settings reference, Deep links, Fleet Sync,
Sidebar and the environment guardrails tutorial described behavior as a
change ("no longer", "legacy", "now properly appears", "was removed").
They now state how the feature works today. No meaning changes.
2026-10-23 08:10:22 -04:00
Anso a5d9d4d52a docs(upgrade): list the current migrations and the reverse proxy requirement (#2172)
The Automatic migrations list now includes the GitOps history retention
window and the first-sign-in copy of explicit appearance and navigation
choices to the account. A short Behind a reverse proxy section points at
SENCHO_TRUSTED_PROXY_CIDRS and the shared sign-in limit that applies
without it. The pinned-version example now uses 0.98.0.
2026-10-22 20:45:32 -04:00
Anso adb0d8a522 fix(docs): repair broken anchors and missing images (#2171)
Two links pointed at headings that do not exist (per-stack alert rules and
the Fleet Sync control anchor); both now use the real heading ids.

The Overview page and the environment guardrails tutorial referenced two
screenshots that are not in the docs. The Overview page reuses the Security
screenshot already shipped on the introduction page, and the tutorial drops
the single frame, since the step text already quotes the message.

The Resources page named its Rollback tab but never described it; it now has
a short section that links to the full explanation. A stray file left over
from the organization migration is deleted.
2026-10-22 20:43:30 -04:00
Anso 9f480e133b chore(whats-new): replace the feed with the 0.98.0 entries (#2170)
A new release now opens on a fresh What's New list instead of appending to
the previous one. The feed holds the 13 entries that shipped in 0.98, with
screenshots for the seven most visible changes, and drops the 0.97 entries,
the RBAC entry from 0.97.0, and three smaller 0.98 items that their feature
pages already cover.

Entry ids are prefixed with the release (v0-98-...), and an authoring check
fails when ids from more than one release share the file. That keeps a
leftover entry from shipping and guarantees the new last id differs from any
id a user has already seen, so everyone gets the unseen indicator once while
fresh installs stay quiet.

The eight screenshots used by removed entries are deleted. The new ones are
element crops with host names, node names and repository paths replaced.
2026-10-22 20:12:48 -04:00
Anso 5b41c0512f docs: reorganize GitOps documentation into one section (#2167)
* docs: reorganize GitOps documentation into one section

Add a GitOps overview that defines the model once (Direct sources versus
Blueprints, the chain from commit to container, the three authority
steps, the policies, source states, executable identity). Split the
oversized Git Sources and Blueprints pages into focused pages for Git
authentication, provider and CI webhooks, repository secrets, and
Git-managed Blueprints, and trim the remaining pages to their own slice.

Group the GitOps pages in the navigation and split the old "Deploy and
automate" group into "Deploy and recover" and "Automate". Existing page
slugs are unchanged. Correct the provider hook labels to match the app,
and repoint What's New links to the new Git-managed Blueprints page.

* docs: add current GitOps screenshots and correct UI labels

Retake the Git Sources, Blueprints, and GitOps portfolio screenshots on
the integration image, and add screenshots to the new GitOps pages
(answer, path, and evidence block; SSH deploy key form; provider hooks;
Secrets tab; Convert to Git, placement review, and rollout controls).

Correct labels to match the app: Deploy key (SSH), the provider hook
event scopes, and the encrypted source policy buttons. The change plan
dialog shows the stack name, not the commit.

* docs: map the GitOps policy names once in the overview

Evidence, the policy controls, and the Git source form name the same
settings differently. State the mapping in one place.
2026-10-22 19:20:47 -04:00
Anso 3f8a678a27 fix(docs): use MDX comment markers on the Git transport support page (#2166)
The generated block on the Git transport support page was fenced with
HTML comments, which MDX cannot parse, so the docs build dropped the
whole page and it returned 404 on the live site. Fence it with MDX
comments instead, in both the renderer and the page, and regenerate.
2026-10-22 19:06:01 -04:00
Anso 77026fc84f fix(deps): bump dompurify to 3.4.17 (#2168)
Two advisories (GHSA-cg2g-xh64-v4q7, critical, and GHSA-mrv2-3pqr-j5jr,
high) affect dompurify up to 3.4.16 and fail the dependency audit gate.
dompurify reaches the frontend only through monaco-editor, which pins
3.4.15, so raise the existing override to ^3.4.17 and refresh the
lockfile.
2026-10-22 18:48:54 -04:00
Anso f967a5966c fix(gitops): health-gate a Blueprint rollout under its deploy stack (#2165)
A rollout reserved its health run under the retained source stack name
until the target acked its first Blueprint rollout. The apply writes the
Blueprint's own stack, so a remote leaf answered 404 for the source stack
and a local gate judged the rollout against the wrong workload.

The run now resolves its stack from the intent that placed the deploy,
and the rollback restore resolves it from the target intent then the
application intent. The source stack is no longer a fallback for either.
2026-10-22 14:21:42 -04:00
Anso 646f60f668 fix(recovery): deploy stacks whose inputs a recovery generation cannot hold (#2148)
* fix(recovery): deploy stacks whose inputs a recovery generation cannot hold

Capturing a recovery generation refused every stack with an include or
extends that leaves the stack folder, any path that holds a variable
(env_file, volume source, include), and any managed file that is a link
out of the stack. Those stacks deployed before recovery generations
existed, so deploy, update, Save & Deploy, scheduled updates and Git apply
all failed for them, and the error called a relative path "host-absolute".

Only integrity problems refuse a capture now: no readable compose file, a
compose file that links outside the stack, case-colliding paths, an
unreadable Mesh setting, and the existing Git manifest refusals. Everything
else is recorded as not captured. The generation stores the list, one log
line names it with the generation id, and Rollback readiness shows Managed
inputs as a warning that lists what a rollback will take from the live
stack. A restore never deletes those paths, and a generation with inputs
left out no longer deletes files it cannot attribute to the stack.

Also fixed in the same capture path:
- A link on a parent directory (config -> ../shared) slipped past the
  inventory and made capture throw SYMLINK_ESCAPE.
- A compose directory that is itself a link made capture fail for every
  file of every stack. Capture and the inventory now compare against the
  real compose directory plus the stack name.
- Coverage refusals that carried a permission or parse error were
  classified by that cause instead of as a coverage refusal.
- Credentials in an include URL are redacted from the notes.

A compose file that is a link out of its stack is still refused, so one
stack cannot read or save another stack's compose file. The message now
says so and points at include:.

* fix(recovery): redact tokens in URL includes from capture notes

The notes that list inputs a recovery generation did not capture only
removed a user:password pair from a URL include. A token used as the user
info (https://token@host/base.yaml) or carried in the query string or
fragment still reached the log and the Rollback readiness detail. Remove
the user info entirely and replace the query and fragment with a marker.

* fix(recovery): report a stored generation's own coverage and keep unenumerable includes from blocking capture

Rollback readiness described the live project even when a recovery
generation was current, so editing the compose file afterwards hid what the
generation had left out, and the generation row still promised an exact
restore. Managed inputs and the generation row now come from the stored
generation: complete, missing some inputs, unreadable, or not recorded. A
generation that recorded nothing (older, or a row with no stored content) is
never called covered, because it may have skipped an outside input without a
note. The row still says when the next capture would be refused, and an
unreadable generation no longer leaves the compose file row at ready.

Capture also still refused valid Compose that the first pass did not cover.
An include or extends path with a variable, a variable project_directory, and
a project_directory outside the stack are now reported as not captured
through an opt-in parse mode that only the authored inventory uses. Git
materialization keeps refusing them. An include's own env_file is still
recorded in those cases.

The compose-file rule is documented: a compose file must be inside its stack
directory, and a link out of it is refused so one stack cannot read or save
another's configuration.

* fix(recovery): run rollback against a selected env file inside the compose root

A selected project env file that links to another location inside
COMPOSE_DIR is admitted by capture, which records the resolved real path
in the stored invocation. Recovery validated that path against the stack
directory alone and refused it, so a deploy or update could begin with a
recovery generation that rollback could not launch.

Recovery now validates against the same compose root that capture
authorizes, decides containment on the real path so a name inside the root
whose link target leaves it is still refused, mirrors in-stack files under
a recovery overlay, and keeps an authorized file outside the stack
directory on its live location. A relocated compose directory still
resolves, because both the lexical and the real root are matched. Anything
outside the compose root is refused, as before.

* fix(recovery): report Partial while managed input coverage is unproven

Rollback readiness reported an overall Ready while the managed-input row
warned that a generation leaves inputs out, recorded no coverage, or could
not be read. The headline then contradicted the rows beneath it, before an
operator acted on it.

The overall state now caps at Partial unless managed-input coverage is
proven complete. Complete coverage still reads Ready, and an unreadable
generation keeps the partial result it already had through the compose
file row.

* fix(recovery): restore the Compose project the generation captured

A partial generation could not tell which live file one of its uncaptured
inputs named, so it skipped every live-discovered delete. That also kept a
Compose root or implicit override added after capture, which then won
auto-discovery and decided what the rollback ran. Separately, a declared
optional env file that was absent at capture and later became a link out of
the stack was never removed, so the recovered service imported its
post-capture values while the generation claimed complete coverage.

A generation now records the Compose layers it selected, and recovery pins
them instead of leaving the choice to auto-discovery. A live Compose layer
the generation did not record is deleted, because Compose selects a root or
an implicit override by name and never resolves one through an
interpolatable input. Those layers are observed and removed as leaves, so a
link is unlinked without following its target, while a path being written
still resolves its target and a parent-directory escape is still refused. A
name a ${VAR} input could resolve to is recorded as an input the generation
cannot hold, so restore never deletes it. A managed path that is a link is
snapshotted as a link, so reconciling an interrupted restore puts it back.

Sharing a compose file between stacks is done with include: or extends:. A
stack whose main Compose file is a link out of its own directory stays
refused, with include: as the supported alternative.

* docs(stack-management): state the restriction on a linked compose file

A stack's compose file has to be a file inside its own directory, or a link
that stays inside it, and the operator guide now says so plainly and names
the alternative. The restore exception that removes a Compose layer added
after a partial capture is stated where rollback coverage is described.

* fix(recovery): restore the project and inputs the generation captured

A rollback could run a different Compose project than the one it captured,
and a failed restore could leave a file changed that nothing tracked.

Recovery pinned an inferred list of Compose layers. That list did not match
how docker compose resolves competing filenames, and a -f flag replaces
COMPOSE_FILE rather than adding to it, so a stack that selected its layers
through that variable lost them. It now pins the layers the capture-time run
actually loaded: the conventional discovery order docker compose itself
uses, or the COMPOSE_FILE chain, read with Compose's own precedence, dotenv
quoting and path separator. A chain that cannot be pinned, and the refusal
that names it, are redacted the same way a capture note is.

A partial generation deleted a live file whose name looked like a Compose
file, even when an input whose whole filename is interpolated could resolve
to it, so the restored project could not render. Such a generation now keeps
every file it cannot place.

A managed file that had become a link was snapshotted as a link only, while
the restore write follows that link, so a failed restore left the target's
bytes changed and no record of them. The snapshot now holds what the write
actually changes, and the revert puts the recorded link back before writing
through it.

A symlinked compose directory was accepted for capture and then refused by
restore, because containment compared physical paths against lexical roots.
Capture, restore, reads, writes and link recreation now use one real-root
rule, while a stack directory that links elsewhere stays refused.

A URL in a capture note was redacted up to its first "@", which is legal
inside a password, so the rest of the credential reached the generation
file, the capture log and readiness. The whole userinfo is now removed,
with the query and fragment.

Tests: focused coverage for each of the above, a direct test of the
COMPOSE_FILE reader, and a docker-backed test that renders the live and the
recovery invocation with the real Compose CLI and compares the models. Each
new test was checked to fail without its fix.

* fix(recovery): read a pre-restore snapshot from one file descriptor

The snapshot resolved a managed path, checked it against the real stack
root, then lstat'd and read it by name. Anything able to write inside the
stack could swap the checked file for a link out of it between the check and
the read, so the preimage could come from a path containment never approved.

The validated path is now opened once and the size check, the mode and the
bytes all come from that descriptor, so they describe the same inode. This
matches how the import preview already reads a candidate compose file.

* fix(recovery): keep uncaptured inputs and match Compose's layer selection

A pre-merge audit of this branch found three defects in the partial-capture
path, plus one inaccurate comment.

A partial restore deleted a live compose root or override whose basename
looked conventional. That removed a dependency of an uncaptured external
include: the live shared file extended a service from a local
compose.override.yml that appeared after capture, and the restored project no
longer rendered. A generation that recorded any uncaptured input now deletes
nothing from the live discovery set, and the layers it selected are pinned on
the recovery run, which keeps the rollback project exact without the deletion.

The COMPOSE_FILE reader ignored COMPOSE_ENV_FILES, truncated a quoted value
that spans lines, did not expand references, and dropped empty chain entries.
Each of those could pin a layer set the live run never loaded, or refuse a
stack that works. The reader now ports the one Compose uses, reads the same
file list, records the files COMPOSE_ENV_FILES names as not captured, and turns
anything it cannot reproduce into an explicit refusal instead of a guess.

A stack whose main compose file or implicit override is a link inside the
compose folder now opens, deploys and captures a partial generation: reads and
deploys follow the link, saves and restores never write through it, and a link
out of the compose folder or a stack folder that is itself a link stays
refused.

Review of those changes also closed a cross-stack env write (the editor env
save, template apply, Git env sync and fleet snapshot apply now check ownership
at the write) and stopped refusing a layer whose name holds a literal dollar.

* fix(recovery): read COMPOSE_ENV_FILES under the compose-root containment rule

The selection reader read a COMPOSE_ENV_FILES entry with a raw read at the path
the process environment names, which static analysis reports as an uncontrolled
path expression. The entry is now read through the same containment rule every
other env-file read uses, and a path outside the compose root makes the
selection unknown rather than being read. The refusal reason names the file, so
the operator can move it into the compose folder or select it with a project
env file instead.

* refactor(recovery): read pre-restore snapshot bytes in one named helper

The descriptor protocol (stat, then read, on the handle the caller opened after
its containment check) now lives in readSnapshotBytes, so the snapshot loop
holds the containment and blob logic. Behaviour is unchanged: a non-regular file
is still refused before any read, and the mode comes from the same descriptor
as the bytes.

* fix(recovery): match Compose when it disables or BOM-prefixes the layer env

COMPOSE_DISABLE_ENV_FILE turns off the project .env Compose falls back to
when nothing else selected an env file, but the reader that decides the
layer-selecting COMPOSE_FILE always read it. With the variable set, the live
run loaded the root compose file alone while the capture recorded the .env
chain, and a recovery pinned and ran the other project. The reader now makes
the same strconv.ParseBool check Compose makes, and only where Compose makes
it, so an ignored file cannot decide the layer set. A value Compose's parser
rejects makes the selection unknown and refuses the capture, because Compose
refuses to run in that state.

The dotenv reader also began at character zero and rejected a leading UTF-8
BOM that Compose trims before parsing, so an ordinary stack with a
BOM-prefixed .env became an unknown layer selection and a coverage refusal.
One leading BOM is now dropped, and a BOM anywhere else stays the character
Compose rejects.

The selection also reports every env file the run loads for interpolation,
named before the read so a file Compose reads but this reader's containment
refuses is still visible to a caller accounting for coverage.

* fix(recovery): record the env files a Git generation does not hold

resolveGitInventory returned an empty omission list and the inventory
returned before the authored COMPOSE_ENV_FILES accounting could run, so a
Git-managed generation persisted complete coverage while Compose loaded
operator env files the managed-project manifesto does not hold. After such a
file changed, a restore reported success and the recovered stack rendered
the new value.

The Git inventory now records every env file the run loads for interpolation
that the manifest does not hold, which holds the generation at partial
coverage and lists the files, while the manifest stays the canonical file
inventory and the layer selection. A path outside the stack folder is named
in the note without a deletion-protection entry, and a file the manifest
holds is not listed.

The operator pages now state that an env file Compose loads for
interpolation which the generation does not manage is not captured and a
rollback reads its current content.

* fix(recovery): pin the compose layers the live run loads, Mesh included

A Mesh-enabled deploy pins its own explicit `-f` list: the base compose
file, the implicit override that list would otherwise suppress, and the
generated Mesh layer. The capture predicted the layer set from its own
copy of Compose's discovery order and its own COMPOSE_FILE handling, so
two things could diverge from the run being backed up:

- The order itself. FileSystemService listed the legacy root family and
  every override the other way round from Compose, so a Mesh-enabled
  deploy of a stack holding both `docker-compose.yml` and
  `docker-compose.yaml`, or both override variants, pinned one file
  while Compose's own discovery loads the other. Recovery then pinned
  the file the deploy never loaded and reported complete coverage.
- COMPOSE_FILE. An explicit `-f` list replaces the variable, so a Mesh
  run ignores a chain in `.env` that the capture still read and pinned,
  and the rollback ran a project the deploy never started.

The discovery order now lives in one place, `utils/composeFilenames`, and
every caller that has to agree with what Compose loads resolves from it:
the file the editor reads and saves, the Mesh injection, the rollback
inventory, Git input discovery, and the project-name pin. The inventory
also treats Mesh enablement as a run that pins files, so it records the
discovery pair as the layers instead of the chain a Mesh run suppresses.

Verified against docker compose v5.6.0, which warns and names its pick
when a stack holds more than one candidate.

* fix(recovery): pin an absolute COMPOSE_FILE layer that names the stack

Compose takes an absolute chain entry as written and resolves a relative
one against its working directory, which is the stack directory for every
Sencho spawn, so `/compose/app/prod.yaml` and `prod.yaml` are the same
layer. The capture passed every entry to the relative-path validator, so
an absolute spelling was refused as "not a path inside the stack
directory" and blocked a project Compose runs.

An absolute entry that resolves inside the authorized stack is now
normalized to its stack-relative name before pinning, which is the form a
recovery run can rebuild under an overlay. The real root is matched as
well, so a relocated (linked) compose directory resolves, the same way
every other containment check in the capture treats it. An absolute path
outside the stack stays unpinnable and keeps its refusal.

* fix(recovery): read Unicode assignment names in a compose env file

Compose validates an assignment name with Go's unicode.IsLetter and
unicode.IsNumber over runes, so `UNICODE_é=value` is an ordinary line.
The reader accepted only ASCII letters and digits, so that line made the
layer selection unknown and refused the capture of a stack whose
`COMPOSE_FILE` had nothing to do with it, which is the compatibility
failure this work removes.

The assignment grammar now accepts any Unicode letter or number and is
read by code point, as Compose reads by rune, while a character Compose
rejects (`$`, `@`, `/`, `+`, `?`, `%`, a space) still refuses. The
template grammar a `${...}` reference is read with stays ASCII, which
matches Compose: it rejects a non-ASCII name inside a reference.

* docs(recovery): name the Unicode table drift in the assignment-name reader

The reader validates a name with the letter and number tables this Node
build ships, so a letter added to Unicode after the Compose binary was
built is accepted here and refused there. That direction only
over-accepts, which cannot block a stack Compose runs, so it is the
documented one.

* fix(recovery): match Go when a dotenv escape is not a valid octal

Compose unquotes each escape its regex matched with Go's
strconv.UnquoteChar, which accepts `\ooo` only as exactly three octal
digits holding a byte, and rewrites a `\0` prefix to `\` before that
call. The reader accepted any run of octal digits and any value, so
`COMPOSE_PATH_SEPARATOR="\012"` became a newline while Compose keeps the
literal `\12` and splits the chain there.

The capture then recorded one layer named after the whole value, and the
recovery run could not load it: a stack that deployed fine had a
generation whose rollback failed before any image hold or health check
could matter. A spelling that fails the unquote is now kept in the
rewritten form Compose itself keeps, so `"\012"` is `\12` and `"\0"` is a
single backslash.

Twenty-four separator spellings were compared against Compose v5.6.0, and
the incomplete, bare, out-of-range and valid byte octal shapes are
asserted in the model-equivalence suite with the literal layer list.

* fix(recovery): record every env file an authored generation does not hold

The authored inventory skipped its COMPOSE_ENV_FILES accounting whenever
the layer-selection reader could not decide, and a listed file outside the
compose folder always produces that state: Sencho's containment refuses to
read it while Compose reads it for interpolation. A Mesh-enabled stack
pins its layers, so the capture proceeded and persisted an empty omission
list with complete coverage, while the run rendered every value from a
file the generation never held. After that file changed, a restore
reported success and the recovered stack ran the new value, and the
generation was eligible for the cleanup rule that removes live files it
cannot account for.

Every named loaded env file the generation does not hold is now recorded,
including the names after the one the reader stopped at, which is the rule
the Git inventory already applies to its manifesto. The recorded omission
holds the generation at partial coverage, keeps the file out of the delete
pass, and lists it in Rollback readiness.

* fix(recovery): rebase a captured env file in the matched root's coordinates

The context-directory fallback records the compose-folder spelling of the
project `.env`, and containment is decided against both the lexical and
the real compose root. The mirror was then derived from the captured
spelling against the root that matched, so with a symlinked compose folder
the lexical name was rebased against the real stack folder and escaped the
overlay: a generation that captured successfully could not launch a
recovery that needed a decrypt overlay.

The mirror is now derived in the coordinates of the root that matched, so
the linked spelling resolves to its overlay counterpart, a path whose real
target leaves the compose root is still refused, and an authorized env
file outside the stack folder keeps its live location.

* docs(recovery): correct the type comments the coverage changes superseded

A generation captured before the layer set was recorded does get its
implicit override named on a recovery run, a compose file that links
inside the compose folder is an accepted partial input rather than a
refusal, and a Git inventory does populate its omission list.

* test(recovery): write the recovery layer the Git coverage case renders

The second case in the Git env-coverage file rendered a recovery layer that
the first case wrote, so it passed only when that case ran before it: run on
its own, or in a shuffled order, it failed on a missing compose file. It now
writes its own fixture.

* fix(recovery): refuse escaped compose layers, hold the project env file, and match Compose templates

A review of this branch found four defects in the recovery path, three
of them able to break a rollback that looked healthy.

Recovery validated a captured compose layer lexically, so a main
compose file that had been relinked outside the compose folder was
followed and rendered: the ordinary read refuses that target, but
recovery accepted it. Every captured layer recovery opens is now
checked on its real path. A target inside the compose folder stays
allowed, including a shared file outside the stack folder, and a
missing file stays allowed, because a restore writes the held files
back. A dangling or looping link, and a target outside the compose
folder, are refused before Compose runs.

The inventory did not account for the `.env` Compose loads after it
picks the project folder, so a generation could be certified complete
while a value it used came from a file it never held: the recovered
stack ran the changed value. The project folder is the explicit
project directory, else the folder of the first compose file, else the
folder of the first captured layer. An authored generation now holds
that env file, and a Git generation records it as an omission when
its manifesto does not hold it. An explicit env file, a listed
COMPOSE_ENV_FILES, and a true COMPOSE_DISABLE_ENV_FILE still replace
that default.

The pre-restore snapshot opened its source before checking that the
source was a regular file, and opening a fifo waits for a writer, so a
restore could wait forever before recording its intent. The source is
now stat'ed and refused when it is not a regular file, and the open
uses a nonblocking flag, so a fifo swapped in between the two cannot
block either.

The dotenv reader counted every literal brace as an interpolation
opening, so a valid assignment such as `LABEL=${UNSET:-foo{bar}` was
refused and the capture failed with an invalid template. The reader
now follows the same template grammar Compose does: a literal brace is
text, only `${` opens a nested template, and an unclosed template or
an empty one is still refused.

* fix(recovery): keep the compose-root barrier at the layer sink and read the snapshot source from one descriptor

The static analysis of this branch reported two shapes the audit
response introduced.

The captured layer check resolved a layer name against the working
directory and then read the result, but the containment check that
authorized it lived in the caller, so the check was not credited where
the path is read. The barrier now sits at the read, in the form the
analysis credits, and refuses a layer that resolves outside the working
directory before the path is touched. The rule is unchanged: a layer
inside the compose root is allowed, a missing layer is allowed, and a
target outside the compose root is refused.

The snapshot source was stat'ed and then opened, which leaves a race
between the two: the file that was checked is not necessarily the file
that is opened. The open is now nonblocking and the descriptor it
returns is the one that is checked, so a fifo, a device, or a directory
is refused without waiting for a writer and without reading a path that
changed underneath the check. The refusal keeps its code and its
reason, and the restore still fails before it changes a project file.

* fix(recovery): keep application data out of restore deletes, rebuild the pinned env, refuse escaped layers

A pre-merge audit of this branch reproduced three recovery defects; each
fix has a test that fails with the change reverted.

Restore treated every live input a generation could not hold as a managed
cleanup candidate, so an application-data link that appeared after a
complete capture was unlinked and the application lost its connection to
its storage. Application data and build inputs are not managed paths:
their exclusions carry no note and no path, the inventory records every
path a silent input references, including the literal tail of a dynamic
bind source, and a restore forwards neither their omissions nor their
live entries, whatever managed input also uses the same path. A managed
input that became a link after capture is still removed, and a captured
bind-mounted file is still written back.

Pinning a COMPOSE_FILE selection as an explicit -f list dropped the
stack root .env from the recovery interpolation environment while the
generation claimed complete coverage, so a root-only value fell back
after a rollback. The invocation now records the env files the live run
applied, project file first and root file last, and recovery replays them
as ordered --env-file flags, reproducing the live precedence. An env
file that links out of the compose root is left to its recorded omission
instead of naming a target recovery would refuse, and a file the capture
cannot inspect refuses the capture outright.

Capture accepted a selected COMPOSE_FILE layer linking outside the
compose root as a partial omission, while recovery refuses that same
target before Compose runs. Selected chain layers now follow the
compose-layer refusal policy the root and override candidates apply; a
shared target inside the compose root stays an accepted partial omission.

* fix(recovery): put the captured env-file barrier at the realpath read

The static analysis of the recovery env replay reported the realpath sink
of the captured env resolver as an uncontrolled path expression: the new
invocation carries the recorded interpolation env files, and the guard
that authorized them was a helper the analysis does not credit. The
barrier now sits at the read, in the form the analysis credits, and
enforces the entry contract the function already applies at its bottom:
a captured env file is a path inside the compose root, spelled
lexically or through a linked compose directory, and anything else is
refused with the same message. The realpath read and its lexical
fallback moved into the same frame so the guard and the read sit
together; behavior is unchanged for every existing caller and test.

* fix(recovery): guard the captured env-file read with one constant root

The barrier the previous commit added spans two alternative roots, one
of them the physical form of a linked compose directory, and the static
analysis credits neither a guard over two roots nor a root it cannot
resolve. A captured env file spelled through the physical root is now
rebased onto the lexical root first, so one guard over one constant
root covers both spellings in the form the analysis credits, and the
realpath read and its guard sit in the same frame. The contract is
unchanged: both spellings are accepted, everything outside the compose
root is refused with the message the refusal already used.

* fix(recovery): match Compose dotenv whitespace and refuse non-file layers

Read the interpolation env file with Go's stage-specific whitespace rules so a trailing BOM stays part of a layer name and statement separators Compose accepts do not refuse capture. Refuse a shared compose target that is not a regular file before any blocking open, including the inventory classifier and declared-input reads.

* fix(recovery): keep unresolved application data through restore

Record dynamic application-data and build-input sources, legacy Git host declarations, stack-containing binds, and directory prefixes so a restore never deletes a live path whose ownership cannot be proven. Resolve absolute, linked-root, relative, alias, and physical bind spellings to one stack ownership record, including Git manifest host sources. Protect mixed-role paths and directory descendants from manifest paths, tombstones, and live forwarding. Read authorized env files through the lexical compose-root spelling and allow COMPOSE_FILE-only stacks. Report whether a stored generation holds the compose layers it selected.

* fix(recovery): refuse a captured input that is no longer a regular file

A recovery generation omits every input it cannot hold, so a restore never snapshots or replaces those paths. A shared compose layer or selected env file could therefore become a FIFO after capture, and recovery still accepted the arguments: Compose then blocked opening it instead of naming the invalid input.

Recovery now requires each existing captured compose layer and selected env file it hands to Compose to be a regular file, on the path the run opens: the overlay mirror for an in-stack file, and the live path for an authorized input outside the stack. A missing file is still left for Compose, and containment is unchanged. The refusal carries NOT_A_FILE and lands before the execution boundary.

Tests capture a real generation for a shared main compose file and a shared selected env file, replace the omitted target with a FIFO, a directory, and a UNIX socket, and assert a prompt typed refusal with no Compose spawn. An overlay mirror is covered too, and regular shared inputs keep their supported partial recovery path.

* fix(recovery): refuse a non-regular interpolation env input before it blocks

A recovery rebuilds the live inventory before it restores anything, because
that inventory decides which live paths are safe to touch. The reader that
selects COMPOSE_FILE read the project .env and every COMPOSE_ENV_FILES file
with a blocking read, so a file that was a regular file at capture and later
became a FIFO with no writer held the rollback (and rollback readiness) inside
that read.

Interpolation env reads now check the path type before opening and the
descriptor type after a non-blocking open, keeping the compose-root
containment rule. A file that exists but is not a regular file is reported as
invalid rather than unknown: capture refuses it whether or not the layer set
is pinned, and restore throws NOT_A_FILE before any project file moves, so a
stack is never left written but unlaunchable. The refusal survives the
first-apply and refused-manifest Git merges, and the recovery spawn repeats
the check for the COMPOSE_ENV_FILES files the child inherits but the captured
argv never names. An explicit --env-file replaces that list for
interpolation, and the scan is positional so a compose layer literally named
--env-file is not mistaken for the flag.

Tests run the real capture and restore context in an externally bounded
child, so a regression fails the suite instead of holding the worker, and
cover a listed env file, the project .env, a first capture that meets the
FIFO, and regular and in-root linked files that must keep working. In-process
tests cover the typed selection, the pinned inventory refusal, the restore
refusal before any write, and the spawn guard.

* fix(security): decide env-file type on the descriptor, not a prior stat

The interpolation env readers checked the path type with a stat and then
opened the same path, which is a check-then-use pair: the file can change
between the two calls, and the check is not what the read uses. The open is
already non-blocking and the descriptor is what the read consumes, so the
type is now decided on that descriptor alone. A socket cannot be opened for
reading at all and a directory is not openable everywhere, so those open
errors map to the same NOT_A_FILE refusal instead of falling through as an
unreadable selection.

The recovery spawn guard resolved COMPOSE_ENV_FILES entries against a working
directory built directly from the stack name. It now uses the validated stack
path the rest of the recovery invocation uses, so the operator's own
environment file list is stat'ed against a path that has already passed the
stack-name containment check. Behavior is unchanged: the same entries are
checked, the same refusal is raised, and overlay working directories still
resolve relative entries the way Compose does.

Tests keep covering a FIFO, a directory, a socket, the project .env and the
spawn guard, and the bounded capture/restore scenarios are unchanged.

* fix(security): anchor the env-file guard on the contained stack path

The recovery spawn guard stats COMPOSE_ENV_FILES entries against the Compose
working directory. That directory came from the validated stack resolver, but
the containment decision happened inside a helper, so the path was not shown
to be contained at the point it reaches the filesystem call. The check now
sits inline on the working directory itself, in the same form the other path
sinks in this service use: resolve the stack name against the compose root and
require the result to stay inside it. An overlay working directory is prepared
for the operation and does not carry a request path, so it is used as is.

No behavior change: resolveValidatedStackDir still rejects an invalid stack
name, and the inline check is the same containment rule restated where the
filesystem call can see it.

* fix(security): keep the operator env-file path out of request-tainted flow

The recovery spawn guard stats COMPOSE_ENV_FILES entries. Those entries are
process configuration the operator sets for the whole Sencho process, and the
same value is inherited unchanged by every Compose child, so inspecting a
path's type in Sencho does not widen what the operator can reach. The path was
resolved inline from the stack working directory, though, so taint from the
route's stack name reached the stat call.

The resolution now goes through resolveOperatorConfiguredPath, whose contract
is operator configuration and never request data, and the path-injection model
records that boundary the same way the existing validation helpers are
declared. The containment check on the stack working directory stays inline at
the call site.

Behavior is unchanged: the same entries are resolved against the same working
directory, and the same refusal is raised.

* fix(security): drop the CodeQL model for the operator env-file path

The path-injection model added for the recovery env-file guard changed the
analysis configuration of this branch, so the required code scanning gate
could no longer compare the pull request against the base branch and reported
a skipped comparison instead of a result. The model is withdrawn.

The guard keeps the inline containment check on the stack working directory.
The paths it inspects are the operator's own COMPOSE_ENV_FILES entries, never
request data, and the alert for that sink is dismissed with that rationale.
The model file matches the base branch again, so the gate can evaluate this
pull request normally.

* fix(recovery): read the env filename Compose opens from COMPOSE_ENV_FILES

A recovery replay checks the inherited COMPOSE_ENV_FILES before spawning Compose so a FIFO cannot hang the rollback. That guard trimmed each entry, while Compose uses every non-empty entry verbatim, spaces included. A valid spaced filename was therefore refused, and a spaced FIFO or directory could pass because the trimmed sibling did not exist.

Read the entries through the same selection helper the capture uses, so the guard names exactly the file Compose opens. Cover the spaced file with a trimmed sibling directory, a real Compose model comparison through the recovery launcher, the spaced special-file refusal, and explicit --env-file precedence.

* fix(recovery): read CR and Unicode separators in env defaults

The template pattern copied from Compose used a JavaScript dot for a substitution default. Go's dot excludes only LF, while JavaScript's also excludes CR, U+2028 and U+2029, so a \r escape or a literal separator in an unused or used default failed to match. The reader then reported an invalid template, which became an unknown COMPOSE_FILE selection and a coverage refusal for a stack Compose runs.

Match Go's dot with [^\n]*. LF still prevents a match, as it does in Compose. Regression coverage compares the reader with the Compose CLI for all four spellings, checks the selection and inventory path, and keeps the LF refusal.

* fix(recovery): decide restore env refusals from the captured invocation

A restore refused a usable generation when the current project env
selection named a file the recovery run never reads. Recovery replays the
generation's own invocation, so the restore now derives the interpolation
env files that invocation opens (an explicit --env-file, then the recorded
COMPOSE_FILE interpolation list, then COMPOSE_ENV_FILES, then the project
.env) and refuses NOT_A_FILE only for a non-regular file among them.

An unusable or undecidable live selection no longer blocks the rollback:
it disables deletion, because the live project is not fully known and no
unrecorded path can be proven stale. The pre-check classifies the target
type on the path Compose opens, which follows links and never blocks, so
a FIFO behind an escaping link or a COMPOSE_ENV_FILES entry outside the
compose root is still refused before any project file moves.

Tests cover a real-context restore after the selection is replaced
(directory and FIFO without a writer), a FIFO behind a project .env link,
an outside COMPOSE_ENV_FILES FIFO, delete-none for an undecidable
selection, and a real Compose render proving the replayed model uses the
captured value.

* test(recovery): pin the captured env source precedence

* test(recovery): assert restored bytes instead of a file check

* fix(recovery): restore a link target's mode on failed-restore revert

A failed restore snapshots the preimage of every managed path before it
mutates, and reverts from that snapshot when a later step fails. When the
preimage was an in-stack symlink, the revert restored the link and the
target's bytes but skipped the target's mode, because the public chmod
path refuses a symlink leaf. It then removed the snapshot, and the caller
removed the restore intent, so the failure looked fully compensated while
a private file had become readable by other local users.

Apply the snapshotted mode to the staging file of the compensating write,
so the bytes and the mode land in one atomic promotion. A link preimage is
treated like any other: the write follows the link, and the mode it
restores is the one the snapshot read through it. If the mode cannot be
applied, the promotion does not happen, the revert throws, and the
snapshot and intent stay for the retry that startup reconciliation owns.
The file explorer's chmod still refuses a symlink leaf.

Tests cover the private link-target round trip under a permissive umask,
an injected mode failure that keeps the intent and reports an incomplete
revert, the retry through reconciliation, and the regular-file mode that
the same write now restores.

* test(recovery): read the compensation target through one descriptor

The compensation test checked the restored target's mode by path and later
read the same path again. A static analyzer models that check-then-use as a
file-system race, even though the assertions run against a private temp
directory, and the alert blocks the pull request.

Open the target once, read the bytes and the mode from that handle, and
close it in a finally. The assertions are unchanged and now describe one
consistent file.

* test(recovery): observe the retried target once

The retry test opened the restored target to assert its state after the
failed compensation and opened the same path again after reconciliation.
A static analyzer models the two opens of one path as a check followed by
a use, so the alert survived the previous descriptor change.

Keep the failure proof in the durable evidence, the incomplete-revert
signal, the pending intent and the retained snapshot, and observe the
target only once, after the retry, through a single descriptor. Coverage
for the corrected behavior is unchanged.

* fix(recovery): replay an absent default env file in the recovery invocation

A stack captured with no default .env, a dynamic data bind and an
interpolated value was recorded as complete. A restore keeps any file it
cannot prove disposable, so an .env created after capture stayed in place,
and the recovery run, which named no env input, let Compose discover it.
The recovered project then resolved ${VAR} references from a file the
generation never held.

Capture now records an empty interpolation env list when the default env
path is active and nothing exists at it: the stack root .env, and the
selected project directory's .env when the layers moved the project. The
stack root file only counts when a pinned COMPOSE_FILE chain keeps it in
the interpolation environment; an explicit -f or --project-directory makes
Compose read only the project directory's file. Recovery replays the empty
list as an explicit empty env input, which replaces default discovery and
COMPOSE_ENV_FILES. Anything present at a default env path, including a
directory, a special file or a link, is not an absence and keeps its
existing accounting. Preservation of unowned live files is unchanged.

* fix(recovery): keep an operator env file and the implicit override across a Git rollback

A Git generation treated every env file the live project loaded but the
manifest does not hold as a cleanup candidate, so a `.env` created after
capture was deleted by the restore and its bytes were not recoverable. The
inventory now separates the paths it records as omitted from the ones it owns
and may remove: a Git manifest owns none of its omissions, so only owned
omissions reach the restore. When no default env file exists at capture the
Git inventory also records an empty interpolation env list, as the authored
inventory does, so the preserved file stays out of the recovered project.

Recovery also pinned only the configured Git compose file, which dropped the
manifest's implicit override from the recovered project, most visibly when
Mesh appended its layer. The recorded layers now include a managed override
that exists on disk, after the configured file.

The partial-restore wording in the atomic deployments page now says that
files the generation recorded still return to their captured state.

* fix(recovery): evaluate the stack env disable from the run environment

A stack root .env can supply COMPOSE_DISABLE_ENV_FILE and COMPOSE_FILE together. Compose uses that file to resolve the chain but keeps its assignments out of interpolation and skips the nested project .env. Capture only consulted process.env, so it recorded and replayed both files, reintroducing values the live run never read.

Read the disable through the canonical env reader, report whether the stack env was read and whether the selected project env is disabled, and record exactly the files the run read. A self-disabled stack env now records an empty input, replayed as --env-file /dev/null. Gate a file-supplied disable to runs whose initial project directory is the stack root: with -f or --project-directory starting elsewhere Compose never opens the stack .env.

Add focused selection tests, inventory cases for the self-disabled and -f start-outside-root shapes, and a real-Compose model-equivalence regression.

* fix(recovery): refuse a chain whose project env link leaves the root

A COMPOSE_FILE chain can move the project directory into a subfolder. When
that subfolder's .env was a link leaving the compose root, the ordered
interpolation list was dropped, so recovery pinned the layer without
replaying the stack .env that had won a shared key. A real Compose run
rendered from-project after rollback versus from-root before it, with no
input changing.

Record the list when every source is namable, and let discovery read a sole
outside project env file in place. When an unnamable source coexists with
another source that must be replayed, refuse the capture before any write
with a note naming the file. Shared links inside the compose root keep the
existing ordered replay.

Covered by unit and real-Compose regression tests asserting the literal
root-winning value and the pre-mutation refusal.
2026-10-22 10:27:48 -04:00
Anso 7a9abb8993 fix(docker): rebuild the CLI and Compose binaries with Go 1.27.3 (#2164)
The pinned 1.27.2 toolchain embeds stdlib v1.27.2 in the source-built
docker CLI and Compose plugin, which the image scan flags for
CVE-2026-111943 (crypto/x509 intermediate-certificate verification,
HIGH). Bump both builder stages to 1.27.3, the release that fixes it,
and update the embedded ldflag version and the toolchain comments.
2026-10-22 08:48:26 -04:00
Anso 108a47aaab fix(git): classify an untrusted-signer TLS failure as a certificate error (#2158)
Some git builds report a private-CA failure as "SSL certificate
verification failed: certificate signer not trusted" instead of the
OpenSSL wording the classifier already knew. The message fell through to
the generic "Git fetch failed" text, so the operator got no hint that the
fix is to upload the CA certificate on the git source.

Match the signer phrase in the certificate-could-not-be-verified branch.
The shared "SSL certificate verification failed" prefix is deliberately not
matched on its own, because the same prefix carries causes a CA upload does
not fix, such as an insecure certificate algorithm; those keep the generic
message that shows the underlying reason.
2026-10-21 09:20:31 -04:00
Anso 95398a182d fix(deps): bump follow-redirects to 1.16.1 (#2157)
Update the backend lockfile to follow-redirects 1.16.1, the release that fixes GHSA-q3jh-wrc6-jgq8 / CVE-2026-111756 and GHSA-8r9p-f939-6h3c / CVE-2026-111755. Both high-severity advisories cover 1.16.0, and the existing axios and http-proxy ranges already permit the patch, so no manifest change is needed. The audit gate passes with this lockfile; it failed on follow-redirects 1.16.0.
2026-10-20 15:44:16 -04:00
Anso 935158b5e6 feat(resources): bring the Resources page to the design system (#2156)
* fix(resources): load the Resources page and volume prune without the slow image df

On a host that uses the containerd image store, the image section of
`docker system df` takes about 30 seconds, and the Resources page waited
on it for its footprint figures. Pruning volumes repeated that full df
once per volume, and the prune preview ran into its 12 second timeout.

- The disk-usage figures now come from calls that answer in milliseconds:
  a shared-size image listing plus the volume and container sections of
  df. The reclaimable image figure is the bytes unused images do not
  share with another image; the exact df is used only when the daemon
  does not report shared sizes.
- External and managed volume bytes were always zero because the volume
  listing carries no usage data. They now come from the volume section.
- Prune planning and execution read only the volume section for volume
  usage, ask for container sizes only when containers are planned, take
  shared image sizes from the listing, and inspect idle networks in
  parallel. When volume usage cannot be read the plan fails with an
  error instead of reading as nothing to prune, and execution skips the
  volume with a clear reason.
- The Resources page loads the tables and the disk usage separately, so
  the tables no longer wait on the figures. Refreshing after an action
  keeps the rows on screen, and a failed load shows an error with Retry
  instead of an empty table.

On a node with 200 volumes and 85 images, the disk-usage request went
from about 31 seconds to 0.2 seconds, prune previews from a 12 second
timeout to under 0.4 seconds, and pruning 200 volumes now takes about
1.5 seconds.

* feat(resources): bring the Resources page to the design system

The Resources page had a reclaim banner behind an opt-in setting, a disk
footprint card, four equal-weight Quick Clean tiles, and per-node chrome
of its own. It now follows the same shape as the other primary pages.

- A hero masthead gives the one answer: how much a cleanup would free,
  Tidy when nothing is left, Measuring disk while usage loads, and Disk
  usage unknown when the node could not report it (never Tidy). Managed
  and external bytes sit beside it.
- Reclaim and a cleanup menu (prune unused networks, prune stopped
  containers) sit in the tab strip. Images and Volumes each have their
  own Prune unused. Every prune opens an itemized preview with a scope
  switch (Sencho-managed by default, or All Docker) that rebuilds the
  plan when it changes.
- The reclaim banner and its opt-in setting are removed, along with the
  footprint card and the Quick Clean tiles.
- Filters use the shared segmented control, scans use the shared scan
  flow, and confirm buttons show progress through the shared busy label.
- The Unmanaged tab is named External. Its rows use checkboxes and its
  project headers are quiet rows. A Rollback protected badge on a
  stack-held image opens the Rollback tab.
- The docs page, the pages that mention the old tiles, and the
  screenshots are updated.
2026-10-20 09:51:36 -04:00
Anso 880e6c6489 fix(resources): load the Resources page and volume prune without the slow image df (#2155)
On a host that uses the containerd image store, the image section of
`docker system df` takes about 30 seconds, and the Resources page waited
on it for its footprint figures. Pruning volumes repeated that full df
once per volume, and the prune preview ran into its 12 second timeout.

- The disk-usage figures now come from calls that answer in milliseconds:
  a shared-size image listing plus the volume and container sections of
  df. The reclaimable image figure is the bytes unused images do not
  share with another image; the exact df is used only when the daemon
  does not report shared sizes.
- External and managed volume bytes were always zero because the volume
  listing carries no usage data. They now come from the volume section.
- Prune planning and execution read only the volume section for volume
  usage, ask for container sizes only when containers are planned, take
  shared image sizes from the listing, and inspect idle networks in
  parallel. When volume usage cannot be read the plan fails with an
  error instead of reading as nothing to prune, and execution skips the
  volume with a clear reason.
- The Resources page loads the tables and the disk usage separately, so
  the tables no longer wait on the figures. Refreshing after an action
  keeps the rows on screen, and a failed load shows an error with Retry
  instead of an empty table.

On a node with 200 volumes and 85 images, the disk-usage request went
from about 31 seconds to 0.2 seconds, prune previews from a 12 second
timeout to under 0.4 seconds, and pruning 200 volumes now takes about
1.5 seconds.
2026-10-20 09:17:17 -04:00
Anso 48b8d698b3 feat(gitops): observe remote rollout targets over the node proxy (#2149)
* feat(gitops): observe remote rollout targets over the node proxy

A health rollout policy could not gate a rollout target on another node. The
gate runs on the instance that owns the rollout, a remote node's Docker daemon
is not reachable from there, so every observation of a remote target ended
unknown and a policy other than observe was refused before dispatch.

The node that hosts the stack now serves its own runtime facts at
GET /api/stacks/:stackName/health-observation, with the declared Compose
restart intent when asked for it, and the gate reads them over the node proxy.
Local nodes keep the in-process Docker read, and the gate keeps one observation
engine, one run row and one attribution path.

health-gate-observation-v1 is advertised by every build that serves the route,
and the placeholder gitops-health-rollout-policy is never advertised, so a hub
that predates the protocol still refuses a remote target before dispatch.

Failures stay fail-closed: a node that does not report the facts a verdict
needs ends the observation unknown at the first poll, any other unreadable poll
keeps the three-strike budget, and a clean exit whose declared intent could not
be read ends unknown rather than failed, so a rollback policy cannot restore a
workload that may have completed exactly as declared. A node row this instance
does not have is unreadable rather than local.

The node-proxy JSON read is extracted into one helper, and the leaf fetches in
the artifact-resolve path use it.

* chore(security): make the compose path containment checks analyzer-visible

The CodeQL javascript-typescript check reports js/path-injection at two sinks
that a rollout health observation reaches with a request's stack name:
FileSystemService.hasComposeFile (the fsPromises.access call) and
ComposeService.renderConfig (the spawn cwd).

Both are guarded already, but not in a form the analyzer credits: the first
called the wrapped assertWithinBase helper, and the second relied on a check
scoped to the branch that assigned the working directory rather than one that
dominates the sink. Both now carry the inline barrier the codebase already uses
elsewhere for this rule, in the same scope as the sink.

No behavior change. assertWithinBase and the inline check use the same rule, and
every caller of hasComposeFile already passes a stack directory under the
compose base. renderConfig re-checks the same root its branch check validated,
immediately above the spawn.

* chore(security): keep the hasComposeFile guard in the shape the analyzer reads

The first pass combined the prefix check with an equality disjunct, which the
path-injection query does not model as a guard. The single startsWith check is
the shape the codebase already uses at its other sinks, and it is what clears
the alert. The compose base itself is not a stack, so refusing it is the
contract every caller already relies on; the test pins that too.

* fix(health-gate): decide definite failures before an ambiguous clean exit

An exit with code 0 whose declared Compose restart intent cannot be read is
undecidable on its own, but it was ending the observation immediately. A peer
that was unhealthy or had exited non-zero later in the same poll was never
inspected, so the run ended unknown and a retry, stop, or rollback policy was
replaced with a pause.

Hold the ambiguity until the rest of the poll is inspected, in both the stack
and the service scope, and decide it only where it is the sole unsatisfied
evidence. At the window end definite failures are judged first: a missing
expected container or replica, an unhealthy container, and a replica count the
gate could not satisfy all outrank an ambiguous peer.

A service replica count can now no longer be satisfied by a replica the gate
never watched while a tracked replica exited unexplained, and an unhealthy
replica of the operated service that appears after the gate armed fails on the
poll where it is seen, since a later recovery would erase the evidence. Scale-0
services keep their previous accounting: a failed leftover is ignored, while a
running container or a declared clean one-shot is still count drift.

Adds regression coverage for both container orders, both scopes, unhealthy and
non-zero-exited peers, a missing replica at the window end, untracked
replacements, the unhealthy-recovery case, and the scale-0 boundary.
2026-10-19 21:16:13 -04:00
Anso 1a52b5052e fix: group the GitOps portfolio filters into one popover (#2154)
The portfolio filter row was five flat dropdowns that wrapped across the
toolbar. It is now one Filters popover with labelled sections (Attention,
Target, Node, Drift, Evidence) and a count badge, the same shape as the
Fleet toolbar. Stack and Blueprint scopes set by other surfaces still show
as removable chips, and Clear filters still resets everything, including
filters only a deep link can set.

Option groups expose their section label and pressed state to assistive
technology, and the Node and Drift dropdowns are tied to their labels.

The Fleet filters popover now lays out its Critical Only button in the same
row as the other sections.
2026-10-19 14:34:47 -04:00
Anso d8891f3b5e fix: keep outline buttons and filter selects flush with the card in the Dim theme (#2153)
Outline buttons and four filter selects (Fleet toolbar, GitOps portfolio,
Readiness findings, Security images) painted with the page background, which in
Dim is darker than a card, so they read as black holes on cards. They now use a
control fill that is card-level in Dim. OLED and Light keep the page background
exactly as before.
2026-10-19 13:51:00 -04:00
Anso 09d454709b fix: remove a stack's mute rules when the stack is deleted (#2152)
* fix: remove a stack's mute rules when the stack is deleted

Mute rules created from a stack's Mute actions stayed behind after the stack
was deleted, and would silently mute a later stack created under the same name.

Deleting a stack now removes the rules that target only that stack on that
node. Rules that also cover other stacks (wildcards, several stacks, a whole
node) or other nodes are kept. Removal retracts the rule from remote nodes the
same way deleting it from settings does. It runs for locally hosted stacks,
for stacks deleted on a remote node through the hub, and for stacks withdrawn
from a remote node by a Blueprint. A failure is logged and never fails a
delete that has already happened.

* fix: log mute rule cleanup failures with a constant format string

The stack name is user supplied and was interpolated into the format string
of the cleanup failure logs. Pass it as an argument instead, so a name that
contains a format directive is printed literally. The logged text is
unchanged.
2026-10-19 13:49:04 -04:00
Anso 495bac574a fix: polish notification actions, topology icon, minimized progress, security sheet and timeline lanes (#2151)
- Notification rows reserve room for every visible action, so the mute menu
  no longer sits on top of long messages.
- The Networking topology tab and the "Show in topology" row action use the
  graph-star icon instead of the Git branch icon.
- A minimized deploy progress pill now closes itself once the task succeeds,
  even if the pointer was over the dialog when it was minimized.
- The scan comparison sheet no longer refetches and blanks every time its
  parent re-renders.
- The Security masthead drops the review action that the "Why Action needed"
  card already offers.
- Timeline pills sit centered in their own lane instead of straddling the lane
  above, and compose file backups now appear in the Backups lane.
2026-10-19 13:32:57 -04:00
Anso e9f824266b feat: open a published port at its configured service URL (#2124)
* feat: open a published port at its configured service URL

Store the address as the compose label sencho.service.url so a port click follows that host, and keep the generated link when the label is absent.

* fix: reject an escaping compose path before reading it

The compose filename scan checks the path stays inside the compose directory before it stats the file, so a rejected path is not treated as a missing file.

* fix: keep inherited labels and unsaved edits when a service URL is saved

A URL edit copies inherited labels onto that service, a changed compose filename keeps the draft unless the operator reloads, and a missing URL list still opens the generated link.

* fix: keep a service URL edit on the service that was opened

A shared label or service anchor is copied before the edit, clearing a merged URL does not restore the inherited address, and a running container opens only its own saved URL.

* fix: keep empty labels and dynamic ports on a service URL

A URL edit dropped empty labels and failed when a nested anchor was still used. A Docker-assigned port now opens that service's own URL, and the pencil stays unavailable when the service is not in the open compose file.

* fix: keep a service URL on the service that was edited

A shared URL value no longer changes another service. Open App does not follow a different service's dynamic address, a named port on its own bind address stays clickable, and a repeated list label takes the new address.

* fix: keep a service URL dialog on the stack it was opened for

The dialog now records the node and stack it was opened on, closes when that selection changes, and refuses the save before it can write a different stack. Clearing or replacing a list alias updates only the edited service. A URL supplied by another compose file stays on that file, and reloading the compose source refreshes the cached addresses.

* fix: edit a service URL without rewriting the rest of the compose file

A save splices the label into the original text, so octal modes, line endings, and unrelated keys stay as written. A node with no host keeps the plain port, a refresh keeps the last URL, and Open App opens its tab before the lookup.

* fix: keep service URL edits and compose saves inside the edited stack

- Refuse a compose file, or a stack directory, that resolves outside
  <compose root>/<stack>, so a read or save of one stack cannot reach a
  sibling stack's compose file. Covers saveStackContent too.
- Edit flow-style labels and services in place, copy anchored flow
  nodes without their defining key, keep source tokens such as 0440 and
  022 when labels are copied, append after the last real label (before
  trailing comments, and at an end of file with no newline), and keep
  the separator when an empty URL value is set or cleared.
- Check every patch before it can be saved: the file is read with YAML
  types and as untyped text, and anything that changes outside the URL
  label, drops a comment inside brackets, or touches a file with a
  custom YAML tag is refused with a clear message.
- Return the cached service URLs only for the node and stack they were
  read for, so a port never links to the previous selection.
- Log stacks the networking summary skips instead of dropping them
  silently.

* test: assert the networking summary skips a sibling-linked stack

A stack whose compose file resolves outside its own directory is skipped
with a warning. Assert the linked stack stays out of every bucket, the
healthy stack is still summarized, and the skip is announced.

* fix: keep service URL edits effective and prototype-safe

The effective-anatomy response now reports, per service, whether the open
main compose file controls the sencho.service.url label (serviceUrlEdit).
That is established from the authored chain: the label key presence is read
in the files merged after the open one (deploy spec order, or the resolved
main file plus the auto-discovered override). A later file that sets the key
blocks the edit whatever its value (invalid, interpolated, or equal); a later
file that uses include, extends, a custom YAML tag, or COMPOSE_FILE, or that
cannot be read, refuses the edit as unverifiable. The pencil lock, the save
guard, and a fresh re-read immediately before the write consume this
metadata, so the editor no longer infers ownership from the label value or
its absence. Post-save verification stays.

Service-keyed maps are built on null prototypes and read through own-property
lookups across the backend extractor, the response parser and resolvers, and
the local anatomy facts. Services named constructor, toString, hasOwnProperty,
or __proto__ now resolve the generated link when they have no label and keep
an own override when they do.

* fix: refuse a service URL edit the compose chain would shadow or reveal

A later bare `sencho.service.url` list item (`- sencho.service.url`) sets
the key to an empty value and shadows the open file, so the source
authority now counts it as a key contribution like the `key=value` form.

Clearing is not the mirror of setting: removing the open file's direct
label can surface an earlier chain file, the open file's own include, a
service's extends target, or its label_file. The anatomy response now
carries a per-service clear authority alongside the set authority, the
editor refuses an empty save the chain would keep winning or reveal, and
the patch helper refuses the same layouts before writing. Setting a URL
is unaffected.

The patch helper also treats a bare list item as the key when setting or
clearing, so a set no longer leaves a trailing empty entry that renders
as an empty value.

Operator docs describe the set and clear limits accurately.

* fix: refuse interpolated label keys and honor explicit dynamic ports

Compose interpolates a list label's key, so `- "${URL_LABEL}=https://app"`
sets `sencho.service.url` even though the authored source never names it.
A later file using that shape reported the open file as editable and the
write did not take effect; an earlier or later one failed to block a clear.
Such a key now marks the service unverifiable, so both guards refuse before
the save. A literal key with an interpolated value stays a known set.

`published: 0` (from `"0:80"` or `{ target: 80, published: 0 }`) asks
Docker to assign the host port, but the dynamic-owner predicate treated it
as a fixed host, so a container-port click and Open App fell back to the
generated node address. It now counts as automatic allocation, leaving UDP
binding, named-service isolation, and ambiguity refusal unchanged.

* fix: keep a saved service URL effective and a port link on its owner

- Read the open compose file's own source in the service URL set
  authority: a labels list whose key is interpolated (`- "${NAME}=..."`)
  makes that service unverifiable instead of editable, including when the
  file's other facts cannot be traced (a top-level `include` or a custom
  tag) and when the file cannot be read at all.
- Refuse a set and a clear in the editor buffer when the target service's
  label list carries an interpolated key, so an unsaved draft is caught
  before any write.
- Open a named container's own automatic binding URL on its card when
  Docker assigned it the host port, even when another service's saved
  file declares the same number, and refuse a saved-only single fixed
  declaration when another service's automatic binding could hold it.
- Carry the runtime main-port owner (`com.docker.compose.service`) from
  the bulk stack status through to Open App so it opens the address of
  the running service that owns the port.

* fix: keep a runtime container card on its own service URL

- Treat a named runtime container as direct ownership evidence: when a
  card carries both the compose service and the running container's own
  port, that service's configured URL applies even after the saved port
  declarations changed or were removed without a redeploy.
- Keep saved-model inference and its ambiguity refusals for callers
  without runtime port data, such as the anatomy footer.
- Cover the changed-port, removed-binding, hostless-remote, and rendered
  anchor cases in resolver and component tests.
- State in the stack management docs that the configured label applies
  first and the generated address is the fallback.

* fix: keep merged label text and a runtime card's generated link

Clearing a service URL from a labels map that also merges a YAML anchor
materialized the whole map from parsed values, which deleted operator
comments inside the map and respelled unrelated scalar tokens. The clear
now copies inherited entries from their own source text, leaves direct
entries untouched, and refuses layouts whose text cannot be kept.

A running container whose compose service has no URL override fell
through to saved-model inference and could turn ambiguous when other
saved services claimed the same host port. Runtime ownership now answers
with the service's own URL or the ordinary generated host and port.

Regression tests cover the preserved comments and tokens (block, flow,
CRLF, merge lists, nested merges, shared anchors) and the generated
anchor plus copy target for a runtime owner without an override.

* fix: wait for the first service URL lookup before opening a port

A first effective-anatomy lookup was read as "no override", so a
published-port link and Open App could open the generated host and port
while a configured service URL was still being fetched. The in-flight
entry now stays pending: the port renders as plain text until the outcome
is known, and Open App opens the tab in the click and navigates once the
lookup resolves. Completed outcomes (no label, a failed load, an older
node) keep the generated fallback, and a refresh that still holds the
prior model keeps rendering it.

* fix: keep a long service URL on the card and a refused path honest

A long host now truncates so the copy button and pencil stay clickable, and every port link keeps its open arrow. Clearing the only service URL removes the labels key, including when the address has a fragment. Opening a stack the server refuses shows that reason. Effective anatomy refuses a compose file or discovered override that links outside the stack. The editor shows a new URL only after the save is accepted.

* fix: keep the port link visible and drop a sole URL list

The port mapping stays on the card when the host is long, and on a phone the identity line wraps above the action buttons. Clearing a labels list that only holds the service URL removes the labels key, while a comment still keeps an empty value.

* fix: keep a cleared service URL renderable and the port controls reachable

A clear that left the labels map with no entries wrote `labels:` with no
value, which Compose rejects ("must be a mapping"), so the stack stopped
rendering and every pencil in it disappeared. The key now goes with its
last label, and a comment the removal would drop keeps the entries as an
empty map or list under the key instead. That covers a clear through a `<<`
merge, a sole label with a trailing comment, a comment line closing the
block, and a shared anchor whose other consumer keeps its address. A comment
between the removed entries is still refused before any write.

The container list now clamps to its pane, so a long host truncates and the
port link's copy button and pencil stay reachable at 1440px and below
instead of sitting past the pane edge. The copy button also gets the 44px
phone target the pencil already had.

A refused compose path answers with the condition instead of "path is not
allowed", and the parser no longer warns about the custom tags those edits
refuse.

Every layout the clear can produce renders under `docker compose config`.

* fix: keep the port line readable in a narrow container pane

The pane clamp left the action icons at full size, so the name and port link collapsed. The card now wraps those icons once the name column would drop below 12rem. A refused URL edit stays in the dialog until the address or the compose buffer changes.

* fix: keep the service URL editable after a failed list load

A failed first load keeps the generated link, explains that the list is unavailable, and offers Retry. A later failed refresh keeps the last address and the pencil. The pencil tooltip matches the copy control.
2026-10-17 18:33:13 -04:00
Anso 25c272eef1 fix: classify a synced sibling .env as a SOPS secret (#2131)
* fix: classify a synced sibling .env as a SOPS secret

A sibling .env fetched with sync was stored as plaintext, so deploy
injected the ciphertext into the container. Classify it with the same
detector used for env files.

* fix: refuse a stale SOPS capability for a synced .env

A same-commit pull reused a candidate that still recorded the synced
.env as plaintext, so apply could publish ciphertext and a manual
deploy was allowed. Replace that candidate, refuse apply and dispatch
when the stored recipients are short, and validate both through the
decrypt overlay.

* fix: guard applied SOPS files and keep decrypted values out of validation errors

Manual deploy followed the last deployed generation after an apply.
Validation errors echoed decrypted env values, and a configured .env
was read from the live stack instead of the candidate.

* fix: withhold decrypted compose diagnostics and pass overlay env files

Validation returned short, partial, and rewritten secret values. A context
directory deploy also omitted the decrypted root .env that validation had
already approved. That env file is accepted only at the path its name claims.

* fix: contain overlay env files against the area root at the filesystem call

The shared helper resolved a directory argument that could be the candidate
itself, and the override branch never reached the compose-root check.

* fix: keep project env-file selection consistent for nested and relocated roots

An authorized git-secrets or git-managed area override is recognized when
the data directory sits inside the compose root. Candidate validation
already accepted that area file while deployment refused it as escaping
the stack directory, so a source that validated could not deploy.

Configured project env files compare real paths with real roots, so a
relocated data or compose directory (a symlink to another volume) is
accepted while a link that leaves the authorized tree is still refused.

Regression tests cover both areas with sibling and nested data roots,
configured env files under a symlinked data or compose directory, and
the escaping-link refusals.

* fix: withhold compose diagnostics for any decrypted candidate

Candidate validation decided whether to withhold the Compose diagnostic
by scanning the decrypted env file for `KEY=value` assignments. Compose
also accepts YAML-style `KEY: value` env files, so a decrypted YAML
SOPS `.env` could disclose its values through the returned validation
result, the pull and webhook logs, and the recorded outcome.

Withhold the diagnostic whenever validation ran through a decrypt
overlay, without reading or interpreting the decrypted content, and
return the same fixed message for a thrown validation error. A failed
overlay decryption still reports its own classified error.

Regressions: a real YAML SOPS sibling `.env` with a real Compose failure
asserting the fixed message, retained ciphertext, and overlay cleanup;
and a Git apply deploy that consumes the decrypted env from the overlay,
keeps the live and staged copies encrypted, and removes the overlay
after a successful and after a failed Compose run.

* fix: withhold Compose output when a deploy runs with decrypted secrets

A SOPS decrypt overlay feeds plaintext into Compose, and Compose quotes
interpolated values back in its own diagnostics (`unable to get image
'<value>?': invalid reference format`). Validation already withheld its
diagnostic for that reason; deployment forwarded it, so a decrypted value
from a synced `.env` could reach the returned deploy error, the server log,
and the durable GitOps settlement.

Carry the same boundary to the operation that runs Compose: an operation
scope keyed on the overlay, entered by deployStack and
composeUpWithRecoveryOverride, read once by the shared subprocess executor,
which drops raw chunks (one notice replaces the stream) and rejects with a
fixed message instead of the collected output. The scope covers the
compensation and rollback compose runs nested inside a failed deploy.

Regression: an encrypted sync-env pull and apply/deploy through a stand-in
docker that fails with the decrypted value in its diagnostic. The value is
absent from the result, the logs, and the stored rows, while the stored
inputs stay ciphertext and the overlay is removed. A control pins that a
deploy without an overlay still reports Compose's own words.

* fix(gitops): render the deploy preflight from the decrypt overlay

A Git deploy runs Compose out of the decrypt overlay while the live stack's
.env stays ciphertext, but the mandatory external-network gate rendered the
live project. An encrypted value used where Compose needs a typed value was
resolved from ciphertext and refused the deploy before up: a published port
failed the render outright, and an external network name classified as an
invalid name. Candidate validation passed on the overlay, so a candidate that
validated could not be deployed through the path its refusal points at.

The gate now renders the project the deployment will use: the overlay working
directory, the authored compose files, the env-file arguments, and the same
project-name pin, all selected for the overlay and contained against the
git-secrets root at the spawn. A decrypted render withholds its Compose
diagnostic, which is where interpolated values are quoted back, and reports a
fixed message instead; the model itself stays in-process for the classifier to
read. The Pilot bind-mapping guard keeps rendering the live project, because
what it checks is where relative binds land on the host.

Tests cover an encrypted published port and an encrypted external-network name
through a real apply and deploy with the real gate, the resolver and render
plumbing, the withheld diagnostic, the containment refusal, and the unchanged
live path.

* fix(gitops): withhold the required-variable diagnostic over a decrypt overlay

The missing-variable guard ran its own `docker compose config --quiet` against a
GitOps decrypt overlay and parsed the full stderr for variable names. A Compose
`:?` message is itself interpolated, so
`${MISSING:?required variable ${SECRET} is missing}` makes Compose print the
resolved value of SECRET inside the very phrase that identifies the missing
variable, and the parser published that value as though it were a name. On a
decrypt overlay that value is a decrypted secret, and the refusal it produced
reached the returned result, the server log, and the durable GitOps settlement
on the deploy retry path, which resumes from an existing manifest with no
candidate validation in front of it.

The overlay branch now collects no output at all and decides on the child's exit
code: a resolved render passes, anything else refuses with a fixed withheld
message spliced between the guard's existing prefix and remedy. The check is
also bounded (20s, matching the preflight render) and spawned without pipes,
since an exit code is now its only signal. The live-project path is unchanged
and still names the missing variables.

A decrypt-overlay deploy whose model cannot be rendered was already refused by
the external-network gate on the same failure, so the outcome does not change;
only the wording and the ordering of the refusal move.

* fix(gitops): read the pilot bind check from the deploy's own project

The Pilot host-path guard rendered the live project and returned without a
decision when that render failed. A Git deploy runs Compose out of the decrypt
overlay, so a typed value held in an encrypted env file (a published port, an
external network name) failed the live render and the guard let the deploy
through. The external-network gate in the same change had started rendering the
overlay instead of the live project, which removed the refusal that used to
follow on the same failure: the deploy ran `up` with the overlay as its working
directory and resolved a relative bind under the temporary overlay instead of
the host path the mapping points at. A short bind can create an empty host
directory and replace a healthy workload against the wrong data.

The guard now reads the render that describes the project Compose will actually
run: the overlay when the deploy runs in one, the live project otherwise. A
mismapped pilot clears a bind only when the overlay's own model clears it, and
the trees a bind must stay out of are the overlay's stack area and the compose
tree, the same reach the live check has always had. An interpolated source is
why the live project cannot stand in for the overlay: ciphertext that Compose
accepts as a relative path resolves inside the live stack directory, while the
decrypted value beside it can name an ordinary external path, so reading live
would refuse a deploy the overlay carries out correctly.

When the render cannot be read the guard still makes no decision, and the
external-network gate refuses the deploy on that same overlay, so the failure
mode is unchanged. The four callers that deploy without an overlay are
untouched, and the refusal now names the paths that disagree rather than
quoting a bind source, which an overlay can interpolate.

Tests cover the refusal before any cleanup or `up`, the interpolated absolute
bind only the overlay resolves, a parent-segment escape, a bind naming the
compose tree, and a relative bind an included file resolves there.

* test(gitops): pin the 1:1 mapping control on the pilot bind guard

The refusal tells the operator to use a 1:1 mount with the same absolute path
on the host and in the Pilot Agent, so that configuration has to clear the
guard. With the same path on both sides there is no mapping to disagree with
and the guard decides on the mapping alone, before it renders anything.

The project in this regression carries a relative bind on purpose: if the guard
ever stopped deciding on the mapping alone it would render that project, find
the bind inside the compose tree, and refuse, so the pass is the mapping
decision rather than an unreadable render. A control asserts the rendered bind
really does resolve inside the compose tree.

* test(gitops): render the include fixture on Compose 2.x too

The included-file regression declared the same service name in the included
file and the including file, and left that service without an image, which
relied on Compose 5.x merging the two declarations. Compose 2.x refuses both
shapes: `services.<name> conflicts with imported resource`, and `service
"<name>" has neither an image nor a build context specified`. The unit-test
runner ships Compose 2.38.2 while Sencho builds 5.1.3, so the regression failed
on CI and passed locally.

The included service is now named apart from the including file's service and
carries its own image, so the model renders on either version. The bind still
resolves against the included file's own directory, which is the routing the
regression pins, so it still fails when the compose tree is dropped from the
guard's trees.

* fix(gitops): keep relative data binds on the live stack

A decrypt overlay was the Compose project directory, so an ordinary relative
data bind was created inside that temporary tree and deleted when the overlay
was removed. The deploy now rewrites those binds onto the live stack, leaves a
directly mounted secret file on the overlay, and refuses a mount that would
have to carry both before the stack starts.

* fix(gitops): check the stack path beside the directory lookup

The compose-file pin looked up the stack directory through a helper, so the
path check was not next to the filesystem call. The lookup now rejects a path
that leaves the compose root in the same function.

* fix(gitops): keep persistent storage on the live stack through a decrypt overlay

A named volume whose local driver binds a host directory keeps its device path
in `volumes.*.driver_opts.device`, not in a service mount, so the rewrite layer
never moved it and the volume was created against the overlay directory that
cleanup removes. The layer now rewrites that device onto the live stack too.
Which devices Compose relocates is answered by Compose: the model is read a
second time with path resolution on, and only a device that comes back absolute
while the authored one is project-relative moved with the project. A remote
driver export (`type: nfs`, `cifs`) or an `o: bind,ro` form keeps the value both
runs pass to the daemon. A device that resolves into the decrypt area is
refused. The Pilot host-path guard reads the same device form, so a rewritten
device cannot reach the host as the container path.

The bind decision now reads the overlay model without path resolution. A
relative source arrives in its authored form, so a mount that climbs two or more
parents keeps the live path instead of a directory beside the overlay, and a
relocated data directory no longer depends on how Compose resolved its own
working directory through a symlink. A relative source that resolves outside
Sencho's secrets area and outside the live compose tree is refused rather than
mounted where the stack never named it. The one-parent and context-directory
cases keep the paths they had.

* fix(gitops): decrypt a restored env that records no recipients

Recovery returned no decrypt overlay when a generation's stored capability
named no recipients, which is the state a generation stored before the synced
sibling `.env` was classified carries. Rolling back to it ran Compose over the
ciphertext, so the container received `ENC[...]` values.

The restored files are now the authority when the capability names no
recipients. The restored `.env`, the configured project env files, and the paths
the generation recorded are classified, an age document supplies its own
recipients, and the overlay is built from them. A file that no identity on the
node can decrypt refuses the rollback with `missing_key` instead. A genuinely
plaintext restored env stays on the plain path.

* fix(gitops): keep live storage, host paths and plaintext through SOPS runs

An authored compose file whose name ends with the generated storage layer's
name no longer disables the persistent-mount rewrite, so a relative bind or a
named volume's local device keeps the live stack as its host path instead of
the overlay directory the operation deletes afterwards. The layer is
identified by the path it owns rather than by an argv token, it is appended
once per argument assembly (the assembly that layers a recovery override no
longer adds it twice), and a compose file that already holds the reserved name
is refused instead of being replaced by it.

Two path forms the rewrite compares are now read in filesystem terms: Compose
renders a literal dollar as `$$`, so a secret file whose name holds one keeps
the decrypted overlay copy as its source instead of being mistaken for
ordinary data and pointed at the live ciphertext.

A rollback runs the same Pilot host-path guard as a deploy, over the invocation
it is about to execute: the captured file set, the env selection, the recovery
layer, and the decrypt overlay. A mount that resolves into a tree this node
manages while the host mapping is not 1:1 is refused before containers are
replaced, so a recovery point whose storage moved cannot attach the wrong host
directory.

Recovering a generation decrypts every restored env input that is still age
ciphertext, not only the files its stored capability recorded, so a generation
that recorded a recognized encrypted env file while its synced sibling `.env`
was classified as plaintext no longer reaches Compose as ciphertext. A file
both sets name keeps the role and path the generation recorded. Readiness is
per file: a file encrypted to several recipients is decrypted when any one of
those identities is held, and only a file no identity here can open refuses
with `missing_key`.
2026-10-17 18:13:54 -04:00
Anso e53627580b fix(docker): floor libexpat at the version that fixes CVE-2026-77214 (#2147)
The Docker Build & Scan job went red with a HIGH finding for libexpat 2.8.5-r0
(CVE-2026-77214, heap over-read in XML_ParseBuffer) in the runtime image, while
nothing in the branch changed a dependency or a base-image input. main's own
dev-image build and scan passed with the same gate earlier the same day, so the
finding reached the scanner's database between the two runs.

libexpat is not in the base image at all: the library is required by the `git`
package the runtime stage installs, so its version is decided entirely by that
stage's `apk upgrade --no-cache`. That layer is cache-busted by a date, so a fix
landing in Alpine's repo during the day is invisible to any build reusing a
layer cached earlier the same day, and the built image kept shipping 2.8.5-r0
after the advisory database learned that 2.9.0-r0 is the fixed version. This is
the same mechanism the pcre2 floor on the same line documents.

Naming the floor does two things: it changes the RUN command so the stale layer
cannot be reused, and it keeps a future cache from serving a version below the
fix.

The frozen pin `libexpat=2.8.5-r0` no longer resolves against Alpine's v3.24/main
index, which is what a fresh build now installs from: the index carries
2.9.0-r0, and `apk info -r libexpat` shows `git` as the requirer.

Verified in a throwaway image built from the pinned base-image digest with the
stage's own apk line: it installs libexpat 2.9.0-r0, and a scan of that OS layer
with the repository's Trivy config, the same CRITICAL,HIGH gate, and a database
that contains the advisory reports it clean. The full multi-stage build and the
image scan are CI's to confirm, since a local multi-stage build is expensive.
2026-10-17 16:22:17 -04:00
dependabot[bot] 7df1514ff1 chore(deps): bump the all-npm-frontend group across 1 directory with 25 updates (#2146)
Bumps the all-npm-frontend group with 25 updates in the /frontend directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.23` | `1.2.1` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.11` | `1.4.1` |
| [@radix-ui/react-context-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/context-menu) | `2.3.7` | `2.4.1` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.23` | `1.3.1` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.24` | `2.2.1` |
| [@radix-ui/react-hover-card](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/hover-card) | `1.1.23` | `1.2.1` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.15` | `2.1.17` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.23` | `1.3.1` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.18` | `1.4.0` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.7` | `2.4.1` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.15` | `1.2.0` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.7` | `1.6.1` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.3` | `1.5.0` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.21` | `1.2.1` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.16` | `1.4.1` |
| [cronstrue](https://github.com/bradymholt/cRonstrue) | `3.27.0` | `3.32.0` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.52.0` | `1.57.0` |
| [motion](https://github.com/motiondivision/motion) | `14.0.0` | `14.2.0` |
| [radix-ui](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radix-ui) | `1.6.7` | `1.8.1` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.7` | `14.6.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.4` | `26.6.5` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.1` | `6.1.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.71.0` | `8.71.2` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.2` | `8.3.4` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `5.0.3` | `5.0.4` |



Updates `@radix-ui/react-alert-dialog` from 1.1.23 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.11 to 1.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-context-menu` from 2.3.7 to 2.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/context-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/context-menu)

Updates `@radix-ui/react-dialog` from 1.1.23 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.24 to 2.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-hover-card` from 1.1.23 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/hover-card/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/hover-card)

Updates `@radix-ui/react-label` from 2.1.15 to 2.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.1.23 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.18 to 1.4.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.7 to 2.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.15 to 1.2.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slider` from 1.4.7 to 1.6.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-slot` from 1.3.3 to 1.5.0
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-tabs` from 1.1.21 to 1.2.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-tooltip` from 1.2.16 to 1.4.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `cronstrue` from 3.27.0 to 3.32.0
- [Release notes](https://github.com/bradymholt/cRonstrue/releases)
- [Changelog](https://github.com/bradymholt/cRonstrue/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bradymholt/cRonstrue/compare/v3.27.0...v3.32.0)

Updates `lucide-react` from 1.52.0 to 1.57.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.57.0/packages/lucide-react)

Updates `motion` from 14.0.0 to 14.2.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v14.0.0...v14.2.0)

Updates `radix-ui` from 1.6.7 to 1.8.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radix-ui/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radix-ui)

Updates `@testing-library/user-event` from 14.6.7 to 14.6.8
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.7...v14.6.8)

Updates `@types/node` from 26.6.4 to 26.6.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@vitejs/plugin-react` from 6.1.1 to 6.1.2
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.2/packages/plugin-react)

Updates `typescript-eslint` from 8.71.0 to 8.71.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.2/packages/typescript-eslint)

Updates `vite` from 8.3.2 to 8.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.3.4/packages/vite)

Updates `vitest` from 5.0.3 to 5.0.4
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.4/packages/vitest)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-context-menu"
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-hover-card"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: cronstrue
  dependency-version: 3.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: lucide-react
  dependency-version: 1.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: motion
  dependency-version: 14.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: radix-ui
  dependency-version: 1.8.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-frontend
- dependency-name: "@testing-library/user-event"
  dependency-version: 14.6.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@types/node"
  dependency-version: 26.6.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.1.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: typescript-eslint
  dependency-version: 8.71.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: vite
  dependency-version: 8.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
- dependency-name: vitest
  dependency-version: 5.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-frontend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 13:47:46 -04:00
dependabot[bot] fb55fc258b chore(deps): bump the all-actions group across 2 directories with 7 updates (#2140)
Bumps the all-actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `7.0.0` | `7.1.0` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |
| [github/codeql-action/autobuild](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |
| [ossf/scorecard-action](https://github.com/ossf/scorecard-action) | `2.4.4` | `2.5.0` |
| [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.38.2` | `4.38.3` |

Bumps the all-actions group with 1 update in the /.github/actions/start-app directory: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/043fb46d1a93c77aae656e7c1c64a875d1fc6a0a...cf430e030ddbb5b0abf93d22962f4752f3646cd9)

Updates `github/codeql-action/init` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/autobuild` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `ossf/scorecard-action` from 2.4.4 to 2.5.0
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/2d1146689b8cda280b9bc96326124645441f03bc...fa7117db83c52d787ae5a89f350999701505d78e)

Updates `github/codeql-action/upload-sarif` from 4.38.2 to 4.38.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2...24c54180a607b1449ed407dd24f251e4e9147c8d)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/820762786026740c76f36085b0efc47a31fe5020...949feb2413d6458794dcd2491c4babbbce0c15c1)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: ossf/scorecard-action
  dependency-version: 2.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-actions
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 13:47:24 -04:00
dependabot[bot] 19b9fcb11a chore(deps): bump golang from 85dc106 to f92b6ef (#2139)
Bumps golang from `85dc106` to `f92b6ef`.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.2-alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 13:47:02 -04:00
dependabot[bot] d2b564af6e chore(deps-dev): bump the all-npm-root group with 2 updates (#2138)
Bumps the all-npm-root group with 2 updates: [@playwright/test](https://github.com/microsoft/playwright) and [js-yaml](https://github.com/nodeca/js-yaml).


Updates `@playwright/test` from 1.63.0 to 1.64.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.63.0...v1.64.0)

Updates `js-yaml` from 5.4.2 to 5.4.3
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.4.2...5.4.3)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.64.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: all-npm-root
- dependency-name: js-yaml
  dependency-version: 5.4.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-root
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 13:46:35 -04:00
dependabot[bot] 07dff3fea7 chore(deps): bump the all-npm-backend group in /backend with 10 updates (#2136)
Bumps the all-npm-backend group in /backend with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [cron-parser](https://github.com/harrisiirak/cron-parser) | `5.10.1` | `5.10.2` |
| [express](https://github.com/expressjs/express) | `5.2.1` | `5.3.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.7.0` | `8.7.1` |
| [zod](https://github.com/colinhacks/zod) | `4.6.5` | `4.6.6` |
| [@types/dockerode](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/dockerode) | `4.0.1` | `4.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.6.4` | `26.6.5` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.71.0` | `8.71.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.2` | `8.3.4` |
| [@aws-sdk/client-ecr](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-ecr) | `3.1146.0` | `3.1149.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1146.0` | `3.1149.0` |


Updates `cron-parser` from 5.10.1 to 5.10.2
- [Release notes](https://github.com/harrisiirak/cron-parser/releases)
- [Changelog](https://github.com/harrisiirak/cron-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harrisiirak/cron-parser/compare/v5.10.1...v5.10.2)

Updates `express` from 5.2.1 to 5.3.0
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/v5.2.1...v5.3.0)

Updates `express-rate-limit` from 8.7.0 to 8.7.1
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.7.0...v8.7.1)

Updates `zod` from 4.6.5 to 4.6.6
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.6.5...v4.6.6)

Updates `@types/dockerode` from 4.0.1 to 4.0.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/dockerode)

Updates `@types/node` from 26.6.4 to 26.6.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `typescript-eslint` from 8.71.0 to 8.71.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.71.1/packages/typescript-eslint)

Updates `vite` from 8.3.2 to 8.3.4
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.3.4/packages/vite)

Updates `@aws-sdk/client-ecr` from 3.1146.0 to 3.1149.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-ecr/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1149.0/clients/client-ecr)

Updates `@aws-sdk/client-s3` from 3.1146.0 to 3.1149.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1149.0/clients/client-s3)

---
updated-dependencies:
- dependency-name: cron-parser
  dependency-version: 5.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: express
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: express-rate-limit
  dependency-version: 8.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: zod
  dependency-version: 4.6.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@types/dockerode"
  dependency-version: 4.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@types/node"
  dependency-version: 26.6.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: typescript-eslint
  dependency-version: 8.71.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: vite
  dependency-version: 8.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-ecr"
  dependency-version: 3.1149.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1149.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: all-npm-backend
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-17 13:46:16 -04:00
Anso 5ca0f6a38a fix(gitops): keep an accepted Blueprint commit on the next poll (#2126)
A review-policy fetch was staging that commit again and posting another pull-ready notice. Reuse the accepted generation when the fetch still matches, and skip the notice when nothing new is staged.
2026-10-17 13:32:15 -04:00
Anso 903419814c fix: serve the UI typefaces from the instance (#2132)
* fix: serve the UI typefaces from the instance

The page loaded its faces from a third-party host on every visit, including sign-in. Bundle those faces and stop allowing remote font and style sources.

* fix: keep the full subset coverage of the served typefaces

Bundling the faces left only the latin and latin-ext subsets declared, so
Cyrillic, Cyrillic Extended, Greek, Greek Extended, Vietnamese and the
symbols2 block rendered in a system face instead of the selected one.

Declare every subset the installed packages publish for the faces the
Appearance preference offers, with the range the package subsets each face
to, and hold the declarations to those ranges and to representative
characters per script in the font test.
2026-10-17 12:16:39 -04:00
Anso f7a18acd31 docs: read SLSA provenance with the tool that actually carries it (#2128)
* docs: read SLSA provenance with the tool that actually carries it

The provenance section told readers to run
cosign verify-attestation --type slsaprovenance, which fails on every
published image:

  none of the attestations matched the predicate type: slsaprovenance

The predicate types present on saelix/sencho:0.97.1 are the cosign signature
plus cyclonedx.org/bom, spdx.dev/Document and openvex.dev/ns. There is no SLSA
predicate. The release build sets provenance: mode=max, which embeds provenance
as a BuildKit in-toto OCI referrer; the cosign attest loop attaches only the two
SBOMs and the VEX, so no slsaprovenance attestation is ever written.

Point the section at the retrieval path that works and say what it is, so the
distinction between the BuildKit provenance and the cosign-signed SBOM and VEX
is explicit rather than implied. Verified against saelix/sencho:0.97.1: the
documented command returns a map keyed by linux/amd64 and linux/arm64 whose
entries hold an SLSA statement with buildDefinition and runDetails.

The publish workflow is unchanged.

* docs: call SLSA provenance inspection by its name

The section heading treated a Buildx metadata read as verification.
State that the command prints the predicate and does not check signer
identity or build claims, and point digest-pinned reads at the index
already authenticated by the signature step.
2026-10-16 23:26:24 -04:00
Anso daed461f93 fix(blueprints): detect a remote name conflict from the leaf stack list (#2129)
The ownership probe's remote arm read GET /api/stacks as an array of objects
carrying a name, but the endpoint answers with the stack names themselves. The
match therefore matched nothing, so every remote name read as clear: Check again
released a name_conflict row while the conflicting stack was still there, the
preview never flagged a remote create as blocked, and the deploy pre-flight
never ran. Only the leaf's own apply-local check held, which is why nothing was
ever written over.

Read the names the leaf sends, and fail closed when the answer is not readable:
a name found among the readable entries is still answered from the marker, while
a name absent from a list holding entries this version cannot read is an
unverified probe rather than a clear name. A non-array body counts as unreadable
rather than as an empty node. The local arm is unchanged.
2026-10-16 09:41:16 -04:00
Anso 6da06ce76e fix: keep readiness finding verbs inside the pane (#2127)
The findings table was wider than the fleet pane, so the resolving verb sat past a horizontal scroll. Size the columns as a share of the pane until there is room for stack and domain.
2026-10-16 08:35:11 -04:00
Anso 57e2d98a62 fix(docker): rebuild the Compose plugin from v5.6.0 (#2145)
* fix(docker): rebuild the Compose plugin from v5.6.0

The source-built Compose plugin moves from tag v5.1.3 to v5.6.0 (commit
42f48072). Its own go.mod resolves the module floors this stage used to
carry by hand, otel/sdk v1.46.0, google.golang.org/grpc v1.84.0,
moby/go-archive v0.3.3, moby/buildkit v0.33.1, and
containerd/containerd/v2 v2.4.1, so the explicit go get list shrinks from
twelve entries to three: golang.org/x/net v0.60.0, x/text v0.42.0, and
x/crypto v0.57.0, which this tag still resolves below their fixed
versions.

The bump clears the two MEDIUM findings that remained in that binary
(buildkit, and sigstore-go which this graph no longer contains) and drops
github.com/docker/docker from the graph altogether. Five VEX statements
that triaged docker/docker v28.5.2 as not_affected describe a module the
built binary no longer links, and the in-toto-golang statement becomes a
status: fixed record because v5.6.0 pulls v0.11.0, the version it was
waiting for.

Verified: the stage builds and the rebuilt plugin scans clean at MEDIUM
and above against the database CI uses; its command surface, config and ps
output shapes, and build fallback behavior match the previous tag on the
same inputs; and a full compose lifecycle through the shipped CLI
succeeds on a scratch stack.

* chore(vex): name the shipped in-toto version in the resolution record

An OpenVEX fixed statement describes the product version that carries the
fix, so the statement for GHSA-pmwq-pjrm-6p5r now names
github.com/in-toto/in-toto-golang v0.11.0 rather than the v0.10.0 build the
Compose v5.6.0 tag replaced. The status note still records the version it
supersedes.
2026-10-16 01:06:07 -04:00
Anso 4f1ed31f02 fix(docker): bump the Compose plugin's containerd to v2.2.10 (#2144)
The image vulnerability gate (CRITICAL and HIGH, exit code 1) now reports
CVE-2026-95837 against the source-built Compose plugin, which links
github.com/containerd/containerd/v2 v2.2.5 via the compose-builder stage's
dependency pin. The advisory is fixed in v2.2.7 and the sibling
CVE-2026-95838 in v2.2.8, so the pin moves to v2.2.10, the newest patch in
the v2.2.x line.

Both vulnerable paths are containerd daemon code that the Compose client
never reaches, so this is build hygiene rather than a live exposure fix.
It still blocks every preview, dev, PR and release image until the binary
is rebuilt.

Verified: the compose-builder stage builds, the rebuilt plugin runs
(version check both standalone and through CLI plugin discovery in the
runtime image), and a scan of the new binary against the vulnerability
database that failed CI reports no CRITICAL or HIGH findings, while the
same scan of the published image built with the previous pin reports the
reported CVE.
2026-10-16 00:04:45 -04:00