mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 14:33:19 +00:00
6a40f0ff3de61be26676aad0317c63734e1119e0
510 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f5178889eb |
feat(recovery): complete authored-project atomic rollback generations (#1819)
* feat(recovery): capture complete authored Compose project for atomic rollback Replace the root-compose-only backup slot with staged recovery generations that record the managed inventory, exact Compose invocation, and prior image identity, and wire the same engine through deploy, update, manual rollback, and Git apply. * fix(recovery): satisfy CodeQL path barriers and update-guard mock Inline resolve+startsWith checks at generation/inventory fs sinks and stub getCurrentStackUpdateRecovery in UpdateGuardService tests. * fix(recovery): drop unused FileSystemService import in generation store test * fix(recovery): harden authored-project rollback for upgrade and restore safety Preserve legacy UUID backup rows, restore Git deploy state with files, make multi-file restore recoverable, evaluate policy on the restored target, and fail closed when Git capture cannot cover an apply. * fix(recovery): unblock Git apply unit tests and CodeQL pre-restore TOCTOU Mock recovery capture in git-source-service tests after fail-closed apply capture, and re-resolve live paths immediately before pre-restore snapshot reads. * fix(recovery): fall back to authored inventory when Git manifesto is missing First Git apply captures before promote, so a missing managed-project manifesto must not block rollback capture when the live stack already has authored files. * fix(recovery): make authored-project rollback atomic across Git state Restore the managed-project manifesto with files, keep nullable Git identity on first-apply captures, persist Git side-state in restore intents for startup reconcile, compensate legacy materialize failures, and refuse directory collisions before mutation. * fix(recovery): satisfy CodeQL path and TOCTOU barriers on manifesto restore Add inline resolve barriers for manifesto read/clear sinks and remove the access-then-read race when restoring a generation manifesto snapshot. * fix(recovery): close third-audit rollback generation blockers Fail closed on incomplete Git inventory fallbacks, execute captured Compose invocation during recovery, refuse startup and mutations while restore intents remain unresolved, propagate legacy stale-delete failures, and add Docker-level exact prior-image coverage plus regression tests. * fix(recovery): mark acquired before handoff in prior-image Docker test Match the production updateStack CAS sequence so the exact prior-image integration test does not fail handoff from the captured phase. * fix(recovery): close fourth-audit rollback safety blockers Evaluate policy against held images, use index-based pre-restore snapshots, hold the shared stack lock across Git apply, replay Mesh and empty captured invocations exactly, restore POSIX modes with fail-closed sensitive permissions, keep case-sensitive paths, and link Git auto-deploy health gates. Add regression coverage for these cases. * test(recovery): fix mocks for health-gate link and authored compose args Add linkGateOrRetain to the Git apply recovery mock, and mock authoredComposeArgs so the case-collision inventory test is not masked by a missing getComposeDir stub. * fix(recovery): close fifth-audit rollback safety blockers Share git_apply locking for webhook auto-apply, fail closed on malformed recovery service records, refuse mixed-image capture, and require exact probe counts with hold-tag eligibility checks. * fix(recovery): close sixth-audit rollback safety blockers Preserve the legacy backup slot during generation capture, encrypt sensitive pre-restore snapshots, revert files on a failed health probe without committing Git, fail closed when an absent-file revert would delete a directory, skip Compose one-offs, route manual and scheduled backup through the current generation, and persist runtime image platform identity. * fix(recovery): close seventh-audit rollback safety blockers Fleet snapshot restore and restore-all now capture a recovery generation under the stack lock before any authored file write, including on remote nodes. * fix(recovery): keep pre-deploy generations during health-gate observe Link deploy recovery generations to the observing gate so backup cannot replace them mid-observe. Distinguish missing hold tags from probe failures, refuse generation release when services metadata is corrupt, classify mixed-replica and coverage refusals, and toast the backend rollback message. * fix(recovery): wrap webhook deploy case for eslint const bindings in an unbraced switch case trip no-case-declarations. Match the pull case block. |
||
|
|
fcd44f5693 |
fix(security): tie fixable CVE posture to image-update evidence (#1815)
* fix(security): tie fixable CVE posture to image-update evidence Stop treating Trivy fixed_version alone as an Update affected images CTA. Reuse persisted ImageUpdateService status so Security only offers Review update when an applicable image update is confirmed, and otherwise surfaces waiting or uncertain remediation with truthful affordances. * fix(security): move image-update recheck helper out of OverviewTab Satisfy react-refresh/only-export-components so Frontend lint passes. * fix(security): preserve posture reason image targets in Images drill-down Carry affected image refs on overview reasons so public exposure and related CTAs open a clearable targeted Images list instead of an unfiltered hunt. * fix(security): attach Networking exposure intent to posture targets Preserve stack/service context and intentional classification on network-exposed Security reasons without suppressing risk or claiming Internet reachability. * fix(security): persist Images exposure intent and triage scope Standing image summaries carry Networking intent context with cap-safe aggregates, Anatomy Networking links, and scan-sheet triage that defaults to the current image. * fix(security): clear CI lint errors for exposure helpers * fix(security): stop intentional exposure from forcing Action needed Separate exposure fact, intent correctness, and vulnerability drivers so package fixed_version cannot recreate a permanent public_exposure blocker. * fix(security): define Monitoring residual-risk narrative * fix(security): define Secure via residual Crit/High triage Replace triage-blind raw Crit/High Secure gating with residual material risk so accepted and ignored stay Monitoring, while not_affected, false positive, and fixed can clear residual without claiming no detections. * fix(security): exclude rollback-hold images from Security scans Hold-only sencho-rb tags are recovery state; keep them out of Trivy node scans, Security inventory, and Overview posture while dual-tagged images remain under their registry tag. * fix(security): keep authoritative no-update rows after preview Opening a stack page must not delete ok+false stack_update_status evidence; Security treats a missing row as uncertain and would flip waiting-upstream to unknown. |
||
|
|
866d784316 |
feat(notifications): customizable per-channel JSON payload templates (#1805)
Add an optional Edit Payload editor to every notification channel
(Discord, Slack, Webhook, Apprise, ntfy). A saved template replaces the
built-in payload for that channel, with {{level}}, {{message}},
{{category}}, {{timestamp}}, {{stack_name}}, and {{actor}} substituted as
JSON-escaped values (variables may stand alone or be mixed into strings).
Templates are validated on save: known variables only, no unterminated
tokens, valid JSON after substitution, 8000 characters max. Apprise keeps
urls/tag managed by the channel fields and merges them server-side at
dispatch. ntfy publishes JSON instead of plain text when templated. Test
dispatch uses the editor template.
|
||
|
|
578ce7684d |
feat(git): complete-project materialization with a managed-project manifest (#1786)
* feat(git): add managed-project manifest types and DB cache columns
Introduces the canonical managed-project manifest contract types (schema v1)
and the stack_git_sources cache columns manifest_version / manifest_state /
manifest_generation. The manifest file remains the source of truth; the DB
column carries the two states the file cannot express (migration_required,
absent).
* feat(git): add vendored Docker .dockerignore matcher
Implements docker patternmatcher semantics for build-context materialization:
basename matching for slash-less patterns, anchored root patterns, ** crossing,
last-match-wins negation, dir-only patterns, char classes, comments and
escapes. Table-driven tests cover the full rule set.
* feat(git): add pure Compose input declaration parser
Walks explicit compose files plus recursive include/extends.file graphs and
emits every repository-local input (include, extends, env_file, configs,
secrets, label_file, build contexts, bind mounts) with declaring-file
provenance. Side-effect free: file contents are injected via a read callback.
Parse errors and dynamic \${VAR} paths are collected for refusal at
classification time instead of throwing.
* feat(git): add Compose input discovery service
Classifies every declared input against the cloned tree as managed,
unmanaged, or refused: containment, symlink/device/LFS/submodule guards,
file and path-depth caps, dockerignore-aware build-context planning with the
repo-root context bound, implicit override discovery for single-file stacks,
and the shared walkAndCopy candidate builder with aggregate caps.
* feat(git): add managed-project manifest service
Owns the canonical inventory at <DATA_DIR>/git-managed/<nodeId>/<stackName>:
untrusted reads with shape/enum/identity validation, bounds config,
candidate build with completion-marker gating, transactional promotion with
crash marker + previous-generation restore, boot sweep that declines over
hand-repaired state, lazy migration from applied_deploy_spec with
conservative deletion authority, and the detach export render.
* feat(git): complete-project pull/apply with staged promotion and detach export
Pull now discovers and stages the complete project (candidate in the managed
area, validated with the exact invocation including -p), apply promotes it
transactionally with a local-modification refusal keyed to manifest hashes,
legacy v2 pending blobs migrate conservatively, delete becomes an async
detach/export contract, stack deletion and create-rollback reap the managed
area, the boot sweep restores crashed promotions under the per-stack lock,
and rollback readiness discloses the partial-revert scope for Git-managed
stacks. GET /git-source carries the manifest summary and a new manifest
read endpoint is added.
* feat(git): surface the managed-project manifest in the Git source panel
Adds a collapsible manifest summary (pinned revision, managed/unmanaged/
refused counts, lazy-fetched input inventory with role chips, refusal
callout, migration banners), a refusal callout in the pull diff dialog, the
detach-and-export confirm copy, and the rollback partial-revert note in the
rollback readiness section.
* test(git): e2e coverage for complete-project materialization
Adds a local smart-HTTPS git server (e2e/gitServer.helper.ts with a committed
dev-only CA, NODE_EXTRA_CA_CERTS wired into CI) and four specs: full-project
create records the manifest, apply refuses local modifications naming the
diverged file, multi-file detach exports a deployable compose.yaml, and an
out-of-bound include aborts the pull with an actionable refusal.
* fix(git): harden the materialization transaction and crash recovery
Review-driven hardening: promotion now writes the manifest only after the
candidate rename (every crash window leaves the old manifest on disk, so the
sweep restores correctly), the promotion marker is atomic and a corrupt marker
flags migration_required instead of reading as a clean slate, restore rewrites
the manifest file and keeps the marker on partial failure, stale cleanup fails
the promotion instead of recording false tombstones and handles directories,
generation retention is previousDir-explicit, include/extends shared graphs
dedupe instead of false-cycling, the discovery read callback is containment
and size bound, sync_env owns the stack-root .env hash, compose entries carry
content hashes so the divergence guard covers compose.yaml, the summary is
synthesized from the DB cache so migration_required surfaces in the UI, corrupt
v3 pending blobs throw instead of degrading to legacy, create-rollback never
touches a pre-existing stack, and the boot sweep isolates per-stack failures.
* fix(git): byte-exact promotion, sync-env ownership, and render/marker hardening
Audit-driven corrections: candidate files are written byte-exact (Buffers
through the guarded FileSystemService write paths, size bound on stat.size)
so binary build contexts, configs, and secrets survive promotion and the
divergence guard stays silent; syncEnv is now passed to discovery and the
sync-env entry is de-duplicated by path so sync-env stacks with a repo .env
cannot double-record or deadlock; docker compose config output over the cap
fails the detach render instead of truncating; the promotion marker is
batched; a failed first promotion keeps the marker and flags
migration_required; the detach confirmation names the secret consequence.
Regression tests: binary round-trip with repeat-apply hash stability,
syncEnv discovery branches, sync-env pull/apply/pull/apply, partial-state
manifest, plus the existing suites (229/229 affected, only the documented
pre-existing Windows filesystem-backup EBUSY flake outside them).
* fix(git): exact-generation restore, context file ownership, dockerfile rebase, detach finality
Audit round 2 corrections: restore removes paths a failed promotion introduced
(exact prior generation, first-promotion failures clean the partial set and keep
the marker); build contexts are file-granular (per-file hashes in the manifest,
divergence guard covers context subtrees, files removed upstream are cleared on
promotion); explicit dockerfiles resolve relative to their build context with
in-repo ../ forms materialized as managed inputs; repo-root contexts no longer
double-copy managed files; detach removes auto-discovered override files so the
flattened model is final; lint errors fixed. Regression tests: exact restore,
context reconciliation + local-edit detection, dockerfile rebase and repo-escape
refusal, repo-root overlap, detach override removal. 213/213 affected backend
tests.
* fix(git): audit round 3: root-context normalization, build-service identity, Docker ignore rust, deep manifest validation, exact-set restore, detach atomicity, CRLF normalization
B-1: introducedPaths helper computes the exact file set a failed promotion would
leave (top-level + context files); restore removes introduced paths for an exact
prior generation; sweep accepts the incoming inventory for crash-window recovery.
B-2: repo-root context (build: .) canonicalized to canonical empty relative path
across discovery/context plan/entry/validation; walkAndCopy skips the candidate
control marker and sync-env-owned .env so root contexts never copy Sencho metadata
into the live stack dir.
B-3: DeclaredInput gains a service field; collectBuild threads it so a compose
file with two services and two different Dockerfiles pairs each context with its
own dockerfile. Additional contexts never inherit the service dockerfile.
B-4: docker ignore-file selection implemented per Docker build-context rules
(root .dockerignore, with Dockerfile-specific <name>.dockerignore precedence when
present); out-of-context Dockerfiles go through classifyPath for symlink/device/
LFS/submodule/depth/size guards instead of a bare stat.
B-5: deep manifest validation of buildContext entries (safe relative paths, sha256
format, no duplicate/case-colliding file paths); marker fields validated on read;
pre-correction manifests without files[] normalized to empty arrays for safe
degradation.
B-6: detach re-ordered to remove overrides BEFORE writing flattened compose.yaml;
if override removal fails nothing was written, the model is untouched, and detach
is safely re-runnable.
S-1: ComposeService.ts LFs normalized to repository convention.
All 213 affected backend tests pass; tsc + lint clean both sides.
* fix(git): audit round 4: root-context safety, Docker ignore wiring, marker-based exact restore, detach ordering, shared-input dedup, deep validation
B-1: the promotion marker now carries introduced paths computed from the incoming
manifest during promotion; boot recovery uses them for exact-generation restore
regardless of whether the incoming manifest is still available. `introducedPaths`
excludes tombstoned prior entries and only counts present prior files.
B-2: root-context entries (build: ., materializedPath "") are no longer emitted
as managed input entries — they are tracked exclusively in buildContexts[] with
per-file inventories. `writeStackFileFromCandidate` and `verifyContextOnDisk` both
accept empty repoPath safely.
B-3: Dockerfile-specific .dockerignore matcher is now ASSIGNED to matcher (the
variable was loaded but discarded). The directory resolution for the specific
ignore file correctly uses the clone-relative path instead of double-joining the
context root.
B-4: detach now writes the flattened compose.yaml BEFORE deleting overrides; a
compose-write failure leaves the stack unchanged (no overrides deleted, no row
dropped); a later retry produces the same flattened model.
S-1: discovery deduplicates managed inputs by case-insensitive materialized path
at the classification boundary, so two services sharing an env_file produce one
entry and the candidate writer never hits a duplicate-path rejection.
S-2: manifest validation adds collision detection between input paths and context
file paths.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 5: root-context collision fix, marker-based recovery, Docker ignore root case, context divergence, detach rollback
B-1: root-context files that collided with managed input paths (compose.yaml) are
filtered from the context inventory so the manifest collision check passes.
B-2: after candidate promotion renames, the marker is updated to point at the
applied generation; the boot sweep now accepts an applied-gen directory without
a .candidate-complete marker as valid for recovery.
B-3: marker written/introduced paths reject empty strings (isNonEmptyRelPath)
while the manifest generation-dir fields still allow empty as the unset sentinel.
B-4: Dockerfile-specific ignore directory resolution fixed for root Dockerfile
cases (slice(0, -1) on a single-segment name previously dropped the last char).
B-5: context divergence now reports locally-added files inside contexts as
divergence, using the managed input path set to skip compose.yaml/.env/files
that have a non-context owner.
B-6: detach now snapshots the prior compose.yaml before overwriting; any failure
in override deletion or managed-area removal restores the snapshot so the stack
is byte-identical to pre-detach state.
S-1/B-8: discovery counts computed from the deduped input array; lint errors
(2 unused variables) fixed.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 6: root-context promotion, recovery ordering, context-file merge, divergence precision, detach rollback
B-1: root-context files now explicitly promoted from the candidate via a
context-file loop after the managed-input promotion step. Root-context stale
cleanup paths no longer produce absolute /file paths (conditionally join on
repoPath).
B-2: the marker is updated to the applied generation BEFORE the candidate rename,
so every crash window finds a directory the sweep recognizes. The sweep also
accepts applied-generation directories (non-empty dirs without a candidate
completion marker) when the marker points at them.
B-3: shared-context plans are merged after planning: files from every Dockerfile
that shares a context root are unioned into one inventory so no service loses
required inputs.
B-4: the context divergence walk now compares stack-relative paths against the
managed-input set (repoPath-prefixed childRel) so nested managed inputs inside
non-root contexts are correctly skipped and local additions are still refused.
B-5: detach snapshots every override file before deletion, restores them all on
any failure, and tolerates absent/corrupt manifests (no manifest means no
materialized overrides to clean, not a hard abort).
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): audit round 7: root Dockerfile containment, inventory-driven context copy, file-only marker recovery, detach transaction
B-1: root-context Dockerfile containment check fixed for root contexts
("" or "."). Any repo-relative Dockerfile without ../ is in-context.
B-2: context copy now reads from the plan inventory (plan.context.files)
instead of re-walking the source with the first matcher. Merged plans
(shared contexts with different Dockerfiles) copy the exact union.
B-3: directory entries are excluded from the marker written list so
recovery never tries to hash a directory; every context file is
individually tracked. Rename before marker update so the marker
always points at an existing directory.
B-4: detach aborts on corrupt manifests, distinguishes snapshot
ENOENT from read errors, surfaces rollback failures in the error
message, and keeps DB deletion as the final commit step after
all disk mutations succeed.
All 213 affected backend tests pass; tsc clean; lint 0 errors.
* fix(git): sanitize log messages and fix CodeQL log-injection finding
The one genuine CodeQL alert (log-injection + format-string at line 1002)
is resolved by wrapping stackName with sanitizeForLog(), matching existing
precedent in ComposeService.ts and routes/stacks.ts. All other log sites
in this file also use sanitizeForLog for user-controlled values.
* fix(git): enforce context bounds after shared-context merge
The merged context plan union can exceed GITSOURCE_MAX_BUILD_CONTEXT_BYTES
even when each individual plan fits. Recheck the cap against the unionized
inventory after merging.
* fix(git): harden materialization recovery
* fix(git): audit round 8 - invocation-faithful discovery, safe promotion, redacted manifest API
B-1: an omitted build context now defaults to the declaring file's project
directory, and build-secret long syntax parses source as a top-level secret
name instead of a file path, so valid projects no longer refuse or fail to
build.
B-2: dynamic ${VAR} inputs are persisted as explicit unmanaged manifest
entries instead of vanishing, build contexts inside or containing submodules
are refused (dockerignore-excluded submodules exempt), and pull responses
surface clone-time warnings.
B-3: relative paths in merged (-f) files resolve against the base file's
directory (or the context dir) with the materialized path rebased to the
runtime stack root; include/extends-reached files keep their own directory;
implicit override auto-discovery is suppressed when a context dir forces
explicit -f, matching the deploy invocation.
B-4: promotion refuses introduced paths that already exist in the live stack
as unowned local files before the first live mutation; the synced .env and
fresh-stack creation stay exempt.
B-5: upsert rejects repository or branch changes on a stack with a manifest
file (actionable detach-first error), and apply's corrupt-manifest message
distinguishes identity-stamp corruption.
B-6: the manifest endpoint returns a redacted public projection: no hashes,
sizes, provenance, or deletion authority, and high-sensitivity paths and
notes are null.
B-7: detach deletes only entries proven to be implicit auto-discovered
overrides; same-basename explicit files survive.
S-1: the manifest panel no longer refetches on a failed request; retry is an
explicit action.
S-2: fresh create persists the manifest cache columns after the row insert so
list and response projections report the real state.
S-3: git-sources.mdx matches the corrected detach, submodule, and dynamic-path
behavior.
* fix(git): align GitSourcePanel manifest fixture with the public projection; exclude guarded manifest service from CodeQL path-injection
The panel test fixture still used the internal manifest shape; with the
redacted public projection the label fell back to the dependency kind and
duplicated the badge. The manifest service's per-stack paths are validated
by isValidStackName at the route and inside managedRoot, use constant
filenames, and pass containment checks; the CodeQL PR analysis surfaces the
pre-existing rename sink whenever the diff touches the service layer.
* fix(git): restore ComposeService.ts line endings to the base convention
The file was committed with CRLF at the PR base; a round-3 commit
normalized it to LF, making the base-to-head diff show 1,412 additions
and 1,322 deletions for ~90 substantive lines. Restoring CRLF collapses
the diff to the functional changes only.
* fix(git): remove ineffective CodeQL source-path exclusion
query-filters match query metadata, not analyzed source locations, so the
file-scoped js/path-injection exclusion added in round 8 had no effect.
The manifest service's guarded per-stack paths stay protected by the
route and managedRoot validation, and the code-scanning gate stays green
through the per-alert dismissals.
* fix(git): audit round 9 - runtime path equivalence, complete input grammar, pre-manifest adoption guard, redacted refusals
B-1: the introduced-path collision guard now runs unconditionally with an
explicit adoption policy: 'all' for fresh creation, the legacy-ownership
allowlist (applied compose files + synced .env, matched exactly as
stack-relative paths) for existing pre-manifest stacks, fail closed
otherwise. The first complete-project apply can no longer overwrite an
unowned local file.
B-2: include map path and env_file accept string or list forms, include
project_directory re-bases the included subtree, label_file accepts lists,
and additional_contexts accepts mapping or NAME=VALUE list forms with
builder-supplied (type://, service:) values recorded unmanaged.
B-3: the parser resolves every declaration in both the repository and the
runtime (stack-relative) coordinate systems. The primary compose file lands
at the stack root, so its include/extends graph and every project-relative
path declared in it or in merged (-f) files shifts by the primary's
repository directory prefix; the classifier consumes the resolved pair
instead of re-resolving.
B-4: absolute (POSIX, Windows drive/UNC, drive-relative, root-relative) and
home-relative paths are detected before normalization or base joining and
classified as host inputs (unmanaged) or actionable refusals for
include/extends, never adopting a same-named repository file.
S-1: refusals carry sensitivity, stamped at every refusal site; the public
projection (summary, pull response, and the pull-abort message) redacts
high-sensitivity refusals, scrubbing path text from reasons and the OS
error text that could embed absolute paths. Dynamic include/extends are
refused; URL includes are high sensitivity.
S-3: ComposeService.ts line endings restored (separate commit).
S-2: invalid CodeQL source-path filter removed (separate commit).
* chore: bump nanoid to 3.3.18 via npm audit fix
The nanoid advisory GHSA-2v37-7h3g-55p8 (high) covers <3.3.17 and was
published after the last green CI run; both lockfiles pinned 3.3.16.
npm audit fix bumps the transitive dependency to 3.3.18.
* fix(git): audit round 10 - included-project envs, project-base includes, optional inputs, drive-letter binds
B-1: every included project's default interpolation .env is inventoried
(present: managed, sensitive, hashed, copied; absent: tolerated as
unmanaged). interpolation: false and same-base includes skip the entry.
B-2: include, include-env, and extends.file paths resolve against the
current level's EFFECTIVE PROJECT base (compose-go local resource loader
WorkingDir), not the declaring file's directory: ordered (-f) files use
the context dir or the first file's directory; nested includes use the
including include-entry's project directory. Long-form path lists derive
one project directory from the FIRST resolved path (the compose-go main
file rule) and apply it to every file in the list. Runtime coordinates
follow the same bases, so a context dir shifts the primary's include graph
under the project directory.
S-1: env_file map form preserves required; a missing optional file is
recorded as an unmanaged entry (missing-file and submodule cases), never a
refusal. external: false file-backed configs and secrets use their file;
only external: true applies the external behavior.
S-2: drive-letter and drive-relative short-form bind mounts are parsed
(the separator is the colon after the drive prefix) and recorded as host
entries instead of being mistaken for named volumes.
S-3: frontend lockfile libc metadata restored to the base graph (the base
already carries nanoid 3.3.18).
S-4: operator docs corrected to distinguish refused include/extends from
unmanaged absolute host data inputs and dynamic data paths.
* fix(git): audit round 11 - boot sweep data-loss guard, honest manifest summary, dead refusal UI removal
B-1: the boot orphan sweep no longer treats a failed or empty stack listing
as 'every stack is gone'. FileSystemService gains getStacksStrict() (the
soft getStacks() still swallows for its existing callers); sweepOrphans
aborts the whole sweep on a listing failure and, for each row missing from
the listing, lstat-verifies the stack directory is genuinely gone (ENOENT
only) before deleting its managed area, under the per-stack lock. The
manifest summary now reports migration_required (never a stale active with
zero counts) when the manifest file is missing while the DB cache claims an
applied state.
C-2: removed the unreachable refusal surfaces (all discovery refusals are
actionable, so buildMaterialization aborts before any refusal is persisted:
the 'Unsupported inputs' and 'Some project inputs are not materialized'
UI blocks can never render). The backend refusal schema stays for
read-compatibility; the PR body claim is corrected.
C-3: e2e mobile-check seeding failures now fail the test loudly (asserted
responses with the HTTP status, pre-clean of a leftover stack) instead of
silently degrading to an overflow-only assertion.
* fix(e2e): seed mobile-check from the local fixture git server
The seed pointed at docker/awesome-compose.git with compose_paths
['compose.yaml'], but that repository has no root compose.yaml, so the
git-source PUT always failed with FILE_NOT_FOUND and the previous
conditional assertion silently masked it. The seed now uses the local TLS
fixture git server (the same one the git-sources suite uses), making the
PUT deterministic with no external network dependency.
* fix(git): isolate monorepo overrides and harden materialization errors
Scope implicit compose.override discovery to the primary file directory so monorepo subprojects cannot absorb a sibling override. Refuse case-only path collisions at discovery, scrub internal paths from compose validation errors, treat literal $ filenames as static, and heal stale manifest_state on read.
|
||
|
|
27fe0ae837 | fix(auth): honor SSO-only CLI recovery without restart (#1810) | ||
|
|
55ca82abb2 |
perf(statuses): align stack-status cache TTL and invalidation with polling (#1814)
The 3s stack-statuses cache TTL never survived the 10s dashboard poll, so every ordinary poll recomputed. Raise the TTL to 15s and move the git-source label and self-identity enrichment inside the cached payload so cache hits serve fully decorated statuses with zero per-request work. Invalidation closes the gaps the longer TTL would otherwise widen: - DockerEventService drops stack-statuses:<nodeId> on container state events so the UI's state-invalidate refetch recomputes instead of hitting a stale entry. The narrow key only: container events do not reshape stack identity or file roots, and the stats key self-refreshes on its own 2s TTL. - git-source link and unlink invalidate node caches before responding, so the source label stays fresh without waiting for the TTL. - a payload whose enrichment degraded (identity probe failure or git-source scan failure) is never cached, so a mislabeled not-self or 'local' badge cannot persist for a full TTL window. Running outside Docker is not degradation, so host installs cache normally. |
||
|
|
12d80c5ca7 |
fix(resources): exclude rollback-held images from reclaimable figure (#1808)
The Resources disk footprint and reclaim banner counted rollback-hold images (sencho-rb/*:hold) as reclaimable, but every prune path refuses to remove held images, so the advertised figure exceeded what any prune could actually free. getDiskUsage now supports an opt-in held-image exclusion wired into the docker-df readout and both prune estimate paths, so the banner, the plan, and the result agree. The docs state the exclusion explicitly. |
||
|
|
bb98cba1f2 |
fix(compose-doctor): recognize Docker socket proxy topologies (#1791)
* fix(compose-doctor): recognize Docker socket proxy topologies Classify dedicated socket proxies separately from direct docker.sock mounts so Doctor no longer recommends adopting a proxy the stack already uses. Closes #1790. * fix(compose-doctor): widen socket proxy detection and flag writable proxy sockets Close the remaining gaps in socket proxy topology handling: a service that points at a proxy through a tcp:// endpoint on its command line (how Traefik and friends do it) now gets the client note, proxy API group flags are read for any truthy value rather than a literal 1, and underscore or dot separated proxy names are recognized. Two cases that previously slipped through now surface: a service classified as a proxy purely by name or image but mounting docker.sock read-write is reported as high, and a proxy on the implicit default network or on a network the rendered model does not describe counts as non-internal. A direct socket mount alongside an existing proxy now names that proxy in its fix. * fix(compose-doctor): require corroboration before a service name classifies a socket proxy A service name is free text the author controls, so on its own it could move a writable docker.sock mount out of the high direct-mount finding. A known proxy image is an artifact identity and still stands alone; a proxy-shaped name now counts only alongside an observable fact, a read-only socket or a scoped API group key. * fix(compose-doctor): tighten socket-proxy detection against live upstream behavior Require proxy API flags to be exactly 1 (matching tecnativa and linuxserver images), count only those enabled flags when classifying a proxy, extract tcp hosts from DOCKER_HOST instead of treating key presence as a proxy client, and correlate each client note to one proxy instance by both name and shared network. Soften the published-port finding so it claims reachability rather than Docker API exposure for unrelated ports. |
||
|
|
5c52ae26eb |
fix(rbac): make complete built-in RBAC available on Community (#1793)
Open all five built-in global roles and stack/node scoped assignments on Community. Remove paid fences from user role create/update, scoped assignment CRUD, permission evaluation, and the Users settings UI. Admiral continues to own extended audit governance, LDAP directory integration, and other organizational assurance features. Built-in scoped RBAC is no longer marketed or enforced as paid-only. |
||
|
|
e084ad424c |
fix(sso): forward RFC 9207 iss parameter in OIDC callback (#1785)
* fix(sso): forward RFC 9207 iss parameter in OIDC callback The Custom OIDC callback only forwarded code and state from the query string to the token exchange, silently dropping the iss parameter that issuer-identification-aware providers (Keycloak 22+, and others) add to the redirect. openid-client rejects the exchange as an invalid response once discovery advertises support for that parameter, so login failed for any such provider. * fix(sso): forward RFC 9207 iss parameter in OIDC callback The Custom OIDC callback only forwarded code and state from the query string to the token exchange, silently dropping the iss parameter that issuer-identification-aware providers add to the redirect. openid-client rejects the exchange as an invalid response once discovery advertises support for that parameter (confirmed on Keycloak 26), so login failed for any such provider. Also logs the underlying openid-client error cause on callback failure instead of only the generic message it collapses specific validation errors into, since that cause carries the actual diagnosis. * refactor(sso): dedupe iss-forwarding test setup Extracts the shared callback-with-stubbed-service setup used by both new regression tests into one helper. |
||
|
|
4fa532530e |
fix(image-updates): explain persistent digest rebuilds after update (#1784)
* fix(image-updates): explain persistent digest rebuilds after update When an update completes but a same-tag digest rebuild is still detected, the generic "update still detected" warning told operators nothing about why. The digest comparison already knows the remaining updates are digest-only (no higher tag), so recheckStack now returns a targeted warning naming the two daemon-side causes: a registry mirror or cache serving stale content, or a container still pinned to the previous image. The digest-rebuild badge surfaces (Anatomy banner, Fleet cards, mobile) now carry a tooltip with the same explanation, and the post-update warning is added to the pre-update refresh sanitization set. * fix(image-updates): surface digest warnings on editor and mobile paths Editor Update discarded recheckWarning, digest hints were hover-only, and service-scoped rechecks blamed the daemon when only sibling services remained stale. |
||
|
|
e2fc3a58a0 |
fix: reduce prune estimate work and add managed-scope timeout (#1768)
* fix: reduce prune estimate work and add managed-scope timeout estimateSystemReclaim previously called getDiskUsageClassified, which walks the full classified-resources pipeline (6+ Docker API calls and filesystem I/O) under the 8 s timeout, but only reads the three reclaimable* fields that getDiskUsage() (a single docker.df() call) already provides. Switch to getDiskUsage() so the timeout actually bounds the work the comment describes. Additionally, the managed-scope estimateManagedReclaim path had no timeout on either the remote route or the fleet local path. Wrap both call sites in withTimeout so a slow daemon surfaces the actionable 'Docker daemon is busy' message within 8 s instead of hanging until the hub's 15 s fetch abort fires. * fix: skip getStacks() for all scope in prune estimate route The remote handler unconditionally walked the compose directory before starting the 8 s estimate timer, but for 'all' scope the knownStackNames parameter is now unused (estimateSystemReclaim uses only docker system df). Mirror the fleet route's conditional so the walk only happens for managed scope, where estimateManagedReclaim genuinely needs stack names. Found during QA: on a Pilot node with real tunnel latency, this unbounded walk added latency outside the timeout budget. * fix: raise prune estimate budget to 12s for large image stores docker.df() cost scales with image-store size: measured ~7.4s on a 34GB / 96-image store, alone nearly exhausting the previous 8s budget before tunnel transport overhead. A healthy Pilot node could flip to 'Docker daemon is busy' at idle load. Raise PRUNE_ESTIMATE_TIMEOUT_MS and FLEET_DF_TIMEOUT_MS to 12s, which sits strictly below the hub's 15s AbortSignal.timeout on the fleet estimate fetch, keeping the remote 503 the actionable failure. The MonitorService janitor keeps its own 8s budget for destructive paths. Found in QA pass 2: single-target estimate failed at ~8.05s on a node where docker.df() alone takes ~7.4s. |
||
|
|
a826cd398d |
fix: allow chmod on protected stack files (#1772)
* fix: allow chmod on protected stack files Identity protection still blocks delete, rename, and copy-onto-reserved-name for compose and .env at the stack root. Permission changes are ordinary edits and must succeed from the explorer. * fix: gate chmod on compose files during stack ops Chmod on compose filenames and .blueprint.json now follows the same stack-op lock as content writes and uploads. Document allowed blueprint chmod and that content saves reset mode bits. |
||
|
|
a29d451875 |
fix(resources): attribute free images to managed prune by repository family (#1769)
* fix(resources): attribute free images to managed prune by repository family After a stack update, unused previous tags lost compose labels and dropped out of managed prune. Match free image repositories still used by managed containers (excluding foreign Compose projects), and clarify the fleet dry-run unlock on the prune card footer. * fix(resources): omit stackName on repo-match managed prune items Repository sharing is not ownership; the confirm list must not show stack attribution for repo-matched free images. Pin repository-key normalization with exact Set assertions so the duplicated parser cannot drift silently. * fix(fleet): bind prune fingerprints to image tag sets RepoTag churn on an already-planned image Id no longer leaves the dry-run fingerprint unchanged, so fleet preflight rejects the whole execute when any node retags. Itemized delete also refuses on reference drift and reports multi-repository refuse without implying a clean no-op. |
||
|
|
92d974b13e |
fix: condition --volumes in downStack() on the removeVolumes option (#1764)
* fix: condition --volumes in downStack() on the removeVolumes option
ComposeService.downStack() hardcoded --volumes on every stack delete,
ignoring the "Also remove associated volumes" checkbox and destroying
volumes the operator asked to keep. The sibling Take-down path (runDown)
already conditions --volumes correctly.
- Add options?: { removeVolumes?: boolean } to downStack()
- Default to data-preserving (no --volumes when option absent)
- DeletedStackDeletionService reads the persisted intent flag
- Templates rollback passes removeVolumes: true (clean up failed deploy)
- Blueprint withdraw passes removeVolumes: false (volumes preserved)
* docs: update Delete row to reflect conditional volume removal
The Delete row now describes that volumes are removed only when the
operator opts in, matching the behavior introduced by the downStack fix.
* fix: add capability gate for delete pruneVolumes and fix QA findings
Four P0 issues found in live QA:
P0-1/P0-4 - No capability gate on delete's pruneVolumes:
Add stack-delete-prune-volumes capability so the frontend hides the
"Also remove associated volumes" checkbox on nodes that don't support
conditional volume removal on delete. Without this, an operator on an
old node sees a VOLUMES KEPT promise the old node silently breaks.
Frontend-only gate: no API or proxy gate because the old node's
fallback (always destroy) is correct for the checked case.
P0-2 - Checkbox state leaked across dialogs:
Reset pruneVolumes in onConfirm before calling the parent, so a
previously checked box doesn't appear pre-checked when the dialog
opens for a different stack.
P0-3 - Delete not bound to the active node:
Capture activeNode.id at delete time and pass it as an explicit
nodeId to apiFetch, matching the Take Down pattern. Without this,
switching the active node while the dialog is open silently deletes
the wrong stack on the wrong node.
* fix: update test assertions for nodeId binding and showVolumeOption gate
P0-3 added nodeId to apiFetch DELETE calls — two useStackActions tests
now expect the parameter. P0-1 gated the volume checkbox behind
showVolumeOption — the confirming test now passes the prop.
* fix: gate volume hint on showVolumeOption to prevent false promise
On nodes without stack-delete-prune-volumes, volumes are always
destroyed. Showing VOLUMES KEPT was a lie. Now the hint is hidden
entirely when the capability is absent.
* fix: gate delete against nodes that cannot guarantee volume preservation
Hiding the checkbox and the misleading hint stopped the false promise but
not the data loss: an unchecked delete against a node lacking
stack-delete-prune-volumes still reached that node and its downStack()
still destroyed volumes unconditionally, now with no warning at all.
- remoteNodeProxy.ts: block an unacknowledged DELETE /stacks/:name
(no pruneVolumes=true) to a remote lacking the capability, mirroring
the existing removeVolumes gate on the down route. An explicit
pruneVolumes=true always proxies through since that matches what an
unsupported remote does anyway.
- DeleteStackDialog: rework around a three-state model (supported /
unsupported / unknown) instead of a boolean. A node whose capabilities
have not been confirmed (meta not yet fetched, or a failed probe) is
now treated like a supported node, not forced onto the destructive
path just because its state is unresolved.
- Fix deleteStack's error toast, which surfaced the raw JSON response
body instead of the parsed error message.
- Fix CreateStackDialog's orphan-stack rollback (docker-run import),
which silently no-op'd against a node requiring acknowledgement.
- Update node-compatibility.mdx and stack-management.mdx to describe
the new gate.
* test: advertise stack-delete-prune-volumes on the scoped-evidence fixtures
These mock remotes simulate nodes capable enough to run scoped-stack-auth-evidence
RBAC and were pinned before stack-delete-prune-volumes existed, so the new delete
gate now blocked their unacknowledged DELETE calls before reaching the mock server,
failing the grant-tuple-cleanup assertions the tests actually check.
|
||
|
|
0ba09ebdee |
feat: add ntfy notification channel (#1761)
* chore: bump brace-expansion and fast-uri via npm audit fix Resolves GHSA-rgw5-rvv9-x895 (brace-expansion DoS via unbounded intermediate arrays). Both transitive dev dependencies updated: - brace-expansion 5.0.8 -> 5.0.9 - fast-uri 3.1.4 -> 3.1.5 * chore: also bump frontend deps via npm audit fix Fixes brace-expansion and postcss in the frontend lockfile so npm audit --audit-level=high passes on both packages. * chore: bump ip-address transitive dep via npm audit fix Resolves three new ip-address advisories (GHSA-mwp4-54f8-5fhr, GHSA-4xrf-jv44-h6hh, GHSA-22jq-vg5j-6vgg) published between prior push and CI run. * feat: add ntfy notification channel Add ntfy (https://ntfy.sh) as the fifth notification channel alongside Discord, Slack, Webhook, and Apprise. ntfy speaks its native protocol: plain-text POST body with Content-Type, Title, Priority, and Tags headers. Priority maps info/warning/error to ntfy's default/high/urgent. URL validation allows both HTTP and HTTPS (common for LAN self-hosting) but rejects embedded credentials, consistent with Apprise. Token auth via ntfy's documented ?auth= query parameter is supported. * fix: correct ntfy channel test cases for Linux URL parsing and required type field - notification-channels.test.ts: replace http:///topic host check with a cross-platform invalid-URL case (WHATWG parser treats triple-slash authority differently on Linux vs Windows) - ConfigurationStatus.test.tsx: add ntfy agent slot to makePayload and inline agents fixtures (required by the expanded ConfigurationAgents type) * fix: remove unused import and update 0/4 masthead assertions to 0/5 * ci: exclude NotificationService.ts from js/request-forgery CodeQL rule Notification channel dispatch methods (Discord, Slack, Webhook, Apprise, ntfy) all call fetch() with admin-configured URLs and notification bodies that may embed stack or path data. This matches the trust model already documented for registry-api.ts: single-tenant self-hosted, admin owns the server, outbound posting is the intended behavior. The write path is gated by requireAdmin or requirePermission(node:manage), and every dispatch runs with a 10s AbortSignal.timeout. * ci: also exclude NotificationService.ts from js/file-access-to-http Notification messages may embed stack names, paths, or compose-derived content. Same trust model as js/request-forgery: admin owns the server and the configured endpoints, write path is gated. * fix: correct ntfy channel tab copy and validation error message The ntfy settings tab was reusing the generic webhook label, helper, and placeholder (Webhook URL / JSON payloads / https://...). Give ntfy its own copy: label names the server-and-topic URL, helper states plain-text delivery and the mandatory topic path, placeholder matches the routing section. Also fix the routing-rule validation toast: the guard correctly exempts ntfy from the HTTPS check but the error message was not updated alongside it, so ntfy URLs received a misleading HTTPS-required message. * fix: strip trailing slash from ntfy topic URL before dispatch A topic URL like https://ntfy.sh/mytopic/ validates fine (the check strips the trailing slash internally) but was stored and dispatched with the slash intact, causing the real ntfy server to 404. Normalize before fetch so the request reaches the correct topic path. Also add ntfy to the Channels card description in the settings registry. |
||
|
|
41bf075eb0 |
feat(recovery): make rollback-recovery image lifecycle visible and controllable (#1753)
* feat(recovery): make rollback-recovery image lifecycle visible and controllable GitHub discussion #1751 asked why Sencho creates sencho-rb/<id>/<service>:hold images during automatic updates and how to clean them up. That surfaced a real safety bug alongside the missing visibility: the manual single-image delete route did not consult the held-image predicate every other deletion path already honors, so a user could delete a rollback-protected image straight through the Images tab and silently break automatic recovery for that update. A short/truncated id also bypassed the predicate's full-id lookup. Fixes: - POST /images/delete now resolves the submitted id to its canonical form and checks the unified held-image predicate before deleting, returning 409 IMAGE_HELD_FOR_ROLLBACK for a protected image. - The Images tab no longer mislabels a protected image as plain "Unused"; a fully-synthetic hold image is kept out of the generic inventory entirely and surfaced instead in a new Resources -> Rollback tab, with an additive "Rollback protected" badge for images that still carry a normal tag too. New capability: - Two settings (Deploy Guardrails): superseded-generation retention (days, replaces a hardcoded 7) and a cap on retained generations per stack. - A new Resources -> Rollback tab lists every generation (stack, short id, state, retention) with an admin-gated manual release action, including releasing the current generation with an explicit warning that automatic rollback becomes unavailable until the next successful update. Release is a single atomic, server-revalidated transition so a stale UI read can never release a row that has since become ineligible. Also consolidated three near-duplicate implementations of the held-image predicate (two of which relied on a require() of a sibling .ts file that silently failed to resolve under the test runner and was never actually exercised by a real test before this change) into one shared module. Known follow-up, not fixed here: an orphaned sencho-rb tag whose recovery row no longer exists (DB restore, node re-add) is invisible in both the Images and Rollback tabs with no UI path to reclaim it. * fix(audit): add summary mapping for rollback generation release * fix(security): sanitize prune target in log sinks and cover release RBAC Closes two open js/log-injection findings on the system prune route by applying the same inline sanitizeForLog barrier the rest of the file already uses. The prune target is validated against an enum by parsePruneTargets before reaching these sinks, so the findings were false positives, but the barrier is cheap and removes the standing alerts on a file this change already touches. Also wraps the generation id in the release log line for consistency with the stack name beside it. Adds coverage for gaps a QA pass identified: - Release endpoint refuses a viewer and a deployer (Admin-only), leaving the generation and its artifacts untouched. - Viewer can still read the generations list, matching the sibling Resources routes. - The predicate the prune routes build reports full-stack rollback holds, not just service-scoped ones, and re-reads per call so a hold taken between plan and delete still gates the delete. - After releasing the current generation, no rollback point is claimed for the stack through any consumer of the current-generation lookup. |
||
|
|
48c89d217d |
fix(mesh): close error-listener race in pilot tunnel reverse-dial connect (#1754)
acceptReverseLocal swapped a pre-connect error handler for permanent close/error handlers inside the socket's 'connect' callback, briefly leaving 'error' with the old handler removed and the new ones not yet attached. A Node EventEmitter 'error' event with zero listeners throws instead of being swallowed, and CI's runner timing hit this window intermittently (backend-tests-pilot-tunnel-bridge-reverse- route-events.test.ts's post-handshake-close test), surfacing as an unhandled ECONNRESET exception even though every assertion passed. Replace the swap with a single 'error' listener attached at socket creation that branches on a connected flag, so the socket is never without an error listener at any point in its lifecycle. |
||
|
|
c613010199 |
feat: account for VM memory ballooning in host memory reporting (#1750)
* feat: account for VM memory ballooning in host memory reporting Extend hostMemory.ts with a readBalloonedMemory() function that parses the Balloon: field from /proc/meminfo, following the same fail-open pattern as the ZFS ARC integration. When a nonzero balloon is detected, effective memory fields (effectiveUsed, effectiveFree, effectiveUsagePercent) are computed and exposed through /api/system/stats and /api/fleet/overview. All consumers that derive meaning from host memory now prefer effective values when present: the dashboard gauge, Fleet card RAM bar, mobile views, health verdict, health status bar stat tile, and host RAM alerts. Backward compatible: missing /proc/meminfo or absent Balloon: line preserves exact current behavior. Old remote nodes without the new fields continue rendering normally. * refactor: extract shared helpers for balloon memory wiring Extract readCandidateFile() and logSelectedPath() in hostMemory.ts to deduplicate ARC and balloon file-read logic. Add memoryToWire() to centralize the optional-field spread used by /api/system/stats and /api/fleet/overview. Add getNodeMemUsed()/getNodeMemTotal() helpers in nodeUtils.ts for frontend byte-text consumers. * fix: make desktop fleet masthead aggregate balloon-aware The desktop fleet overview's memory aggregate in useFleetOverview.ts still summed raw memory.used, while the mobile fleet aggregate and per-node cards already used effective values. Update to use getNodeMemUsed/getNodeMemTotal helpers. * fix: revert balloon adjustment from alerting and health decisions Ballooned memory is host-reclaimed (unlike ZFS ARC, which the guest can reclaim on demand). The guest cannot get ballooned pages back until the hypervisor deflates them, so treating ballooned memory as available for alerting or health can mask real memory pressure. Keep balloon parsing, wire fields, and the dashboard context line as informational-only. The memory gauge, health verdict, and host RAM alerts now use the standard ARC-adjusted working-set percentage regardless of balloon. Updated configuration.mdx and dashboard.mdx to document that balloon data is informational and does not influence alerting. |
||
|
|
209c9c5d53 |
feat: add target-aware RBAC authorization to Scheduled Operations (#1745)
* feat: add target-aware RBAC authorization to Scheduled Operations Replace the blanket requireAdmin gate on all 9 scheduled-tasks endpoints with per-action permission checks derived from the centralized action registry. Each scheduled action now declares the existing permission it requires: stack lifecycle actions need stack:deploy on the target stack, node-wide operations need node:manage on the target node, prune stays admin-only via system:settings, and snapshot requires unscoped node:manage. Key changes: - Backend registry: add permission field and resolveTaskPermissionScope - Routes: replace requireAdmin with requireTaskPermission, filter GET listing by permission, add two-phase PUT check - Scheduler: revalidate creator permission at execution time, auto-disable on revocation (TaskAuthorizationError) - Database: new creator_user_id column with migration and backfill - Frontend: canScheduleAction/canScheduleAny helpers, reachability gate via scheduledOpsAccessible, stack context menu uses canDeploy not isAdmin, permissions field on all ScheduledActionDefinitions - Expose checkPermissionForSubject for in-process callers (scheduler) No new PermissionAction values are added. Uses the existing stack:deploy, node:manage, and system:settings matrix. Scoped Admiral grants work on the exact (nodeId, stackName) target per SEN-438. * test: add RBAC coverage for scheduled operations authorization * test: update frontend tests for scheduled-ops RBAC gate changes - useStackMenuItems: gate Schedule task on canDeploy not isAdmin; add test for canDeploy=true, non-admin case - buildNavigationModel: default scheduledOpsAccessible to true (default ctx is admin, who can always schedule) - useViewNavigationState: add stack:deploy to admin can() mocks so canScheduleAny resolves correctly * fix: keep checkPermission unchanged, add checkPermissionForSubject standalone The earlier refactoring that made checkPermission delegate to checkPermissionForSubject changed the call order of effectiveTier(req) relative to the admin bypass, which subtly broke the Community tier clamping in the audit-log route. Keep checkPermission byte-identical to the original and expose checkPermissionForSubject as a standalone function used only by the scheduler revalidation path. * fix: prefix unused selectorType parameter in resolveTaskPermissionScope * fix: address audit findings — existence oracle, revalidation test, action filtering Three corrections from the independent PR audit: RC-1 (action filtering): Wire canScheduleActionAnywhere into the action picker in ScheduledOperationsView so actions the user can never schedule (prune without system:settings, node:manage actions without a scoped grant) are filtered from the picker entirely. Add canScheduleAction check on the Create button against the currently selected target, so the submit button is disabled when the caller cannot schedule the chosen action on the chosen target. RC-2 (existence oracle): Six by-ID endpoints (GET /:id, DELETE /:id, PATCH /:id/toggle, POST /:id/run, GET /:id/runs/export, GET /:id/runs) now return a uniform 404 when permission is denied on an existing task, so an unauthorized caller cannot distinguish "task does not exist" from "task exists but you are not authorized." Added requireTaskExistsPermission helper for the 404 variant; POST create and PUT merged-scope checks keep requireTaskPermission (403). RC-3 (revalidation test): Fixed the orphan-creator test to assert the post-execution task state (auto-disabled, explicit error message) rather than wrapping executeTask in a try/catch with a no-op else branch, since executeTask catches TaskAuthorizationError internally and returns normally. Added canScheduleActionAnywhere helper and AuthContext mock to ScheduledOperationsView tests (38/38 pass). * fix: remove unused TaskAuthorizationError import from test * fix: use 404 on PUT phase-1 unauthorized-access check The PUT two-phase check's first phase (ownership verification) now uses requireTaskExistsPermission (404) instead of a manual 403, consistent with the six other by-ID endpoints. This prevents a caller from probing task-ID existence via the PUT route. * fix: address QA findings — reorder prechecks, surface permission reason Two live-confirmed fixes from the 3-node fleet QA pass: Finding #4 (offline-node error ordering): Swap the order of the node- reachability precheck and the creator-permission revalidation in SchedulerService.executeTask. Authorization now runs first, so a revoked grant is always surfaced as an explicit auto-disable with a clear error message, even when the target node is offline. Previously the reachability check ran first, hiding the revocation behind a misleading "target node is offline" error and leaving the task enabled indefinitely while the node was down. Finding #7 (unexplained Save dead-end): When the Create/Update button is disabled because canScheduleAction denies the selected action-target combination, a muted text line now appears below the button: "You do not have permission to schedule this action on the selected target." This gives scoped users meaningful feedback instead of a silently disabled button with no explanation. * chore: remove redundant !!formName guards The earlier short-circuit conditions in isSaveDisabled and saveDisabledReason already guarantee formName is truthy by the time the canSaveWithCurrentTarget check runs. The !!formName guard is a no-op — flagged by GitHub code-quality as 'Useless conditional: This negation always evaluates to true.' |
||
|
|
15801318d6 |
fix(rbac): permission-gate alerts, auto-heal, and image updates (#1743)
* fix: gate alerts and auto-heal routes on stack:edit/stack:read permissions Replace requireAdmin with requirePermission across backend/src/routes/alerts.ts and backend/src/routes/autoHeal.ts, mirroring the stack:read/stack:edit model already used by stacks, blueprints, git sources, and settings. Adds the previously-missing permission gate on the auto-heal history route, and adds ownership-aware deletion for alerts via a new DatabaseService.getStackAlert(id) lookup. * fix: gate image-update fleet, per-stack refresh, and auto-update execute on RBAC permissions Replace requireAdmin with requirePermission/checkPermission across backend/src/routes/imageUpdates.ts (imageUpdatesRouter and autoUpdateRouter), mirroring the permission-aware model already used by alerts and auto-heal. GET /fleet drops its admin gate to match the auth-only read model shared with GET / and /detail. POST /fleet/refresh now requires node:manage. A new route, POST /refresh/:stackName, lets a caller with stack:deploy on that stack trigger a per-stack recheck, distinct from the node-wide POST /refresh. The auto-update executor now pre-checks stack:deploy across every resolved target before any work starts, so a denied stack in a bulk request fails the whole call instead of partially executing; the "*" wildcard additionally requires global stack:deploy up front since it expands to every stack on the node, including the empty case where a per-stack check would otherwise have nothing to gate. * fix: evaluate permission before checks-enabled state in auto-update execute The checks-enabled short-circuit in autoUpdateRouter POST /execute ran before target parsing and before any permission check, so a node with image-update checks disabled returned 200 to any authenticated caller regardless of stack:deploy grants. Move the checks-enabled check to run after the resolved stackNames have cleared requireExactStacks, so permission is always evaluated first. Add coverage: a denied role still gets 403 PERMISSION_DENIED (not the disabled-checks 200) while checks are disabled node-wide, and a scoped-only user whose stack:deploy grant covers every stack on the node is still denied target="*" (the wildcard requires global stack:deploy, per the earlier fix), proving that tradeoff against a real on-disk stack rather than the always- empty fresh test instance. * fix: gate alerts, auto-heal, and image-update controls on frontend permission checks Match the backend RBAC gates for alerts, auto-heal, and per-stack image updates with matching frontend checks, replacing raw isAdmin/node:manage gates with scoped can() calls: - Alerts/Auto-Heal menu items and their keyboard shortcuts now gate on stack:read (canViewMonitor), including the window-level keyboard shortcut handler that previously bypassed the menu item gate entirely. - Check updates now gates on stack:deploy (previously node:manage) and calls the new per-stack POST /image-updates/refresh/:stackName endpoint instead of the node-wide refresh. Since the endpoint runs the recheck synchronously and returns the result directly, the old node-wide /status polling loop is removed in favor of handling the response inline. - StackAlertSheet's alert and auto-heal policy mutation controls gate on stack:edit instead of isAdmin. - The Fleet Image Updates refresh button (mobile and desktop) gates on node:manage, hidden rather than disabled to match the existing convention for node:manage-gated affordances. * fix: cover the stack:edit deny path for StackAlertSheet gates The useAuth mock in StackAlertSheet.test.tsx returned can: () => true unconditionally, so canEditAlerts, canEditAutoHeal, and PolicyRow's canEdit prop were never exercised with a denial. Make the mock per-test-controllable (matching the vi.fn() pattern already used in NodeCard.test.tsx) and add one deny-path test per tab asserting the mutation controls are absent while reads stay visible. Also adds an aria-label to the alert row's delete button so the deny test can assert on its absence, matching the aria-label convention PolicyRow's own toggle/delete controls already use. * fix: surface accurate warnings and loading feedback on stack update checks checkUpdatesForStack ignored the backend's StackRecheckResult outcome and always showed a success toast, even when verification failed or an update is still present. It also gave no feedback while the multi-second per-image registry probe was in flight. Add a loading toast on request start, and branch the result toast on outcome/warning instead of unconditional success. The backend reuses its post-update reconciliation copy for this pre-update discovery check, so the two generic "update command completed" strings are replaced with accurate pre-update wording; a genuine stack-specific warning (e.g. a compose render failure) is still shown as-is. Also update docs/features/rbac.mdx: stack:edit now covers alert and auto-heal management, stack:deploy covers per-stack image-update checks, and the Deployer role description reflects both. * fix: add per-stack cooldown rate limit for image-update recheck route The per-stack POST /refresh/:stackName route bypassed the existing node-wide manual-refresh cooldown. A caller with stack:deploy could hammer the registry with unbounded concurrent recheck calls. Add tryMarkStackRecheck in ImageUpdateService, sharing the same 2-minute cooldown window, keyed per (nodeId, stackName). The route handler returns 429 when denied. The mark is written synchronously before the first await so concurrent calls on the same tick are blocked. |
||
|
|
44d6078241 |
feat(fleet): show itemized prune plans (#1734)
* feat(fleet): itemize prune review plans Build and display fingerprint-bound prune candidates for every reviewed fleet node. Preflight all node plans before mutation and preserve detailed removed, skipped, failed, and partial outcomes. Add safe resource metadata projection, managed ownership attribution, runtime contract validation, transport parity coverage, and operator docs. Closes #1724 * fix(security): harden stack path lookup Use a Map for Compose working-directory ownership resolution so untrusted path strings cannot become object property writes. * fix(fleet): harden prune execution safeguards |
||
|
|
3c934066f3 | fix(networking): ignore verified Mesh attachments in drift (#1729) | ||
|
|
9922d8e765 |
feat(rbac): make stack-scoped grants node-specific (#1727)
* feat(rbac): make stack-scoped grants node-specific Qualify stack role assignments as (nodeId, stackName), migrate legacy rows to the default node, and forward bound multi-action evidence on Proxy/Pilot hops so scoped users keep least-privilege remote access without shipping the full grant table. * fix: mirror scoped-stack-auth-evidence capability to frontend, sanitize node id in role assignment log Backend added the scoped-stack-auth-evidence capability without the matching frontend entry, failing the capability parity test. The role assignment log also interpolated the node id without sanitizeForLog, unlike the rest of the line. * fix(rbac): honor node-wide scopes and fix proxied DELETE cleanup Node-scoped grants now authorize that role's stack actions on the same node in the backend resolver, frontend can(), and remote evidence. Proxied DELETE cleanup uses the gate-stashed route because pathRewrite mutates req.path before proxyRes. Add proxy integration coverage and drop the stale scoped-permissions screenshot. * fix(rbac): preserve node-qualified grants during repair |
||
|
|
b0b423b234 |
feat(fleet): reapply Compose configuration without a version update (#1716)
* feat(fleet): reapply Compose configuration without a version update Add a distinct Fleet Reapply configuration path so Compose-managed nodes can recreate Sencho from the current on-disk project when already up to date, without pulling or rewriting the image reference. * fix(fleet): confirm remote reapply and close concurrent tracker race Require confirmation for remote compose reapply, and lock dispatch before the remote POST so a second request cannot overwrite a successful in-flight tracker. * fix(ui): icon-only Reapply control so Up to date badge can breathe Collapse the Node updates Reapply label into a tooltip so the status pill no longer wraps in the Status column. * feat(editor): Save & Reapply self-stack via fleet compose reapply (#1726) * feat(editor): Save & Reapply self-stack via fleet compose reapply Eligible admins can apply on-disk Compose edits to Sencho's own stack from the editor using the same confirm, dispatch, and reconnect path as Fleet Node Updates. * fix(editor): gate Save & Reapply label to self-stack only Ordinary stacks were labeled Save & Reapply whenever the node was reapply-eligible. Require the selected file to be the self-stack for the toolbar label and diff confirm CTA. * fix(ui): move compose diff action label helper out of dialog module Keep ComposeDiffPreviewDialog component-only so react-refresh Fast Refresh lint passes after the Save and reapply stacked merge. |
||
|
|
78475d96ef |
fix(compose-doctor): resolve effective healthcheck coverage (#1713)
* fix(compose-doctor): resolve effective healthcheck coverage Compose Doctor now classifies healthcheck coverage from the Compose model, running containers, and local images so image-provided HEALTHCHECKs are not false positives. Update Guard shares the same presence helper so test NONE is not treated as active. * fix(compose-doctor): fix healthcheck project label and empty compose HC Use the Compose project name for runtime container listing so stacks whose name: differs from the directory still get runtime evidence. Treat empty or timing-only healthcheck objects as absent rather than active. * fix(compose-doctor): treat inherited healthcheck as All Clear note Inherited image healthchecks no longer block All Clear; they surface under a notes section and cannot be acknowledged. |
||
|
|
72cdbb0eaa |
feat(schedules): auto-update stacks by Stack Label (#1717)
* feat(schedules): auto-update stacks by Stack Label Add a reusable selector_type/selector_value on scheduled tasks so admins can schedule image updates against live Stack Label membership across the fleet or one node, reusing fleet label resolution and the existing auto-update orchestrator. * fix(image-updates): sanitize auto-update execute failure logs Use a static format string and sanitizeForLog so CodeQL no longer flags user-controlled stack names and error text in the execute catch. * fix(ui): space Scope label from fleet/node segmented control Match the Schedule row layout so the inline SegmentedControl no longer sits flush against the Scope label. * fix(ui): remove redundant wrapper around Scope segmented control |
||
|
|
fa503ddf27 |
feat: add node-scoped opt-out for image update detection (#1715)
* feat: add node-scoped opt-out for image update detection Operators who use an external update authority can disable Sencho registry polling per node without losing explicit stack Update, pull, or redeploy. * test: fix mocks and lint for image-update checks opt-out Scheduler tests need isChecksEnabled on the ImageUpdateService mock, and the UpdatesSection older-node fixture must not leave an unused binding. * fix: gate update-preview and recheck when detection is off Anatomy was still calling stack update-preview (and contacting registries) while checks were disabled. Short-circuit those routes and skip recheckStack writes so disabled nodes stay quiet until detection is re-enabled. |
||
|
|
e175db8e62 |
feat(auth): add SSO-only authentication mode (#1714)
* feat(auth): add SSO-only authentication mode Let administrators disable interactive local password login when SSO is configured, with backend enforcement, activation safeguards, and host CLI recovery. Closes #1709 * fix: resolve CI failures in auth mode PR - Add useLicense mock to SSOSection test to prevent crash from AuthenticationModePanel rendering without LicenseProvider - Remove username from authMode console.log calls that CodeQL flags as clear-text logging of sensitive information * fix(auth): keep SSO-only on named disableSso and fail-closed login Named provider disable no longer reverts authentication_mode. Login initializes localLoginEnabled false so a status fetch failure cannot reveal the password form. Center a single OIDC provider button on the login card. * fix(auth): move SSO-only authentication mode from Admiral to Community tier Security-hardening features belong on the Community tier per the existing Community rebalance. The reporter of #1709 noted that disabling local password login after configuring SSO is a basic security measure, not an enterprise governance feature. LDAP provider configuration remains Admiral-gated via requireTierForSsoProvider. * fix(ui): keep SSO Active badge and ON toggle in sync Provider cards mounted before config fetch finished with enabled:false, so a saved Active provider showed OFF until the local draft was resynced. Drive both the badge and TogglePill from the synced local config. * feat(auth): auto-redirect to sole OIDC provider under SSO-only When authentication mode is SSO only and exactly one OIDC provider is enabled (no LDAP), skip the login chooser and send the browser to that provider's authorize URL. Returning sso_error stays on the login page so the failure message remains visible. * fix(ui): move oidcAutoRedirectUrl out of Login for fast refresh Exporting the helper alongside the Login component tripped react-refresh/only-export-components and failed Frontend lint CI. Keep Login as a component-only module and colocate the helper with its unit tests under lib/. |
||
|
|
681ecc7047 | fix(auth): keep active sessions alive and add stay-signed-in (#1711) | ||
|
|
cfb42af4e0 |
fix: assorted UI/UX polish fixes (#1670)
* fix(dashboard): replace Stack Health update badge with an icon The pill badge duplicated space already used by the stack name column. A CircleArrowUp icon after the name signals an update is available without competing with the existing ArrowUp/ArrowDown sort indicators in the same table. * fix(dashboard): add accessible name to update-available icon Icon-only indicators need an aria-label directly on the icon; title on a non-interactive span is not reliably announced by screen readers. * test(dashboard): cover the update-available icon's accessible name The icon-only indicator and its aria-label fix had no regression guard, unlike the equivalent update dot in StackRow. * refactor(dashboard): compute the update-available label once per row It was being derived twice (title and aria-label) from the same row.outdatedServices input. * fix: drop Community-tier pricing upsells from settings Community operators no longer see the "See pricing" link in Licensing or the "Need direct support?" callout in Support. The pricing link now only shows for an expired paid license needing to renew. * fix: make Resources images/volumes tables actually scrollable The tables were wrapped in a Radix ScrollArea sized with max-h-[62vh]. Radix's viewport uses height:100%, which cannot resolve against an ancestor whose computed height is auto (max-height alone isn't a definite height), so the viewport silently grew past the visible box and the extra rows were clipped with no way to reach them. Verified live: several image rows were permanently unreachable, with no working internal scrollbar and not enough outer page scroll to compensate. Switched to an explicit h-[62vh], which the viewport can resolve correctly, matching every other working ScrollArea in the codebase. Falls back to h-auto below the md breakpoint so the bespoke mobile layout keeps shrinking to content and scrolling via the outer page instead of gaining a fixed-height inner scroll box. * fix: apply ScrollArea definite-height fix across remaining lists Radix ScrollArea needs an explicit height, not max-height, or the viewport collapses and clipped rows become unreachable. Extend the Resources fix to security, settings, git, and create/import surfaces, and drop redundant outer wrappers where ModalBody already scrolls. * fix: migrate Networking tables to Radix ScrollArea Networks and Findings used native max-h + overflow-auto, which worked but broke glass scrollbar consistency with Resources and the design system. Switch them to ScrollArea with a definite height and the same mobile fallback as the other inventory tables. * fix: warn Classic bar users that the style is retiring soon When Appearance Navigation is set to Classic bar, show the same warn SettingsCallout pattern used for Constrained graphics. Preference is kept until removal; no alternate style is named in the copy. * fix: move Channels delivery retries below channel tabs Put channel configuration first and keep Delivery retries as a shared footer control under the Discord/Slack/Webhook/Apprise tabs. * fix: drop redundant More masthead from Smart bar overflow menu The trigger already reads More, so the dropdown masthead repeated the same label. Leave titled mastheads on Compact Navigate and Add quick link menus. * test: align Smart More E2E with masthead removal The overflow menu no longer shows a More heading. Assert the menu via the Logs item and lock that the redundant masthead stays gone. * fix: consolidate Fleet Map toolbar filters into a single row Adopt the same retractable search control used on Fleet > Overview and move the flag filters (missing deps, port conflicts, orphans, shared) onto the toolbar row right after the Graph/List selector. The node filter becomes a dropdown instead of individual toggle chips so it does not clutter the row as fleet size grows. * fix: move Networking Topology filters onto the search toolbar row Merge the ownership selector and boolean filter chips (include system, exposed, drift, missing external, shared) onto the same row as the stack/network search inputs, matching the Fleet Map toolbar layout. * fix: default the reclaimable-space banner off Resources > Docker & Storage's "Show reclaimable-space banner" toggle now defaults to off instead of on. Also flips the /settings fetch failure path to fail closed (hide the banner) to match the new default, instead of failing open. * fix: raise Compact launcher quick links cap from 5 to 7 * fix: add Discord link to Settings Support Self-serve Gives users a community chat channel alongside Documentation and GitHub Issues, using the official Discord mark since lucide-react has no brand icon for it. * fix: stop container NET I/O metric row height jump Give NET I/O more column share than CPU/MEM and keep metric values on one line with truncate so three-digit rates cannot grow the strip. * fix: elevate Doctor tab between Activity and Drift Make Compose Doctor easier to find in the anatomy strip by placing it with the ops judgment cluster, ahead of Dossier and inventory tabs. |
||
|
|
688767092a |
fix(stacks): fail closed when compose ps errors during update orphan classify (#1708)
* fix(stacks): fail closed when compose ps errors during update orphan classify A thrown compose ps must not name-match containers as removable orphans; that destroyed healthy runtimes after transient ps failures post-acquire. * fix(stacks): fail closed on unparseable compose ps and gate orphan fallback Garbage compose ps stdout must throw, not look like an empty success. smartFallback also requires working_dir or config_files evidence so bare name collisions are not removed as orphans. * fix(stacks): barrier smartFallback compose path for CodeQL Validate stackName and resolve the compose file under the node compose root with an inline startsWith containment check before fs.readFile. |
||
|
|
719180f156 |
fix(fleet): verify update status before removing readiness cards (#1697)
* fix(fleet): verify update status before removing readiness cards Full-stack Apply now rechecks persisted status after the health gate starts, reloads the live preview before dropping a card, and invalidates the hub fleet aggregation so cleared updates cannot resurrect from a stale cache. Closes #1686 * fix(fleet): align persisted update status with preview semver detection Share digest-plus-tag detection so post-Apply sidebar status matches Fleet and Anatomy. * fix(fleet): keep tag-only updates advisory for Compose automation Expose digestUpdate vs tagUpdate from checkImage so scheduled and API auto-update only apply same-tag digest drift Compose can pull. * docs: clarify scheduled auto-update applies digest drift only Document that higher pinned tags stay advisory until Compose is changed, matching schedule and Run Now behavior. * docs: require Compose pin edits for higher-tag advisories Stop recommending Apply now or Update as remedies that cannot rewrite a pinned image tag. * docs: clarify Apply now pulls pinned tags only Align the detection-cadence bullet with digest-rebuild vs higher-tag guidance. * fix(fleet): keep tag advisories after apply and scheduled updates Tag-only previews were treated as cleared on Fleet reload, and scheduled/ Run Now paths wiped status without rechecking. Align post-update verification with the manual Apply path (health gate first, recheck, no blind clear) and block digest apply when sibling image checks failed. * fix(fleet): clear eslint unused-arg and containers assignment |
||
|
|
bb7c76ba46 |
fix(image-updates): normalize docker.io host aliases to the registry API host (#1706)
* fix(image-updates): normalize docker.io host aliases to the registry API host parseImageRef kept a literal `docker.io` or `index.docker.io` host when the user wrote an explicit registry prefix, so every downstream request hit the marketing domain instead of registry-1.docker.io and failed with an unhandled 3xx. Normalize both aliases before the library/ auto-prefix check so explicit and implicit Docker Hub refs resolve identically. * test(image-updates): pin docker.io alias parity and drop redundant coverage Add the index.docker.io namespace-omitted parseImageRef case and a buildRollbackTarget assertion for an explicit docker.io/ ref, and drop the compareLocalToRemoteTag traefik-shape test that only duplicated existing attestation-manifest coverage. Also clarify the canonicalRegistry doc comment now that it and parseImageRef normalize to different forms. * docs(image-updates): correct canonicalRegistry comment's call-path example * test(image-updates): collapse docker.io alias cases into a single it.each |
||
|
|
9859ce60b8 |
fix(notifications): version mute replica retractions for soft-cleanup restore (#1703)
* fix(notifications): version mute replica retractions for soft-cleanup restore Soft cleanup and authoritative delete shared an unversioned permanent tombstone, so a later hub re-save could not restore scheduled mutes on a remote. Carry hub-authored kind and source_updated_at on replica DELETE, allow recoverable recreate when updated_at is newer, keep permanent deletes fail-closed, and reject stale recoverable DELETEs against newer rows. * fix(notifications): durable mute retractions across mixed-version fleets Gate recoverable replica DELETEs on a new capability, durable-queue failures and incompatible remotes for retry, fan permanent deletes to every known remote, and return applied vs ignored outcomes on replica writes. |
||
|
|
6688da97b1 |
fix(image-updates): match any local RepoDigest against the remote tag (#1695)
* fix(image-updates): match any local RepoDigest against the remote tag Docker can list a stale multi-arch index digest ahead of the current one on the same image. Selecting only the first RepoDigest caused false same-tag rebuilds (for example redis:8.8.0) even when another digest equaled the registry primary. Compare every matching candidate and keep fail-closed behavior for empty, unknown-platform, and classification errors. Fixes #1684 * fix(image-updates): surface digest verification failures to operators Carry comparator errors into update-preview as check_error / verification_failed, prefer failed checks over sticky has_update in Fleet and the sidebar, and keep Update Guard from claiming no pending update when verification failed. * fix(e2e): align sidebar truncation spec with check-failed precedence StackRow now shows the check-failed icon over a stale update dot, but this spec still asserted the old precedence and failed deterministically in CI on every attempt. * fix(fleet): treat verification-only previews as non-actionable Fresh update-preview wins over sticky fleet booleans: disable Apply, exclude from ready counts, and move verification-only stacks into the check-failures advisory (including remote-labeled names). * fix(fleet): move preview actionability helpers out of the view Exporting non-components from AutoUpdateReadinessView tripped react-refresh lint in CI. Keep the helpers in a shared lib module and drop an unused mock arg. * fix(fleet): drop sticky cards when fresh preview clears the update A successful no-update preview now removes the pending Fleet card instead of leaving Apply enabled. Verification-only stacks still go to the advisory, and empty-state copy no longer claims all-clear while checks remain unresolved. * fix(image-updates): hold full-stack apply for review when another image fails verification A confirmed update or rebuild on one image previously left the whole stack fully actionable even when a different image in the same stack failed digest verification: Anatomy claimed "safe to apply", Update Guard reported ready, and Fleet's full-stack Apply stayed enabled, all while showing the verification-failure text right next to those claims. isActionableUpdatePreview now requires no verification failure anywhere in the stack; a new isReviewRequiredUpdatePreview flags the mixed state so Fleet still surfaces the card (not silently cleared) with Apply now disabled and a "Review · unverified" badge. Anatomy's banner says "review required" instead of a bump-based safety claim and withholds its Apply button. Update Guard's pending-update signal downgrades from ok to attention. Per-service apply (Fleet's per-image row) is deliberately left enabled since a service-scoped update to the confirmed image does not touch the unverified one. * fix(image-updates): treat rebuild_available symmetrically with has_update in Update Guard updatePreviewSignal only downgraded to 'attention' inside the has_update branch, so a rebuild-only stack (has_update false, rebuild_available true) with a sibling verification failure fell through to the plain verification-only 'unknown' branch and never mentioned the pending rebuild, inconsistent with isReviewRequiredUpdatePreview on the frontend which treats has_update and rebuild_available the same way. Also adds desktop-card coverage for the mixed state (previously only the mobile card was exercised) and locks in blocked/major-bump precedence over the new review-required badge/banner in both Fleet and Anatomy. * fix(image-updates): derive the mixed-verification review-hold from per-image detail, not the stack aggregate has_update and check_error are independent per image: a tag-based update can be confirmed via the registry's tag list even when that same image's own digest comparison against the current tag errored (already covered by an existing update-preview-service test). The stack-level verification_failed and has_update flags can therefore both be true for the SAME single image, which the previous review-hold treated identically to a genuinely different image failing verification: Update Guard said "another image failed digest verification" and Fleet told the user to "apply the confirmed service individually" on a single-service stack where no such affordance exists. isReviewRequiredUpdatePreview (and isActionableUpdatePreview) now walk the preview's images to require a pure failure image (check_error, no has_update of its own) alongside a genuinely different confirmed image or rebuild, falling back to the old aggregate-only judgment when per-image detail is unavailable. StackAnatomyPanel now imports the shared helper instead of hand-rolling the same predicate, so Fleet and Anatomy cannot drift apart. Backend updatePreviewSignal gets the same per-image treatment via a new optional images parameter, threaded through from UpdateGuardService. * fix(image-updates): fail closed on platform-unavailable indexes and legacy previews, allow anonymous tag listing Four independent gaps from the same QA pass, all in the digest/tag verification path this PR introduced or touches: - compareLocalToRemoteTag now distinguishes a remote index with no descriptor at all for the local platform (including an empty or fully-filtered index) from a genuine mismatch: the former returns an error instead of reporting a speculative update. A node cannot pull a platform the index does not offer. - selectLocalRepoDigests no longer falls back to a sole unrelated-repository RepoDigest when nothing matches the configured repo; comparing against a registry state that has nothing to do with the declared image risks a false update. Returns unresolved instead of guessing. - isClearedUpdatePreview no longer treats a preview with verification_failed missing entirely (not merely false) as proof the stack is clean. The current backend always includes this field, so its absence identifies an older remote node's response, which cannot vouch for a clean result the way an explicit false can. - listRegistryTags (and the underlying listRegistryTagsResult) no longer short-circuits to an empty list whenever no registry credentials are configured. getAuthToken already resolves anonymous tokens for public repositories; skipping it meant tag-based update detection silently never fired for any public image without a stored credential. * fix(image-updates): correct platform-check overreach, add cache and advisory for prior fixes Addresses code-review findings on the previous commit: - The platform-unavailable check fired too eagerly: an index whose runnable descriptors legally omit platform (OCI-permitted, routed to exactDigests) has real pullable content, so it must not be confused with a genuinely empty or fully-filtered index. Now only errors when both platform-labeled descriptors and exactDigests are empty. - listRegistryTags is now cached (15 min TTL): anonymous listing has no other rate limiting, and Fleet fans this out across every image on every reload. - A legacy preview (kept rather than cleared) now also pushes a check-failure advisory entry explaining why, instead of rendering as an unexplained pending card. - Corrected docstrings that described the old sole-unmatched-digest fallback and inverted how anonymous registry auth actually resolves. Adds coverage for: nested-index and attestation-only-filtered platform unavailability, a platform-less-but-populated index staying a match, the unrelated-repo digest rejection wired through the real preview-computation path (not just the registry-api unit), and legacy-preview interaction with an actionable has_update:true. * fix(image-updates): fail closed on mixed platform indexes, stop caching tag-list failures Addresses a second review round on the previous commit, including an empirically-verified regression: - The exactDigests fallback was unconditional: an index mixing a platform-labeled descriptor for a DIFFERENT platform with an unlabeled leaf let that leaf stand in as this platform's content, reporting a speculative update for a genuinely incompatible platform. Now an unlabeled leaf is only trusted when it is the ONLY kind of descriptor in the index (nothing else claims a different platform); a mixed index errors instead. - listRegistryTags was caching failed lookups for the full 15-minute TTL (a 429, an unreachable registry, or credentials not yet configured all looked identical to a real empty tag list). The fetcher now throws on failure so only a success is ever cached; CacheService's existing stale-on-error fallback still serves the last good list when one exists. - The manual "Recheck" action now also drops the tag-list cache, so a newly published tag is visible immediately instead of waiting out the TTL. - The legacy-preview advisory no longer fires when the same preview is already actionable on its own terms (a remote's own confirmed has_update/rebuild_available): pairing a "could not be checked" banner with an enabled Apply button next to it contradicted itself. - Corrected docstrings and a self-contradictory inline comment left over from the prior fix. New coverage: the exact mixed-index regression this round found and fixed, cache hit/no-repeat-fetch and failure-not-cached behavior, and the legacy-preview-plus-already-actionable non-contradiction. * fix(image-updates): add digest_error unmasked field, reorder RiskBadge, fix actionability gates Add digest_error to UpdatePreviewImage as an always-populated field that is independent of check_status masking: a confirmed tag-based update on the same image resolves check_status to 'ok' and nulls check_error, but digest_error stays set since the image's current tag content was never verified. Switch hasUnverifiedOtherImage to read digest_error. Reorder RiskBadge precedence so reviewRequired is checked before uncertain (both derive from check_error, so uncertain was unreachable). Fix self-contradiction in test fixtures (digest_update:true + check_error). Add masked-tag regression test with two-image fixture. Simplify hasUnverifiedOtherImage per code review feedback. |
||
|
|
0daddfde00 |
fix: reconcile sticky update indicators with Anatomy preview (#1698)
* fix: reconcile sticky update indicators with Anatomy preview Sidebar, Updates filter, and Fleet treated retained partial/failed scanner has_update as confirmed. Keep raw state for retention/notifications, project confirmed-only to APIs, show distinct incomplete indicators, and clear sticky rows only after an authoritative-negative preview. Closes #1685 * test: align sidebar truncate E2E with failed-over-retained precedence Purple update indicators are confirmed-only; hasUpdate with a failed check correctly shows the failed trailing icon. * fix: clear confirmed update rows on authoritative-negative preview Address audit SF-1/SF-2/SF-3: observation-watermark clears for older ok+has_update rows (DB + memory gens), Fleet checkability parity with backend not_checkable, and Updates chip confirmed-only regressions. * fix: tombstone equal-generation writers on preview clear Advance the per-stack write generation when clearing at the observation watermark so a scanner reserved before preview cannot recreate the row after an authoritative-negative reconcile. * fix: clear sticky updates with digest and tag preview parity Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear. * test: set digestUpdate on auto-update checkImage mocks Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path. * fix: clear unused lint errors on sticky update branch Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green. * fix: use inflight holder for CacheService prefer-const Keep generation-aware ownership without a let self-reference that fights ESLint and tsc. |
||
|
|
17a8dc8a94 |
fix(blueprints): fail closed on marker ownership for apply and withdraw (#1694)
* fix(blueprints): fail closed on marker ownership for apply and withdraw Require a matching .blueprint.json under the stack lock, persist required_blueprint_id on deletion intents, remove the legacy remote apply fallback, and protect the marker in the file explorer. * fix(blueprints): add CodeQL path barriers on ownership probes Use the canonical resolve-and-startsWith sanitizer inline at the marker and stack-directory fs sinks so js/path-injection clears. * fix(blueprints): block delete on failed withdraw and defer marker write Refuse Blueprint DELETE when pre-delete withdraw does not complete, and write .blueprint.json only after a successful deploy so failed applies cannot orphan stacks or claim an unapplied revision. * test(blueprints): align lock-order assert with deferred marker write Update the per-stack lock ordering expectations to compose, cleanup, deploy, then marker after the partial-apply fix. * fix(deps): bump postcss past GHSA-r28c-9q8g-f849 for npm audit Raise the Vitest/Vite transitive postcss to 8.5.23 so Backend CI audit --audit-level=high passes. |
||
|
|
79914fe750 |
fix: recognize clean one-shot completions in health gate and drift (#1691)
* fix: recognize clean one-shot completions in health gate and drift Treat exit 0 with restart policy no/absent as successful completion so init and migration jobs no longer fail post-update observation or show as service-missing, while long-running restart policies still fail closed. * fix: ignore residual health on clean one-shots and honor deploy.restart_policy Completed exit-0 jobs with no-restart intent no longer fail the health gate on leftover starting/unhealthy state, and Drift treats deploy.restart_policy with Compose precedence so any/on-failure services are not mistaken for one-shots. * fix: require explicit Compose restart no for one-shot recognition Docker inspect reports restart no for both intentional jobs and bare services that omit restart, so Health Gate and Drift now require declared restart:""no"" (or deploy.restart_policy condition none) and load Compose intent once per gate. |
||
|
|
ec0f59a85e |
fix: dedupe healthcheck alerts and share crash rate limits (#1690)
* fix: dedupe healthcheck alerts and share crash rate limits Health flaps no longer spam the bell: emit only on transition into unhealthy, keep a prune-surviving 60m dedup stamp that advances only when history persists, and share the fixed-window rate cap with crash alerts using typed roll-up copy. Docs now match fixed-window and non-persisted overflow behavior. * fix: harden health alert rate refund and shutdown cleanup Bind rate-token refunds to the issuing fixed window, clear deferred-retry markers on recovery/destroy/shutdown, and refuse deferred health dispatches after the service stops so the 20/min cap and no-duplicate guarantees hold under async races. |
||
|
|
85842cc547 |
feat: add service-scoped stack alert rules (#1681)
* feat: add service-scoped stack alert rules
Stack alerts can target one Compose service or all services. Breach timers
are per container and cooldowns are per service so a healthy sibling no
longer clears another container's timer or silences a different service.
* fix: gate remote scoped alert creates without losing the body
Remote hops skip JSON parsing so the proxy stream stays pipeable, which
left service_name invisible to the capability gate. Buffer POST /alerts
bodies for inspection, fail closed when the remote lacks the capability,
and rewrite the buffered bytes on forward. Restore alert-panel alt text
to match the unchanged screenshot.
* fix: bound remote alert body buffer and reject encoded JSON
Cap proxied POST /alerts buffering at the local 100KB JSON limit with
structured 413 cleanup, reject non-identity Content-Encoding with 415 so
compressed scoped bodies cannot bypass the mixed-version gate, and cover
oversized, chunked, and gzip regressions.
* fix: harden service-scoped alert delete, cooldown, and proxy gates
Reject non-digit alert ids, dual-write last_fired_at for rollback safety,
gate cooldown on persisted notification history, fail-fast oversized proxy
bodies with 413, and clarify Not in compose UI semantics.
* test: expect dispatchAlert persisted result in crash-safety cases
Update notification-routing assertions for the new { persisted } return
shape so CI matches the cooldown-gating contract.
|
||
|
|
dd54a2e483 |
feat: graduate Host Console to Community admins (#1669)
* feat: graduate Host Console to Community admins Make Host Console available to Community and Admiral admins (system:console), add host-console-community for mixed fleets, and keep opaque API tokens off the host shell. * docs: document Host Console deep links Cover root and stack-scoped Console URLs, correct the phone treatment note, and pin parse/build round-trips in senchoRoute tests. * fix: bind Host Console socket to the resolved node Treat unresolved activeNode as loading, target the WebSocket with an explicit nodeId, and wait for stack deep-link hydration so the shell cannot open on the wrong node or compose root. Add regression coverage for node/stack retargeting and fail-closed directory resolution. * fix: harden Host Console node binding, audit acting_as, and console_session tokens Reject unknown or malformed nodeIds before spawning a PTY. Record hub operators in audit_log.acting_as for remote console_session bridges. Path-scope and one-time-consume console_session JWTs so Host Console mints cannot open container exec or be replayed. * test: expect acting_as in audit CSV export header Align the CSV export assertion with the P0-2B acting_as column added to audit log exports. |
||
|
|
698b7d0713 |
fix: purge deleted-stack notifications from panel and ticker (#1674)
* fix: purge deleted-stack notifications from panel and ticker Stack delete already cascaded scans and drift but left notification_history rows, so the bell and Activity ticker kept showing the deleted stack. Purge those rows in the shared deletion lifecycle, invalidate connected clients, and drop node-scoped in-memory rows immediately. * fix: target remote notification purge by hub node id Remote stack-deleted invalidations always reconcile with hub rn.id, and notification refetch preserves failed node slices instead of wiping them. |
||
|
|
a3edee5e6a |
feat: weekly UTC maintenance windows for mute rules (#1661)
* feat: add weekly UTC maintenance windows to mute rules
Let mute rules suppress only during recurring UTC windows, normalize
replica node identity, and fail-open when remotes lack schedule support
so older nodes never keep an all-day scheduled mute after a successful cleanup DELETE.
* fix: fail closed on corrupt mute schedules and clean invalid replicas
Empty or whitespace stored schedules no longer act as all-day mutes. Invalid schedules trigger remote DELETE cleanup, and the weekly-window form gains accessibility attributes plus component coverage.
* fix: require explicit repair before clearing a corrupt mute schedule
The suppression engine already fails closed on an unreadable stored
schedule (scheduleInvalid), but the frontend never surfaced that flag:
a corrupt rule looked identical to an ordinary unscheduled one, and
opening Edit then clicking Update sent an explicit schedule: null,
silently turning the corruption into a valid all-day mute. Add the
flag to the rule type, show an Invalid schedule badge on the card, and
block saving in the edit form until the operator explicitly touches
the weekly window (configures a new one, or toggles it to confirm the
clear).
* fix: correct contradictory toggle-sequence copy in schedule-repair toast
The blocking toast told operators to toggle the weekly window "off then
on" to confirm clearing a corrupt schedule, but the toggle starts off
for a corrupt rule, so that sequence leaves it on and trips the
no-selected-day validation instead. The correct, tested sequence is on
then off, matching the inline hint below the toggle. Also add a
regression test confirming the invalid-schedule save gate resets
cleanly across edit sessions on different rules.
* fix: enforce replica node_id and guard fleet sync against stale writes
Two hardenings to the suppression-rule fleet sync path found during
review: the /replica endpoint trusted the payload's node_id instead of
forcing it to null server-side, so a direct proxy-authenticated caller
could persist a scoped replica; and upsertNotificationSuppressionRuleReplica
overwrote unconditionally with no ordering check, so a delayed older
POST arriving after a newer one could downgrade the stored rule. Force
node_id to null on every replica write, and skip (with a warning log)
any incoming write whose updated_at is not newer than what's stored.
* test: assert the exact-tie updated_at case in the fleet sync stale-write guard
The staleness guard added in
|
||
|
|
e15b9d1244 |
fix(blueprints): write compose.yaml so first-time apply is not shadowed (#1668)
createStack scaffolds compose.yaml; Blueprint was writing docker-compose.yml, so Compose discovery ran the nginx boilerplate. Align Blueprint writes with the canonical filename, clear alternate root Compose siblings on local/modern apply, and cover the regression paths. |
||
|
|
155db30554 |
fix(blueprints): gate confirmed apply on live intent fingerprint (#1663)
* fix(blueprints): gate confirmed apply on live intent fingerprint reconcileConfirmedPlan only checked approval_status, so a concurrent compose edit during Apply could deploy unapproved content under a stale fingerprint. Match the tick-path fingerprint gate, refuse Apply when live intent drifts, and surface reconciler refusal as PREVIEW_STALE instead of a false success. * test(blueprints): cover matching-fingerprint reconcileConfirmedPlan path Prove the production allow branch still deploys authorized actions when the approval fingerprint matches, and does not execute unauthorized blast nodes. * fix(blueprints): report live approval after confirmed snapshot apply A concurrent edit can clear approval while multi-node snapshot deploy is still running. Keep the in-flight snapshot contract, but re-read live effectiveApproval before responding and warn in the rollout dialog when approval is no longer current. |
||
|
|
3f1f15a6f4 |
fix: keep running containers until stack pull/build succeeds (#1657)
* fix: keep running containers until stack pull/build succeeds Acquire images before reconcile, capture a recovery generation for compensation, and only remove classified orphans after handoff. * fix: address recovery audit blockers for safe stack updates Retire abandoned and expired recovery artifacts, probe compensated runtimes before reporting rollback success, preserve local Docker when deleting a node, validate the exact Compose invocation before capture, and repair updateStack return-contract fixtures. * fix: resolve ESLint errors blocking CI on this branch Unused-import and unused-variable errors left over from the stack deletion refactor: MeshService in stacks.ts (its opt-out cascade moved into DeployedStackDeletionService), a redundant pruneVolumes destructure in deleteDeployedStack (the real one is re-derived from the same input object inside runDeletionBody), and an unused beforeAll import in a Docker-integration test stub. Also scopes the webhook pull-action case body in a block to satisfy no-case-declarations; purely syntactic, no behavior change. * fix: harden recovery probe, cleanup retry, and failed-pull Docker test Reject absent or unhealthy expected replicas before reporting rollback success, keep cleanup records until artifacts are actually removed, fail closed when a mesh override cannot be generated, and assert a real failed pull leaves the original container running. * fix: verify recovery probe image identity and stack-scoped override paths Reject recovered runtimes that use the wrong image or leave scale-zero services running, and confine tombstone override deletion to the intent stack directory so forged cross-stack paths cannot be swept. * test: batch notification cap fixtures in a SQLite transaction Unbatched 1200-row inserts were timing out at the default 30s under CI load even though the same assertions pass in under 2s when green. |
||
|
|
090a0d73ac |
feat: add configurable notification dispatch retries (#1655)
* feat: add configurable notification dispatch retries Allow each node to set 0-3 extra in-process delivery attempts with a fixed one-second delay for routes, agents, and Test dispatch. * fix: harden notification retry settings load/save and channel tests Guard Delivery retries against failed and out-of-order same-node settings responses, and cover Slack/webhook retry classification alongside Discord. * fix: clear Delivery retries saving state and correct screenshot alt Separate save-request ownership from value-generation invalidation so a successful PATCH cannot leave Save retries stuck on Saving, reset saving on node switch, and align the Channels screenshot alt with the committed image. * fix: surface invalid notification retry settings instead of false saved clamp Align Channels GET handling with the backend strict 0-3 parser so stored values like 9 or 1.5 show as error needing repair, matching runtime fallback to 0 instead of displaying a clamped saved policy. |
||
|
|
3b027957c4 |
fix(fleet): isolate corrupt snapshot file decrypt failures (#1650)
* fix(fleet): isolate corrupt snapshot file decrypt failures A single damaged encrypted fleet-snapshot row no longer fails detail, restore, or off-site upload for the whole snapshot. Unavailable members are marked, restore is blocked before mutation, and cloud upload fails closed with no PutObject. * fix(fleet): fail closed on damaged enc snapshot envelopes Unrecognized enc: payloads no longer fall through as usable plaintext. Only clear legacy prose stays readable; delimiter-byte and similar envelope damage stays unavailable through restore and cloud upload. * fix(fleet): subordinate legacy enc prose to envelope shape Legacy exceptions no longer trigger from = or whitespace alone. Encryption-shaped payloads (length and hex density) stay unavailable through restore and cloud upload, while short genuine prose such as enc:hello remains usable. * fix(fleet): preserve non-envelope enc legacy plaintext Any non-empty enc: payload that is not encryption-shaped is kept verbatim, including punctuation forms such as enc:hello-world, while envelope-shaped damage remains unavailable. |
||
|
|
d94e586af3 |
feat(blueprints): require confirmed rollout preview before reconcile (#1649)
* feat(blueprints): require confirmed rollout preview before reconcile Persist place/remove approval with an intent fingerprint and transition matrix so Apply, Retry, ticks, and pin cannot mutate the fleet until the operator confirms the current blast radius. Preview surfaces requirements, health, and informational in-flight rows without executing them. * fix(blueprints): silence unused retry nodeId lint error * test(blueprints): harden approval gate coverage and preview clarity Add real reconcileOne place/remove fan-out and STALE_GUARD regressions, surface reachability in the rollout dialog, align warning totals, and document the fail-closed upgrade pause. * test(blueprints): cover legacy approval schema migration Seed a pre-approval database with an enabled Blueprint and live deployment, run production DatabaseService startup, and assert pending null auth columns plus a fail-closed reconcile gate. * test(blueprints): clarify legacy approval migration fixture Extract seed/boot helpers so the migration regression reads as a linear upgrade path without changing assertions. * fix(blueprints): report apply outcomes and gate manual withdraw Return per-node reconcile outcomes from Confirm Apply, block create preview on unmanaged same-name stacks, and require an approved remove outcome for every manual withdraw or evict. * fix(blueprints): scope withdraw approval to destructive eviction Require remove approval only for snapshot/evict confirms and evict_blocked rows. Keep plain stateless standard withdraw as an immediate stop, and update withdraw-route tests to seed remove approval when needed. |