mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-10-25 21:20:25 +00:00
docs/tutorials-batch-3
2180 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
4f013338d1 | docs: add provider hook tutorial for GitHub and GitLab pushes | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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`.
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |