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 a source-level gated 0.6.0 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. Issue #104 adds a source-level gated UI-neutral job-lifecycle and safe-shutdown boundary. Issue #109 adds its bounded Tasks presentation through five fixed request methods and one validated event listener. Issue #110 adds a fixed, synchronous, transient chat service behind authenticated internal routes. That service requires an exact named profile and model plus current policy and consent. Issue #111 adds fixed stream-start, SSE, and cancellation routes plus a source-only Electron Main/preload bridge. Main owns authenticated SSE, strict owner and sequence validation, bounded batching, acknowledgement backpressure, and cancellation; it exposes no bearer or HTTP authority. Issue #112 adds the bounded renderer Chat presentation, three fixed chat-lifecycle methods, and two fixed native actions. These chat boundaries add no tools, file or database access, genealogy operation, job submission, cloud-account, updater, autonomous action, persistent transcript, or generic command route. The sidecar is not a general domain-data transport. The desktop shell guide defines the supported 0.6.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.6.0 has no updater, update feed, background update channel, or staged rollout.
On Ubuntu, the packaged-runtime and installer checks exercise the production
keyring integration against the distribution-provided GNOME Secret Service.
The checked-in launcher starts that service on a disposable D-Bus session with
owner-only temporary storage, waits for its well-known name, and proves native
store, read, and delete operations with a non-secret probe. Exact-head packaged
verification reaches that service through a private socket and a separate
unpublished Linux verifier package. Only the verifier package
compiles the adapter that reads an exact command-line switch carrying the
owner-only temporary root. The production package is assembled and scanned
first, its adapter never reads the selector, and its build rejects the selector
literal. In the verifier, Electron Main rejects the root outside Linux or when
it is not an absolute Linux path, then derives the sidecar’s home, XDG paths,
and exact runtime/bus address from the root rather than inheriting ambient
values. The launcher binds its private D-Bus daemon to that same owner-only
socket. Every Linux sidecar launch pins the native Secret Service backend,
ignores ambient Python keyring selectors and configuration, and excludes
provider credential and PATH values. Normal launches also exclude home,
cache, configuration, and data values; they ignore ambient D-Bus and XDG
runtime selectors and bind to unix:path=/run/user/<uid>/bus using the
kernel-reported process user ID. Release-installer verification runs the
installed production package against that same production-derived endpoint.
The launcher requires an owner-only runtime directory owned by the current
user. If the endpoint is absent, it creates a private D-Bus daemon, records the
socket identity, and removes only that owned endpoint. If the canonical
endpoint already exists, the launcher requires a current-user-and-group,
non-symlink Unix socket, proves that it is a live session bus with no existing
Secret Service owner, and rechecks its exact metadata before reuse. It never
kills or removes a reused bus. In either path, the verifier starts its own GNOME
Secret Service with only disposable home, configuration, data, and control
storage. The launcher preserves the packaged command’s real exit status and
removes the disposable state afterward. No mock or alternate Python
keyring backend is permitted. This is verification infrastructure only; it
does not add a production credential fallback or weaken keyring-only startup.
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 during normal launches. Linux ignores ambient D-Bus and XDG runtime selectors and derives the native Secret Service bus address from the kernel-reported user ID. The exact internal verification adapter instead permits only paths and the bus address derived from the disposable verifier’s validated absolute root; that adapter is absent from ordinary production packages and does not accept ambient home, XDG, or D-Bus values. - 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 starts from the network-free
provider=nonedefault, binds IPv4127.0.0.1on port0, and exposes authenticated fixed routes only. Ambient provider credentials remain excluded from the packaged process. The supported 0.6.0 composition has/api/v1/healthand/api/v1/capabilities; the source-level gated #105 code 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. Issue #104 adds fixed job list, status, cancel, SSE-event, and safe-shutdown routes. Those routes expose lifecycle metadata only and admit no work. Issue #110 adds fixed chat capability, session-create, session-read, session-delete, and synchronous run routes. A run can leave the network-free default only after exact profile/model policy, current consent, bounded-input, and provider preflight checks succeed. It still has no generic route dispatcher. Issue #111 adds only fixed stream-start, session/run-owned SSE-event, and stream-cancel operations for the same bounded use case. Packaged composition also adds one hidden, bodyless/api/v1/runtime/shutdownroute that only Electron Main can call with the active session credentials; it admits no application work and is omitted from generated OpenAPI. - 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 first invalidates the public active session, then gives the captured
Main-only session up to three seconds to request authenticated, bodyless
runtime shutdown. Uvicorn begins its configured graceful drain, and Electron
allows up to 10 seconds on POSIX to observe the sidecar leader exit. On Windows,
leader observation consumes only the shared deadline remaining after reserving
four seconds for taskkill.exe, five seconds for forced leader-exit
observation, 1.5 seconds for linear directory-cleanup retry backoff, and a
500-millisecond scheduling margin. A full three-second shutdown request
therefore leaves up to six seconds for the first Windows leader observation. A
204 response alone is never termination proof. If the request fails or the
leader remains live, Electron uses the existing bounded forced process-tree path.
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. Electron gives the no-shell taskkill.exe child process four seconds
and independently rejects a nonsettling executor after the same bounded
interval. It then allows up to five seconds for Node to observe the leader exit,
including when taskkill.exe times out, fails, or reports that the leader exited
first. The command result alone is never treated as proof: failure to observe
the leader exit still fails closed. Only after process-tree termination is
verified does Electron remove the private working directory. Windows removal
retries transient filesystem errors at most five times with a 100-millisecond
linear delay; a persistent cleanup failure still vetoes application exit. No
sidecar is started by the development mock shell.
Electron bounds the complete supervisor stop to 20 seconds, including the graceful request and exit observation, any launch already in flight and all verified process-tree termination. Exceeding that deadline fails closed and leaves the supervisor unavailable instead of allowing application shutdown to wait indefinitely.
The sidecar creates its SQLCipher-backed job repository only when startup
diagnostics permit database access. It reconciles any persisted nonterminal
snapshot to exactly one failed JOB_INTERRUPTED terminal state at startup; it
never automatically replays side-effecting work. The private runtime shutdown
callback sets Uvicorn’s graceful-exit signal only after the authenticated,
bodyless request reaches its exact route. Shutdown drains the Uvicorn
server task and loopback listener, child stdio, the supervised process tree,
Electron’s private temporary working directory, and the encrypted provider and
job database sessions. Issue #110 also closes the chat service before its
provider dependencies and discards every process-local session and message.
Issue #111 first cancels and records one payload-free terminal audit outcome for
every active stream. Startup reconciliation records one stable interruption for
an active run left by a prior process without replaying provider work.
Before quitting, Electron main waits for a bounded safe-shutdown assessment. If
work remains active, a native dialog offers Wait, Request cancellation,
or Stay open. Cancellation is cooperative: protected publication can remain
pending-safe-point, and Electron never silently abandons it. Process shutdown
continues only after a safe assessment and verified sidecar stop. Electron owns
the supervisor before asynchronous verification or process launch and installs
its named SIGTERM handler before runtime startup. It idempotently re-arms the
same handler when the Electron-ready runtime takes ownership, so initialization
cannot leave the signal on its immediate-termination default. A degraded startup
admits no process-local jobs, so its main-only shutdown assessment is explicitly
safe empty only while the supervisor is idle, starting, or unavailable,
has no authenticated session, and has never exposed one. Once an authenticated
session has been exposed, losing it never restores that shortcut. The
explicit-empty case skips the unavailable HTTP assessment but still cancels pre-spawn work,
drains any launch already in flight within the supervisor deadline, and
requires verified process-tree stop.
The first app.quit() lifecycle is vetoed during that assessment. After IPC
disposal and verified sidecar stop release every owned resource, the authorized
completion callback uses app.exit(0) rather than re-entering
platform-specific window closure.
Packaged release verification retains only exact, newline-framed shutdown phase
and failure codes emitted by Electron Main. It discards decorated, partial,
overlong, and all other process output, and keeps at most the latest 16 accepted
records. A close timeout therefore identifies the last verified lifecycle phase
without exposing bearer values, response bodies, environment values, or local
paths. These records are release-gate diagnostics and do not change the public
application, bridge, or sidecar contracts.
Issue #111’s chat stream service is registered for orderly drain before its
routes are enabled. Other future provider streams and database sessions must do
the same.
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, credential-management, provider-configuration, endpoint-test, and
consent-administration routes, the fixed chat-stream routes, plus the main-only
job shutdown preflight. The
bridge exposes the typed result, sanitized diagnostics, and retry outcome, but
never the session, bearer, port, resolved address, response body, raw HTTP
data, or a credential value.
For Issue #109, Electron main also uses the fixed job list, status, cancel, and
SSE-event routes. Its internal client applies the bearer and Last-Event-ID,
requires the exact event-stream content type, bounds a response or event stream
buffer to 1 MiB, and fails a stalled connection after three seconds. The renderer
receives only parsed snapshots, events, and stable coded failures; it receives
no HTTP or sidecar connection authority.
Issues #111 and #112 consume the #110/#56 chat boundaries through fixed capability, session-lifecycle, stream-start, SSE-event, cancellation, and acknowledgement clients. Electron Main rejects redirects, wrong content types, wrong owner/run identities, invalid DTOs, and stale, duplicate, or nonmonotonic sequences. Neither preload nor the renderer acquires HTTP, bearer, provider, sidecar-session, or generic route authority.
The supported 0.6.0 window.ancestry surface contains exactly getAppInfo,
getStartupDiagnostics, getCapabilities, retrySidecar, getPreferences,
and updatePreferences. The 0.6.0 source-level gated surface adds three opaque
file-grant methods, exactly five settings/credential methods (getSettings,
updateSettings, getSecretStatus, setSecret, and deleteSecret), and
exactly six provider/consent methods (getProviderConfiguration,
createProviderProfile, validateProviderEndpoint, previewConsent,
createConsent, and revokeConsent). Issue #109 adds exactly five task
request methods (listJobs, getJob, cancelJob, subscribeJobEvents, and
unsubscribeJobEvents) plus the fixed, validated onJobEvent listener. Issue
#111 adds exactly three chat-stream request methods (startChatStream,
cancelChatStream, and acknowledgeChatStream) plus the fixed, validated
onChatEventBatch listener. Issue #112 adds exactly three chat-lifecycle methods
(getChatCapability, createChatSession, and closeChatSession) and two native
actions (openExternalLink and copyText). Issue #348 adds exactly three local-
runtime methods. The resulting source-level gated bridge has 36 fixed request methods;
there is no generic send, listen, route, channel-selection, clipboard, or shell
operation. Issue #104’s shutdown client remains Electron-Main-only.
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.
Main owns every task event subscription and binds it to the requesting sender, current frame, trusted application document, sidecar session, job, opaque subscription identifier, and last accepted sequence. A sender may hold at most 32 subscriptions. Duplicate identifiers fail, explicit unsubscribe is idempotent, and a terminal event closes the stream. Cross-document navigation, renderer exit or destruction, bridge replacement, sidecar-session loss or replacement, and application shutdown abort and remove affected subscriptions. Duplicate or stale events are not forwarded. A gap or expired replay cursor is reported with a stable code so the renderer can refresh the backend snapshot and create one replacement subscription. No task state is persisted in renderer storage.
Main also owns every active chat stream and binds it to the requesting sender, current frame, trusted application document, sidecar session, chat session, and run. A sender may hold at most four active streams. Main batches events within 16 ms or 4 KiB and measures exact UTF-8 JSON delivery bytes. At 256 KiB of unacknowledged data it pauses the private sidecar source; a 15-second acknowledgement stall cancels the exact run and produces a coded terminal outcome. One interrupted connection may resume the same run from its acknowledged cursor, but Main never starts or retries provider execution after output. Terminal delivery stays owned until acknowledged. Cross-document navigation, renderer exit or destruction, sidecar-session loss or replacement, bridge disposal, and application shutdown cancel and remove affected streams. Issue #112’s renderer reduces these events into bounded, owner-scoped transient conversation state. It rejects gaps and ownership mismatches, ignores duplicate or replayed events, acknowledges accepted batches, exposes explicit stop and regenerate controls, and closes its session on teardown. Model text passes through a closed CommonMark/GFM component allowlist with raw HTML, images, embeds, implicit autolinks, and executable actions disabled.
The two native actions remain narrowly owned by Main. openExternalLink accepts
only normalized HTTPS URLs without credentials, control characters, or a custom
port and opens only after a native confirmation displays that exact destination.
copyText writes plain text only. The renderer receives no generic shell,
navigation, or clipboard capability.
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.
Electron Main creates one random UUIDv4 for each application launch and derives one absolute diagnostics directory beneath Electron’s application-data root. It sends both only in the exact private stdin launch frame. Python rejects a missing, malformed, relative-directory, or additional launch-frame field and uses that exact directory for both Python-owned writers. The UUID correlates the Electron Main, packaged-sidecar, and Python-core JSONL streams, but it is not a user, device, installation, authentication, or genealogy identifier. Neither private value is placed in arguments, environment variables, URLs, readiness or health messages, renderer data, authorization material, stderr, or child output.
Each component writes a separate bounded file beneath the Main-owned diagnostics directory. Records use an allowlisted schema and stable event-code catalog, carry no free-form message or metadata field, and are capped before serialization. The writer rotates three 512 KiB files, refuses symlinked directories and active files, and degrades without blocking startup, supervision, shutdown, or exit. The exact stderr shutdown receipt remains the authoritative sidecar termination evidence in Electron Main; structured diagnostic JSON and child output are never accepted as that receipt or as control input.
Only two fixed, zero-argument bridge actions expose diagnostics: Main may open the app-owned directory with the native shell, or clear the bounded files. Neither action accepts or returns a path, URI, filename, record, or arbitrary filesystem operation. There is no renderer read API, export route, upload, telemetry, network transport, CI artifact, or automatic support bundle. See the desktop diagnostics contract for the schema, event catalog, retention limits, privacy rules, and operator procedure.
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.
After the report is ready, writable sidecar startup creates the encrypted
revision 0002 schema only when one bounded inventory query proves the
workspace has no user tables. A complete current schema is reused without DDL,
and only an exact revision 0001 layout receives the reviewed job-table
migration. That packaged migration explicitly begins one native SQLite
transaction before creating either table or index, so any late DDL failure
rolls the entire migration back to revision 0001. Unversioned partial schemas,
unknown revisions, and missing or unexpected tables fail with
DATABASE_MIGRATION_REQUIRED; startup never silently repairs or replaces them.
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.6.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.