* feat(db): add auto_heal_policies and auto_heal_history schema and CRUD
Adds two new SQLite tables (auto_heal_policies, auto_heal_history) to
DatabaseService.initSchema() and exposes CRUD methods: getAutoHealPolicies,
getAutoHealPolicy, addAutoHealPolicy, updateAutoHealPolicy,
deleteAutoHealPolicy, recordAutoHealHistory, getAutoHealHistory,
incrementConsecutiveFailures, resetConsecutiveFailures, setPolicyEnabled.
Also adds AutoHealPolicy and AutoHealHistoryEntry TypeScript interfaces.
* feat(events): track health-status duration and expose state accessors
- Add healthStatus and unhealthySince fields to InternalContainerState
- onHealthStatus now records unhealthySince timestamp on first transition
to unhealthy, and clears it when the container recovers or restarts
- onStart resets both fields so a restarted container begins from 'starting'
- Add listContainerStates() and getContainerState() public accessors for
use by the upcoming AutoHealService evaluator
* fix(auto-heal): key allowlist in updateAutoHealPolicy, cascade delete, extract ContainerHealthSnapshot
* feat: add AutoHealService evaluator singleton
Polls every 30 s, matches containers to enabled policies via Compose
labels, and restarts containers that have been unhealthy beyond the
configured threshold. Enforces cooldown, per-hour rate cap, and
recent-user-action suppression; auto-disables policies after repeated
consecutive failures. Also adds DockerEventManager.getService() accessor
required by the evaluator.
* fix(auto-heal): prune stale restartTimestamps, guard undefined policy id
- Prune restartTimestamps entries for containers no longer running after
each container list fetch, preventing unbounded map growth from dead
container IDs.
- Guard against policies with undefined id at the start of the per-policy
loop; warn and skip rather than proceed with a non-null assertion.
- Extract handleAutoDisable private helper to bring executeHeal under 30
lines and isolate the auto-disable side-effect sequence.
- Move ContainerInfo type to module scope.
* feat: add auto-heal API routes and wire AutoHealService lifecycle
Registers five REST endpoints under /api/auto-heal/policies (list, create,
patch, delete, history) with requirePaid + requireAdmin guards and Zod
validation. Wires AutoHealService.start()/stop() into the server startup
and graceful-shutdown blocks alongside MonitorService.
* test: add AutoHealService and DatabaseService auto-heal unit tests
- 15 unit tests for AutoHealService.shouldHeal covering all decision branches
(healthy state, duration threshold, user-action suppression, cooldown,
rate limiting, and correct skipReason values)
- 13 integration tests for DatabaseService auto-heal CRUD: policy round-trip,
stack-name filter, partial update, cascade delete, history ordering/limit,
consecutive failure counters, and setPolicyEnabled toggle
* fix: log AutoHealService shutdown errors consistently
* fix(api): requireAdmin-first guard order and try/catch on auto-heal routes
* feat(ui): add StackAutoHealSheet component
* feat(ui): add Auto-Heal context menu item to EditorLayout
* fix(ui): StackAutoHealSheet label, token, a11y, and useEffect fixes
- Rename 'All services in stack' to 'All services' in combobox options and placeholder
- Replace text-green-600 with text-success design token in actionColorClass
- Add htmlFor/id pairs to all four numeric form inputs for accessibility
- Inline fetch logic into useEffect, removing stale closure risk and eslint-disable comment
- Remove now-unused fetchPolicies and fetchServices standalone functions
- Update 'Auto-disable after' label to 'Auto-disable after (failures)' for clarity
- Add toast.error in policy fetch failure path; services fetch silently skips as before
* docs: add auto-heal-policies feature documentation
* test(e2e): add auto-heal policies CRUD spec
* fix(docs): correct auto-heal-policies nav position in docs.json
* feat: pilot agent outbound-mode for remote nodes
Adds a second mode for managing remote nodes: the agent dials an outbound
WebSocket tunnel to the primary, so the remote host no longer needs an
inbound port, a reachable URL, or its own TLS certificate. Works behind
NAT, residential routers, and corporate firewalls.
The primary multiplexes HTTP and WebSocket requests over a single tunnel
via a hybrid JSON + binary frame protocol, bridged through a per-tunnel
loopback server so existing proxy and upgrade handlers route pilot-mode
nodes identically to proxy-mode ones.
Enrollment uses a single-use 15-minute pilot_enroll JWT exchanged for a
long-lived pilot_tunnel credential on first connect. Proxy mode continues
to work unchanged and both modes are supported side-by-side.
* test(e2e): switch to proxy mode before asserting api_url field
Remote nodes default to Pilot Agent mode, which hides the api_url input.
The SSRF-validation tests need proxy mode, so the helper now selects
Distributed API Proxy after picking Remote type before asserting the
field is visible.
* fix(e2e): wire Combobox id prop so node-mode selector resolves
The Combobox trigger button had no id, leaving its Label orphaned and
making getByRole name-based lookups fail. Adding id to the primitive,
passing id="node-mode" from NodeManager, and updating the E2E helper to
use #node-mode fixes both the a11y regression and the CI timeout.
Add aria-label to the Scan history entry button, aria-labels to the
pagination chevrons, and aria-live polite + aria-label on the page
indicator so screen readers hear "Page 2 of 5" instead of "2 slash 5"
when navigating the comparison sheet and scan history list.
Add a node-local-scope sentence to the comparison intro, document the
cross-image Shared relabeling, and add three troubleshooting entries:
Compare disabled, unexpected results (cross-image and truncation), and
older scan missing from Scan history.
Emit a [Compare:diag] entry gated on developer_mode with the scan ids,
effective tier, input and bucket sizes, suppression count, and
truncation flag. Gives operators a trace to correlate with user reports
of unexpected compare results without adding hot-path cost.
When comparing scans from two different image refs, same-CVE+package
matches are not necessarily the same finding. Relabel the pill and
row status from 'Unchanged' to 'Shared' in that case to convey that
the caveat in the warning banner applies.
Scan history fetched a fixed 200 most-recent rows and paginated them
client-side, so older scans silently fell off mature nodes where
baselining is most valuable. The list now fetches one page at a time
via offset, with status=completed and imageRefLike filters applied
server-side. Search input is debounced to avoid per-keystroke fetches.
Compare endpoint loads up to 1000 findings per scan. When a scan exceeds
this cap, the response now includes truncated=true and row_limit, and
the comparison sheet surfaces a banner so users understand the diff may
be incomplete. Also exposes total_vulnerabilities on scanA/scanB for UI
use and logs a warning when truncation occurs.
Enrich scheduled vulnerability scan completion notifications with
per-severity CVE counts so recipients can triage from the message body
alone. Expose the scan action in the schedule creation UI, require an
explicit node_id, and harden fire-and-forget alert dispatches so a
failing webhook cannot crash the scheduler.
Notification body now reports scanned/skipped/failed counts plus
critical/high/medium totals aggregated across fresh and cached scans,
reflecting the current node posture rather than only what was newly
scanned on this run.
Adds a new SarifExporter service that builds a SARIF 2.1.0 document
from the stored scan findings (vulnerabilities, secrets, misconfigs).
Rule IDs are namespaced to avoid collisions in a flat result list.
Suppressions carry through as SARIF suppressions[] entries so GitHub
code scanning and Defender for Cloud see the same accepted status
shown in the UI.
Exposed via GET /api/security/scans/:id/sarif, admin + paid-tier
gated to match the SBOM export precedent. A SARIF button appears
in the scan sheet next to SBOM and CSV for paid tiers.
Extends Trivy scans with secret detection in image filesystems and
misconfiguration scanning for Compose stacks. Adds tabs to the scan
drawer for vulnerabilities, secrets, and misconfigs. Secret matches
are redacted server-side (first 8 chars + ellipsis) before storage.
- TrivyService: --scanners vuln,secret for images; trivy config for stacks
- DB: scanners_used/secret_count/misconfig_count cols; secret_findings,
misconfig_findings tables; cache key scoped by scanners
- Routes: POST /security/scan accepts scanners array (requirePaid when
secret requested); POST /security/scan/stack; GET .../secrets and
.../misconfigs (paid-tier reads)
- UI: tabs in VulnerabilityScanSheet; scan-options dropdown on images;
Scan config button on stack header
Operators can accept known-benign findings once and have Sencho filter
them out of scan drawers, comparison views, and other read surfaces.
Suppressions replicate from the control instance to every remote node.
* New cve_suppressions table with a COALESCE-based unique index so NULL
scope slots collide the way users expect
* Admin + paid-tier CRUD routes; writes are rejected on replicas
* Read-time filter enriches vulnerability details and compare payloads
without mutating stored counts
* Settings > Security panel for managing rules, per-CVE suppress action
in the scan drawer, dimmed rows with a shield-off indicator
* Vitest unit tests for the filter (glob, expiry, specificity) and
route tests (auth, tier, replica, UNIQUE conflict)
Scan policies now propagate from the control Sencho instance to every
registered remote. The control is the source of truth; replicas render
rules read-only with a managed-by-control banner. Pushes fire on every
policy write, record per-node success and failure on a new
fleet_sync_status table, and use node_proxy Bearer tokens exclusively
so only sibling Senchos can apply incoming sync payloads. Policy scope
now travels as a string identity (api_url or a local sentinel) so
node-scoped rules evaluate correctly on each target.
Side-by-side vulnerability scan comparison with two entry points:
- Compare button plus inline baseline picker inside the scan drawer.
- New Scan History page reachable from the Resources Hub, grouping
completed scans by image with a checkbox selection flow.
The comparison sheet shows a severity delta ribbon, Added/Removed/Unchanged
filters, and a paginated CVE table. Cross-image comparisons are allowed
but flagged with a warning. Compare access is gated to Skipper and
Admiral tiers; the underlying /security/compare endpoint is unchanged.
Scheduled scan tasks now dispatch a completion alert through the
existing notification system: info when every image scanned cleanly,
warning when one or more images failed. The alert includes the task
name and the run's scanned/cached/failed summary so operators do not
need to open the task history.
* feat(security): one-click managed Trivy install
Add a Vulnerability Scanner card to Settings, Security with install,
update, uninstall, and auto-update controls (Admiral-only). The installer
downloads a verified Trivy release into the existing data volume at
/app/data/bin/trivy and defaults the cache to /app/data/trivy-cache, so
no host mounts or extra env vars are required. Detection probes the
managed path, a TRIVY_BIN override, and the host PATH, distinguishing
managed vs host installs. A daily scheduled check surfaces available
Trivy updates, installs them automatically when opted in, and dedupes
notifications per version.
* fix(frontend): silence react-hooks/set-state-in-effect in useTrivyStatus
The initial status fetch and managed-source update check both call
setState from the effect body. Match the existing pattern used in
useDashboardData / SSOSection and disable the rule at the call site.
* fix(security): harden Trivy scan lifecycle, logging, and docs
- Call TrivyService.initialize() at startup so capability state is
accurate before first request; add periodic re-detect to the scheduler
so newly installed Trivy binaries light up without a restart.
- Add markStaleScansAsFailed sweep (+ idx_vuln_scans_status index) to
recover any scan row left in_progress after a crash or timeout; sweep
runs before the paid-tier gate so every tier self-heals.
- Split scanImage persistence into beginScan/finishScan so the manual
scan route owns a single code path and can return a scanId synchronously
while work continues asynchronously.
- Validate image refs on /api/security/scan and /sbom via new utility;
defense-in-depth against shell-metacharacter payloads.
- Dispatch a warning-level alert when a post-deploy scan fails so the
operator has a user-visible path to the failure instead of a silent log.
- Share DIGEST_CACHE_TTL_MS and severity ordering across service and
route layers; remove dead invalidateDetection().
- Add [Trivy:diag] logging gated behind developer_mode for support
diagnostics; production logs unchanged.
- Frontend: defensive toast fallback chain, sr-only SheetDescription,
and a truncation badge when the 500-item detail fetch is capped.
- Tests: extend trivy-service and vulnerability-db suites; add
image-ref and severity unit tests.
- Docs: expand vulnerability-scanning troubleshooting with recovery,
re-detect, and diagnostic-log guidance; link Dockerfile comment to
trivy-setup.
* fix(security): drop unnecessary escape in image-ref forbidden-char regex
Adds a workflow that fires on every v* tag push, computes the window
since the latest release post's anchor version in the sencho-website
repo, and (when the window reaches 5) opens a draft PR in
sencho-website with a fully pre-filled scaffold: grouped
ChangelogSection blocks parsed from CHANGELOG.md, version field set to
the new anchor, and a checklist body covering intro, screenshot, docs
link, and reading time. No auto-publish; the human writes the
narrative.
Uses the same sencho-token-app GitHub App as release-please, with its
installation extended to include sencho-website.
* feat(images): Trivy-powered vulnerability scanning
Scan container images for known CVEs via Trivy. On-demand scanning and
severity badges are available on every tier; scheduled scans, scan
policies, SBOM generation, and scan history are gated to Skipper+.
- New TrivyService (binary detection, per-image scan, SBOM, digest cache)
- Three new tables: vulnerability_scans, vulnerability_details, scan_policies
- 12 routes under /api/security (scan, results, summaries, SBOM, policies, compare)
- Post-deploy async scans wired into all five deploy paths, with a
per-deploy opt-out toggle in the App Store deploy sheet
- "scan" action type added to SchedulerService for fleet-wide recurring scans
- Frontend: severity badges in Resources Hub with animated cursor detail,
scan results drawer with vulnerability table and filters, and a new
Security section in Settings for scan policy CRUD
- Policy threshold violations dispatch a warning or critical alert based on
the policy's block_on_deploy flag; deploys themselves are never blocked
* fix(security): compute scan age in useEffect to satisfy react-hooks/purity
Add docs/security.mdx with the full auth stack as the lead section:
password auth, SSO (5 providers + generic OIDC), TOTP MFA, JWT sessions,
API tokens, RBAC (5 roles), audit logging, AES-256-GCM encryption,
rate limiting, and node-to-node authentication.
Includes tier availability matrix, permission matrix, production
hardening checklist, and vulnerability reporting section. Links to
dedicated feature pages for configuration details.
Add navigation entry in docs.json Reference group.
* feat: add Custom OIDC provider and move SSO to Community tier
Add a generic Custom OIDC provider that works with any spec-compliant
OIDC identity provider (Keycloak, Authentik, Authelia, Zitadel, KanIDM,
Pocket ID, etc.) via standard discovery. Supports configurable claim
mapping for User ID, Username, and Email fields to handle non-standard
providers.
Move all SSO functionality (LDAP and OIDC) from the Admiral tier to the
Community tier so every user has access to identity provider integration.
Backend: add oidc_custom to AuthProvider type, extend SSOService with
claim mapping fields and env-var seeding, add oidc_custom to route
validation, remove requireAdmiral guards from SSO config endpoints.
Frontend: add Custom OIDC card with Display Name, Issuer URL, and claim
mapping fields to SSOSection; add KeyRound icon on login page; remove
AdmiralGate wrapper and lock icon from SSO settings nav.
Tests: update tier guard expectations, add oidc_custom authorize/config/
provisioning tests and claim mapping coverage. All 992 tests pass.
Docs: add Custom OIDC configuration reference, provider-specific setup
examples, troubleshooting section, and updated screenshots.
* fix: settings dialog close button overlap and combobox styling
Reposition the close button in Settings Hub above the scroll area so
it stays fixed when content scrolls. Increase dialog height to
accommodate the growing number of setting sections.
Fix combobox trigger styling to match Input component tokens
(border-glass-border, bg-input) and eliminate the gap between trigger
and dropdown list (top-full -mt-px). Apply the same fixes to
multi-select-combobox for consistency.
Add items-start to the Scopes/Default Role grid so the combobox
aligns with the adjacent input field. Add showClose prop to
DialogContent for consumers that need custom close button placement.
Update SSO doc screenshots at 1920x900.
* feat(convert): harden /api/convert endpoint with auth, validation, and tests
Applies authMiddleware to the docker run to compose endpoint, validates
that the payload is a non-empty string within an 8192 character budget,
rejects inputs containing null bytes, and wraps composerize in a
try/catch that surfaces a 422 with a clear message when the library
cannot produce a services block. Adds a Vitest suite covering the auth
gate, happy path, common flag coverage, boundary and null byte
placement variants, and malformed command handling.
* feat(editor): add From Docker Run tab to create stack dialog
Introduces a third tab in the Create New Stack dialog that accepts a
docker run command, calls the converter endpoint, and previews the
returned compose YAML before writing it to a new stack directory. Uses
the defensive toast pattern, clears the stale preview when the input
changes, and rolls back the empty stack directory if saving the
converted YAML fails so the user never ends up with an orphan stack.
* docs(stack-management): document docker run to compose converter
Adds a Convert from a docker run command section to the stack
management page covering how to use the new tab, the list of supported
flags, and troubleshooting for unparseable inputs. Screenshots show the
empty tab, a successful conversion with the compose preview, the
resulting stack in the editor, and the error toast surfaced when the
input cannot be converted. Appends a matching entry to the
troubleshooting page.