Packaged desktop sidecar
Issue #225 adds the control-only native sidecar used by the packaged Electron main process. Issue #102 hardens its payload verification and process-tree supervision. Issue #226 adds the narrow typed bridge that lets the renderer read sanitized control state without becoming a sidecar client. Issue #105 adds an unreleased 0.6 source boundary for atomic non-secret settings and write-only credential management. Issue #107 adds a sanitized schema-v1 startup report, keyring-only packaged secret resolution, and fail-closed mutation gating for local first run. None of these changes exposes genealogy, job, chat, provider execution, cloud-account, updater, or generic command routes; the sidecar is not a domain-data transport. The desktop shell guide defines the supported 0.5.0 user surface, installation model, and sanitized recovery contract.
Native targets and release evidence
The native build workflow creates a self-contained PyInstaller directory for
each target and embeds it under Resources/sidecar/<target>/ (macOS) or the
equivalent Electron resources directory on Windows and Linux.
| Supported operating system | Architecture | Resource target | Native CI runner |
|---|---|---|---|
| macOS 15 | arm64, x64 | darwin-arm64, darwin-x64 |
macos-15, macos-15-intel |
| macOS 26 | arm64, x64 | darwin-arm64, darwin-x64 |
macos-26, macos-26-intel |
| Windows 11 | arm64 | win32-arm64 |
built and executed natively on windows-11-arm |
| Ubuntu 24.04 | x64 | linux-x64 |
ubuntu-24.04 |
The native build writes a deterministic sidecar-manifest.json containing the
exact target, application/sidecar build, and sorted full payload inventory. It
binds regular files by size and SHA-256 and records only safe in-tree symbolic
links. The Electron production build embeds the SHA-256 of that adjacent
manifest in main-process code. Before generating a bearer or starting a child,
main verifies the embedded digest, exact target/build, complete inventory,
every file, and every link. An unexpected, missing, substituted, or escaping
entry fails closed with only a structural startup diagnostic.
This manifest binding detects sidecar-payload substitution; it is not a
publisher signature, notarization, or whole-application integrity mechanism.
Project-produced 0.x release installers and annotated tags remain unsigned by
policy. Manifest binding does not make the payload publisher-signed and cannot
authenticate a wholly rewritten application bundle. Trusted publisher signing
and applicable notarization remain the Issue #132 distribution gate and become
mandatory at v1.0.0. The
verify-to-spawn filesystem interval also remains a residual TOCTOU boundary;
trusted signing does not remove the need to narrow or eliminate that interval.
The workflow smoke-tests the native executable before packaging and verifies the exact packaged resource afterwards. A system Python installation is not used at runtime. CI output is an unsigned, unpacked verification artifact, not a supported release. Supported distribution requires a manually installed installer plus provenance, installation, target-execution, and packaged assurance gates. Version 0.5.0 has no updater, update feed, background update channel, or staged rollout.
Private lifecycle
- Electron main resolves only the current native resource target and completes the manifest verification described above.
- Only after verification, it creates a fresh 32-byte (256-bit) bearer with the operating-system random source.
- It starts the executable with no arguments, no shell, a private temporary
working directory, and an allowlisted environment. Provider credentials,
PATH, and home-directory values are not inherited. - Electron writes one bounded JSON line to stdin containing the exact API contract, application build, and bearer. The bearer is never placed in command-line arguments, environment variables, renderer state, readiness output, or diagnostics.
- The sidecar forces
provider=none, binds IPv4127.0.0.1on port0, and exposes authenticated fixed routes only. The released 0.5.0 composition has/api/v1/healthand/api/v1/capabilities; the unreleased #105 source adds/api/v1/settingsplus fixed status, set, and delete operations beneath/api/v1/secrets/{reference}. Issue #107 adds the read-only/api/v1/startup-diagnosticsroute. It still has no generic route dispatcher. - It emits one bounded readiness line containing only the contract, sidecar build, and assigned port. Electron validates all three fields and verifies a token-derived HMAC health proof before marking the private session ready.
Contract or build mismatch fails closed without restart. Startup and probe
work are bounded to 10 seconds. Other launch failures and unexpected crashes
receive at most two restart attempts for the application lifetime. Application
quit allows up to 12 seconds for Uvicorn’s configured 10-second graceful
shutdown, then force-kills and performs one final one-second wait. POSIX
launches use a detached process group and signal the full group even if its
leader already exited. On Windows, the sidecar assigns itself to a
non-inheritable, kill-on-close Job Object before reading bootstrap input, while
Electron uses taskkill.exe /T /F for a live tree. Closing the sidecar owner
therefore terminates descendants even when Electron cannot observe the original
leader. No sidecar is started by the development mock shell.
The current shutdown drains only resources that the implemented control
sidecar actually owns: the Uvicorn server task and loopback listener, child
stdio, the supervised process tree, and Electron’s private temporary working
directory. FastAPI exposes an application-lifespan shutdown hook, but the
current health/capability-only composition registers no domain jobs, provider
streams, or database sessions. Every future resource of those types must
register an orderly drain through that lifecycle before its route is enabled.
The native Windows descendant-kill assertion can run only on Windows; the
exact-head hosted windows-11-arm receipt is the authoritative native proof.
Non-Windows local runs exercise only the explicit no-op branch and do not
substitute for that evidence.
The supervisor exposes a main-process-only control interface. A fixed-route
internal client may acquire the authenticated session only while its lifecycle
is ready; the session is cleared before restart, failure, or shutdown. The
interface also exposes sanitized lifecycle diagnostics and one application-
lifetime manual retry. Concurrent retry requests share a single launch attempt,
and an exhausted retry is a deterministic no-op. Electron main uses the session
only for authenticated requests to its fixed startup-diagnostic, capability,
settings, and credential-management routes. The bridge exposes the typed
result, sanitized diagnostics, and retry outcome, but never the session,
bearer, port, raw HTTP data, or a credential value.
The released 0.5.0 window.ancestry surface contains exactly getAppInfo,
getStartupDiagnostics, getCapabilities, retrySidecar, getPreferences,
and updatePreferences. The current unreleased source adds three opaque
file-grant methods and exactly five settings/credential methods:
getSettings, updateSettings, getSecretStatus, setSecret, and
deleteSecret. There is no generic send, listen, route, or channel selection
operation. Main accepts a call only from the registered
WebContents, its exact current main frame, and the exact trusted
app://bundle/index.html URL. It rechecks those facts on every request.
Arguments and responses must also pass strict runtime schemas and a structured-
clone policy that rejects unknown or inherited fields, accessors, symbol keys,
sparse arrays, non-finite numbers, repeated references, cycles, excessive
depth, and excessive UTF-8 bytes or item counts. Preload validates the response
again before exposing it to the renderer.
Main admits at most four non-coalesced operations per renderer and queues at most eight more. Capability reads share one in-flight operation for up to 32 callers. Every call has an absolute five-second deadline; queue saturation, timeout, and cancellation return stable redacted codes rather than backend details. Cross-document or unclassifiable navigation of the main frame, renderer exit or destruction, bridge replacement, sidecar-session loss or replacement, and application shutdown cancel and clean up affected work. Trusted same-document application route changes preserve work; main still rechecks the exact current frame and trusted URL on every request. Establishing the first healthy session does not cancel the retry that created it. Timed-out underlying operations continue to occupy an active slot until they actually settle, so an uncooperative backend cannot turn repeated renderer timeouts into unbounded hidden work.
Preference updates require the last renderer-visible non-negative revision and
return a coded conflict when it is stale. Packaged main persists the exact
bounded preference schema in preferences.json beneath Electron’s OS app-data
directory. Writes are validated, serialized, and atomically replace the file
without following a preference-file symlink. Missing and supported legacy data
use safe defaults; corrupt and unsupported data produce stable path-free
diagnostics and are not silently overwritten. The renderer receives neither
the storage path nor any additional storage capability.
Application settings use a separate Python-owned schema and revision. A read
returns the complete five-setting catalog with reviewed labels, help, types,
defaults, validation bounds, restart flags, sensitivity flags, and current
values. A patch supplies the exact visible revision and only changed
allowlisted keys. The service rejects stale revisions, unknown keys, sensitive
settings, invalid values, missing schema fields, and unsupported schema
versions before serializing one atomic AppConfig replacement. The renderer
cannot select a storage path or submit an arbitrary configuration object.
Credential management is narrower still. Python owns the exact secret-reference
allowlist and exposes only present, missing, or unavailable status plus
explicit set and delete operations. The set request contains one write-only
value and no read route or response can return it. The OS keyring remains the
sole writable authority. Credentials sourced from the environment are
read-only for explicit CLI/headless operation. The packaged sidecar selects
keyring-only mode and never consults those environment variables. Unavailable
or locked keyring behavior fails closed with stable redacted codes instead of
using Electron safeStorage, renderer storage, an environment fallback, or a
plaintext file. A successful delete is returned only after an immediate
presence check proves absence. Main, preload, mock fixtures, and renderer
caches retain only status metadata; the renderer clears its password input
before awaiting the bridge and again after every attempt.
Diagnostics and recovery
User-facing failures are deliberately generic. Stderr is drained but not forwarded into Electron logs; structural sidecar diagnostics contain no bearer, port, URL, request, response, genealogy, provider, filesystem payload, raw exception, or stack. Lifecycle diagnostics contain only state, a generic failure class, and remaining automatic/manual retry counts.
Once the private sidecar session is ready, Issue #107’s fixed read-only route
returns a schema-v1 startup report. It contains an overall ready or
degraded status, normalized platform/architecture labels, and exactly four
ordered components: configuration, SQLCipher, keyring, and workspace. Each
component has only a reviewed status, stable code, message, remediation,
restart requirement, and blocks_mutations value. Unknown schemas, component
names, fields, statuses, or codes fail response validation. Any blocking
component rejects settings and credential mutations with
STARTUP_MUTATION_BLOCKED; the renderer also keeps preference changes and
capability loading disabled while still allowing Diagnostics and the one
bounded main-owned retry.
Startup inspection is side-effect-free: it does not write configuration, initialize a database, create a key, alter keyring contents, or weaken permissions. Do not add usernames, hostnames, absolute or temporary paths, environment values, records, prompts, payloads, raw launch frames, response bodies, executable paths, stderr, exceptions, or stacks to the report or support evidence.
For a startup or compatibility failure:
- observe the degraded lifecycle or startup-component state; the window remains open for read-only Diagnostics;
- follow only the reviewed component remediation, such as unlocking the OS keyring, restoring valid configuration, installing supported SQLCipher, or repairing the app-owned workspace directory and owner-only permissions;
- use the bounded main-process retry at most once, or quit the application so any supervised process is terminated;
- never initialize a replacement encrypted database, replace an existing key, or select plaintext SQLite as a recovery shortcut;
- reinstall the same complete application build to restore a matched Electron/sidecar pair when the failure is structural;
- run the native smoke and packaged-resource checks for that target;
- if the problem persists, record only the application version, normalized platform labels, gate name, and stable generic failure code.
Local native verification uses:
uv run python scripts/build_sidecar.py --expected-target darwin-arm64
uv run python scripts/smoke_sidecar.py \
desktop/build/sidecar/darwin-arm64/ancestryllm-sidecar/ancestryllm-sidecar
pnpm --dir desktop build
pnpm --dir desktop exec electron-builder --config electron-builder.yml --dir --mac --arm64
node desktop/scripts/verify-sidecar.mjs darwin-arm64 desktop/release
Choose the exact native target; cross-built sidecars are rejected. A desktop
support or 0.5.0 release claim also requires the release tracker, declared
binary-signing mode, platform execution, installation, and packaged assurance
gates to pass. Unpacked CI artifacts do not satisfy those gates.