#127048 detected both modern nvidia open kernel drivers AND propetiary
drivers as the proprietary one, disabling wayland.
this fix looks for the "open kernel module" string in
/proc/driver/nvidia/version, and lets Wayland rendering continue to be
used.
The fresh-install receipt is kept from before the consent answer and counted
after an opt-in, so "data from before you opt in stays on this machine" was
false for it on the CLI send explainer and Desktop's consentWindow line in all
9 locales; only the pre-chat offer named the exception. Both now carry the same
"apart from the fresh-install note (noted on this machine and counted only once
you opt in)" clause. No i18n key changes; the CLI lines stay under 80 columns.
CLI setup, the pre-chat consent offer and the Desktop collectedReliability line (all 9 locales,
no new key) now say update and install results carry a fixed-list reason and stage, and that a
fresh install is noted on this machine and counted only once the user opts in. The offer's
'data from before you opt in is never sent' sentence names that one exception.
The spawn fixtures run commands that never read stdin, so on a fast Linux
runner the shell can exit before shellExec's stdin write lands and the
write fails with EPIPE. The stream had no error listener, so vitest
reported it as an unhandled error and failed the electron project even
though every test passed. The close verdict is the result; the EPIPE is
noise.
A double that rejects any command over 4 KB - the shape of an OpenSSH mux
socket that cannot pass descriptors behind a large session request - must still
get a verdict, with the judge on stdin and out of argv. Red on base, where the
inlined program makes the gate raise "Could not prove that the remote Hermes
install is clear for SSH startup."
shellSshDouble forwards stdinData the way the real mux exec does, so the live
spawn and v2-marker fixtures now run `python3 -` against a real python3 and keep
proving the argument positions, lock behaviour and verdicts unchanged.
assertRemoteInstallUpdateClear inlined the ~7.5 KB marker gate program with
`python3 -c`, making the ssh command ~8.1 KB. ssh.exec rides the POSIX
ControlMaster mux socket, where ssh writes the session request and only then
hands our stdio descriptors over with sendmsg(SCM_RIGHTS); a request that still
fills the socket buffer - 8 KB for a unix-stream socket on macOS - leaves no
room for that descriptor message, so the pass fails with EMSGSIZE:
mm_send_fd: sendmsg(0): Message too long
mux_client_request_session: send fds failed
ssh exits 255 and the fail-closed gate reads it as "Could not prove that the
remote Hermes install is clear for SSH startup." on every attempt, so no remote
host connects while nothing is wrong on the remote. The window depends on the
client's socket buffer and the command's exact length, which is why a shorter
probe to the same host can still answer.
Hand the program to `python3 -` on stdin instead. Python compiles the whole
program before running it and the gate never reads stdin, so argv keeps the same
`<marker> [payload] [hermes...]` positions while the command stays a few dozen
bytes however the judge grows.
- desktop main.ts: the challenge IPC handler moves to
electron/challenge-window-ipc.ts (registerChallengeWindowIpc), and
resolvePortalBaseUrl moves next to the portal session that already
consumed it (portal-session.ts), so the facade ends below its main
line count; imports are sorted.
- desktop renderer: free_tier.challenge gets its own family handler
(gateway-event/free-tier.ts) in the dispatcher table instead of a
branch in handleLifecycleEvent.
- ui-tui: the challenge presenter and the existing billing step-up link
presenter move to app/gatewayBrowserLinks.ts, keeping the event
switch at or below its main size; the presenter test moves to its own
file.
No behaviour change.
Removed: the desktop/gateway surface-string tests (the gateway's
behaviour is pinned by "never waits", the desktop's by the inherited-flag
invariant), the full-payload-dict snapshot of a status read (the RPC test
covers the field), the server-copy whitespace/length detector, and the
TS "passes optional through as not-required" pass-through check.
Kept: wait outside the locks, background/gateway callers never block,
off-origin URLs refused, no loop on a second 428, the final mint is
authoritative, blips vs verdicts, PoW verdicts unchanged off the token
exchange, control characters never reach a terminal.
Also: FreeTierChallengePayload sorted into events.__all__.
- challenge-window.ts uses createWindowOpenHandler (deny + sanitised
origin log, the link-title window's own hardening) instead of an
inline deny.
- The renderer's outcome type is the generated contract's
FreeTierChallengeResultParams['outcome']; global.d.ts uses the main
process's ChallengeOutcome instead of a third hand-copied union, so a
drift between the window and the contract fails the renderer typecheck.
- runFreeTierChallenge takes the gateway requester itself and builds the
free_tier.challenge_result call, so lifecycle.ts and free-tier.ts stop
each hand-rolling a reporter closure.
No behaviour change.
The main process de-duplicated challenge windows by URL alone, and
remembered settled outcomes by url:attempt. If an optional (measuring,
never-revealed) run of a ticket was in flight, or had just ended
`failed` on #interactive, and the same ticket turned required, the
required ask got the optional run or its remembered `failed`, so no
window was ever shown for the human to clear. Key both the in-flight
run and the settled memory on URL + required.
NAS may answer POST /api/anonymous/token with 428 challenge_required and a
portal URL. Hermes implements the one `browser-v1` primitive: open the URL
(hidden where it can), poll /api/anonymous/challenge/status, exchange again.
Everything the page does (BotID, in-browser proof of work, an interactive
Turnstile fallback, its copy) belongs to the portal and changes without a
Hermes release. The mint stays authoritative throughout.
Python (hermes_cli/anon_challenge.py)
- the exchange reports surface (desktop / cli / gateway), version and the
browser-v1 capability; a UA of the documented shape goes on every call
- the wait runs outside the auth-store and shared-store locks, under a
per-profile work lock; only a caller someone is waiting on waits — the
keepalive tick and a status paint announce the challenge and return
- 403 signin_required / access_denied and any 428 this version has no
primitive for become a terminal sign-in error, scoped to the token
exchange; sign-up and the promotion endpoints keep their previous verdicts
- status-poll 429 / 5xx / transport are blips; a 4xx is a verdict (the
credential is gone); a second 428 starts a fail-fast cooldown so a boot
burst of token readers does not each re-mint and reopen a window
- the pending challenge is published as the free_tier.challenge event and
on free_tier.status for a client that connects later; the desktop reports
how each window ended (free_tier.challenge_result) so the waiter re-mints
promptly, and an attempt counter lets a fresh foreground attempt reopen a
window the user closed without a status replay reopening it
- terminal: print the link, open a graphical browser once per ticket; a
gateway process never opens one; control characters never reach the tty
- challenge URLs are only ever opened on the portal origin the exchange hit
Desktop (apps/desktop/electron/challenge-window.ts)
- the page loads in a hidden, sandboxed window of its own partition, pinned
to the portal origin (will-navigate, will-redirect, off-origin commits end
the run); a phase is read only from a main-frame portal URL fragment
- #interactive reveals the window (never for an optional challenge), #done
and #failed close it; an HTTP error commit, a load failure and a crashed
renderer are retryable, a closed window is not; timers are bounded
- the renderer relays the event and the status read to one window per
attempt and reports the outcome once; a backend without the RPC is fine
Regression test for #131121: after subscribe -> unsubscribe (a row's
mount -> unmount), a second call with the same id must return a fresh
atom — the entry was released instead of being retained for the
window's lifetime — while identity stays stable while subscribed.
Co-authored-by: Yuan Li <dskwelmcy@163.com>
CronJobCreate did not declare a repeat field, so Pydantic dropped it during
request parsing, and _create_cron_job_sync never forwarded one to
cron.jobs.create_job — a finite repeat count submitted via POST /api/cron/jobs
was silently stored as an unlimited recurring job.
- Add optional repeat field to CronJobCreate (int or CLI string form)
- Forward it from _create_cron_job_sync; non-positive counts 400, string
forms coerce through the shared normalize_repeat_value the CLI uses
- Add repeat?: number to the desktop CronJobCreatePayload type
- Route-level regression coverage: finite survives create+list, omission
stays unlimited, invalid counts 4xx with no job stored
The picker door (openSessionFromPicker — /resume overlay, ⌘K, artifacts)
only ran openSession, whose in-place path treats the already-active
session as a front-only no-op. The sidebar's own door always queues an
explicit requestSessionResume, which use-route-resume honours via its
explicitlyRequested branch to re-run resumeSession even when the session
is already active. Give the picker the same explicit reselect so clicking
the active session re-attaches the transcript and re-focuses the composer
(#62045) instead of doing nothing.
The marker probe (~8.3k chars encoded) was the second Windows SSH
command still over cmd.exe's 8191-char limit after the platform probe
moved to stdin: it died with 'The command line is too long' and the
relaunch gate could never observe CLEAR. Ride the same stdin transport
(powerShellStdinCommand) instead of splitting the probe, keeping the
C# no-follow marker reader and its fail-closed semantics intact, and
assert every emitted Windows probe command stays under the limit.
Co-authored-by: nikkoxgonzales <nikkoxgonzales@users.noreply.github.com>
Fixes#106716
Refs #106903
Avoid the Windows cmd.exe command-line limit by sending the PowerShell platform probe over SSH stdin instead of embedding a long encoded command. Keep probe statements newline-separated so PowerShell try/catch blocks parse correctly, and cover the real PowerShell path with regression tests.
Since "an old Desktop never reads a dead owner while the POSIX hand-off dies", posix.sh hands the
update marker to a custodian process that outlives the hand-off and logs
"update marker names its custodian pid N (hand-off pid M)". The producer-receipt test still waited
for the marker to name the hand-off script's own pid and failed at the refresh assertion. It now
requires that log line for this hand-off and asserts the refreshed marker names that custodian.
withdrawBridge re-asked every unsettled answer, so on the never-started
path a helper that errors (a hung or blocked interpreter) cost 3 x the 20 s
MARKER_HELPER_TIMEOUT_MS with the user waiting. Only `busy` (the sidecar
lock, usually brief) is re-asked now; every other answer stands at once
and the existing settled / quit decision is unchanged. The existing test's
"asked three times" for `error` encoded that cost and now expects one ask
(review K132354 P3 withdraw-retries-error).
With no marker the gate ran the script's `reclaim` on every primary boot
and every pool/profile backend; on Windows that is a powershell.exe load
on the boot critical path (20 s timeout). The script answers "not held"
whenever no regular file sits at the checkout lock path, so the gate now
resolves that path exactly as marker.sh checkout_lock_path / marker.ps1
Get-CheckoutLockPath do (checkoutLockPath, replayed against the real
marker.sh for every layout) and skips the helper when it is missing.
Anything it cannot settle (an unreadable .git, a stat error other than
"missing") still asks the script (review K132354-helper-always-spawned).
waitForUpdateToFinish kept the parked run's identity (marker line 2 and the
stable run id) as local state in main.ts and threaded both into
reportHandoffResult. That correlation is update behaviour, so it moves to
update-handoff-report.ts as parkedRunReport(): park(marker) on every live
marker the gate sees, report(host) once the wait ends, matching the result
to the last parked run. main.ts keeps only the composition calls and is
three lines shorter than the parent PR's main.ts.
a0a6009a7ff taught liveMarkerProbe to route a missing marker through the
script helper, which pushed the probe closure past the cyclomatic cap.
helperSubject() now says what is asked about (a dead marker body or no
marker: hold id, owner pid, run identity, log label) and keepsGateShut()
holds the "no marker: only `held` blocks" rule. Behaviour unchanged; the
gate tests pass as they did.
Review 5411223284 (open thread: update-marker-gate.ts absent).
The script helper's `reclaim` answered `absent` the moment the marker file
was missing, before the checkout lock was looked at, and the Desktop gate
(update-marker-gate.ts liveMarkerProbe) returned "nothing runs" for a
missing marker without asking the helper at all. An update that holds the
checkout lock before it publishes its marker, or after it retired it (the
CLI paths of the stacked PRs do both), therefore let the Desktop boot a
backend into a checkout still being written.
- marker.sh marker_op_reclaim_locked / marker-claim.ps1 Invoke-MarkerOp:
no marker -> `held` when the checkout lock (owner byte + lease bytes on
Windows) is held, else `absent`, under the same marker lock.
- update-marker-gate.ts (gate-lane file, additive): a missing marker is also
routed through the helper; only its positive `held` keeps the gate shut
(blocked screen, Retry, Start anyway as for a dead marker). Any other
answer, `error` included, proceeds as before, so a broken script never
blocks a markerless boot.
The gate test "a live owner never reaches the helper; an absent marker
neither" pinned the missing check for the absent half; that half is now the
new "no marker: only held keeps the gate shut" case.
Review 5411223284 (open thread: update-marker-gate.ts ~193 / helper absent).
When the gate saw a marker without a `run:` line (an older producer, or a
boot that never identified the run), handoff-result.ts matched the result's
started_at against marker line 2 for exact equality. The scripts' 300 s
heartbeat (marker.sh marker_refresh_locked, marker-claim.ps1
Update-MarkerHeartbeat) rewrites line 2 with the current time while the run
lives, so for any update longer than 5 minutes the compare failed and,
because the file is consumed before the compare, the result was gone.
Line 2 is now matched as a time inside the result's run: equal to
started_at, or after it and no later than finished_at. A result from an
earlier run finished before this run began and is still discarded. The
consume-before-compare order stays: a result that is genuinely another
run's must not resurface on every later boot.
Two existing assertions used an "other run" with started_at 100 and
finished_at = now against line 2 = 200, i.e. line 2 inside that run's
lifetime, which is exactly the heartbeat case; they now use an other run
that finished before the parked line 2 (same intent, realistic times).
Review 5411223284 regression 2.
When the hand-off script misses the 20 s claim window, the Desktop asks the
script helper to withdraw its bridge marker. Any answer but `taken` used to
report "the updater did not start", kill the spawned tree and (on Windows)
restart the backend. A `busy` or `error` answer leaves the bridge in place,
and the posix daemon re-exec'd under setsid is out of killHandoffTree's
reach: it adopts the bridge whenever it starts and runs `hermes update`
beside the backend this Desktop just restarted.
Only a settled answer (withdrawn / absent / foreign: no script can adopt
this run any more) now reports the failure and kills the tree. busy / error
/ unsupported are re-asked (3 attempts, 250 ms apart); if the bridge still
stands the Desktop quits as for a hand-off, so the next boot's update gate
judges the bridge or the script's claim under the script's lock.
The old "a timeout withdraws the bridge" case pinned `busy` -> failure,
which is the reported bug; it is replaced by busy/error -> quit (bridge
bytes untouched, no restart, no kill) and a withdrawn -> failure + kill case.
Review 5411223284 regression 1.
readAndConsumeHandoffResult was over the code-health CC target (21 > 20).
Extract its phases into module-private helpers in handoff-result.ts:
consumeHandoffResultFile (read, parse, rename-to-.corrupt, unlink),
handoffRunMismatch (run_id validity, run_id / started_at correlation; returns
the exact log line it used to emit) and toHandoffResult (field coercion).
Check order, log lines, and the JSON-`null` body path (still logs "no
finished_at") are unchanged. Behaviour unchanged.
On main the remote-lifecycle test file already sits at its code-health
line cap (2246; over the 2000 target, so it may only shrink). This
branch's fakeSsh rework added one net line and the check refused it.
The comment explaining mutexWrapped moves onto the declaration it
explains; no code or assertion changes.
checkRendererSkew deferred the warning whenever the marker file existed;
Desktop no longer deletes dead markers (A7 rule 3), so a crashed update's
leftover suppressed it indefinitely. The skew checker now accepts an async
isUpdating and main defers only on a LIVE marker (readLiveUpdateMarker,
owner liveness), the in-flight update, or the hand-off. A sync predicate
keeps the check synchronous (review K132345 P3 renderer-skew-dead-marker).
a94916f0313 dropped the failed-receipt fast exit (#122206) because a retry
still reads `failed` while it runs. At this head `hermes update` still writes
v1 markers (pid + started, no `ct:`), which read live on a bare alive pid for
up to V1_MAX_AGE_S, so a recycled pid on Windows parked boot ~20 min again
if this landed before the lock PR.
The gate now treats a live v1 marker (no `ct:`, no delegate) as over when
latest.json records `failed` with a finished_at after the marker's start
second: that update already ended. A retry's marker starts after the old
receipt and a v2 owner's creation time tells a reused pid apart, so both
still wait. The marker is left in place. Also drops the update-gate test
that set the removed `hasFailedReceipt` dep, which nothing read (review
K132345-v1-before-lock, K132345 P3 dead-failed-receipt-test).
main deleted a dead marker and proceeded. The gate now asks the script
helper instead, and every `error` (non-zero exit, timeout, unreadable
script, bad output) kept it shut with no ceiling: machines where bash or
PowerShell is blocked hit the blocked screen after every crashed update,
in the primary and every pool/profile wait.
A body the helper has never answered for now gets HELPER_ERROR_ATTEMPTS
(3) consecutive tries at the 5 s re-probe without a blocked screen, then
the gate proceeds as main did (the marker is left in place), process-wide
so a later pool wait proceeds at once. Once the helper has answered for a
body, a later error stays a transient failure of a working helper and
keeps waiting (R8 M5); held / busy / live are unchanged (review
K132345-helper-error-blocks-boot).
On the module-init repair path the lock body probed our own creation time
with a synchronous `ps` (macOS) and every 25 ms contended poll re-probed the
holder (ps, or a PowerShell of up to 15 s on Windows) on the Electron main
thread. Our own creation time now comes from process.getCreationTime() when
Electron provides it (ownCreateTimeSync), and lockHolderIsLiveSync takes a
creation-time probe the wait caches per pid: liveness is still re-read every
poll, the judging rule is unchanged (review K132345 P3 repair-lock-sync-probe).
The embedded Python and PowerShell judges inlined 0.005 / 1200 / 2.0; they
now interpolate OWN_CT_EPSILON_S / V1_MAX_AGE_S / CREATE_TIME_TOLERANCE_S
from update-marker-judge.ts (which the corpus test pins), so no copy can
drift. The PowerShell judge's process facts and clock moved into
Get-MarkerProcessFacts / Get-MarkerNow (same rule) so it is now replayed
against update_marker_corpus.json wherever pwsh/powershell is on PATH
(review K132345-judge-constants).
attachOrReserveSpawn retried an unreadable /api/health identity for the
whole HOST_SPAWN_GATE_STALE_MS (60 s) budget every 500 ms and re-ran
`git rev-parse HEAD` (5 s timeout) on every round before falling back to
the token attach main always did. The local commit is now resolved once per
attach call, and an UNCONFIRMED identity gets its own HOST_IDENTITY_RETRY_MS
(3 s) budget from first sight: a briefly slow health check is still retried
instead of spawning a second backend beside it, and a definitive mismatch is
still refused (review K132345-attach-identity-retry).
What: waitForManagedUpdateOperations takes an optional timeoutMs and answers
false when operations are still pending at the deadline (nothing is
dropped: they keep running under their own registration). The apply path
joins through joinManagedUpdatesForApply, bounded at 120 s; still pending,
apply returns ok:false 'managed-update-running' without resolving a
strategy or spawning the hand-off. before-quit keeps the unbounded join.
Why: an unresolved or late-registered managed operation parked applyUpdates
forever; the bound gives apply its own deadline while still never starting
local mutation (or the scripts' Desktop-exit deadline) under a live remote
writer.
Review: andrexibiza 5413004252 H3 (apply half; the quit half is recorded
as a DECISION: bounding before-quit means abandoning the observer/restore
transaction, which needs a durable remote-work hand-off design).
Red: start sha fails both new managed-ssh-update tests (join still parked
at 2 s; no joinManagedUpdatesForApply). Green: managed-ssh-update 37/37.
main.ts: 19823 -> 19829, still below the start sha's 19835.
What: processCreateTime (the async probe every marker judgement uses) no
longer falls through to processCreateTimeSync: Linux reads /proc with
CLK_TCK fetched once by async getconf, macOS asks `ps -o lstart=` through
async execFile, Windows keeps its async CIM call. hostJudgeEnv's isAlive is
async and reads the macOS process state through posixProcessStateAsync;
JudgeEnv.isAlive may return a Promise. psCreateTime/processCreateTimeSync
stay only for the module-init repair lock, which cannot await.
Why: the gate polls on Electron's main thread while a marker exists, and
the sync path blocked it on execFileSync ps (5 s timeout) on macOS, on
getconf on Linux, and on PowerShell (15 s) wherever the sync variant was
reached.
Review: kshitijk4poor 5411222842 (psCreateTime / PowerShell execFileSync on
the main thread).
Red: start sha fails both update-marker-async-probe tests (execFileSync
getconf / ps observed). Green: both pass; marker suites 192 + 1 skipped.
Native: the macOS branch runs here against procps `ps` with
process.platform spoofed; the Windows CIM branch is unchanged code and is
not executable on this Linux host.
What: update-marker-judge.ts exposes identityStateSync, the one copy of the
A7 rule-4 identity rule; the async judge reads the same facts and applies
it. processIsLiveSync (a second, drifting copy) is replaced by
lockHolderIsLiveSync, which runs that rule for the module-init repair lock
with the lock's mtime as the v1 ceiling anchor.
Why: the copy had no own-pid check and no v1 age ceiling, so a repair lock
naming our own pid without our creation time (a previous incarnation whose
pid we reused) or a live unrelated pid with no creation time was treated as
held forever and every launch threw 'Could not repair the desktop
installation ID'.
Review: kshitijk4poor 5411222842 (processIsLiveSync second copy).
Red: start sha fails both new desktop-installation tests ('...our pid
without our creation time...', '...live only within the v1 ceiling').
Green: desktop-installation 11/11, marker corpus/readonly/gate/handoff 176.
What: holdTicker({show, clear}) in update-marker-gate.ts owns a wait's hold
state machine (keep the probe's latest held state, consume it per poll,
show it after the unbroken grace, clear otherwise); the primary boot wait
in main.ts and waitForPoolUpdateClearance both use it, replacing the
copy-pasted held/holdGraceClock/show-or-clear code. UpdateHoldWire moves to
electron/update-hold-types.ts and src/global.d.ts re-exports it instead of
hand-mirroring it as DesktopUpdateHold (renderer users take UpdateHoldWire).
Why: the hold state machine was duplicated between the two waits and the
wire type had two definitions that could drift. c8fc9ae5b4a/b5e2fc62e3b had
already moved the controller ({show, clear} + registerUpdateHoldIpc) and the
pool wait out of main.ts and replaced the positional optionals with
updateGateDeps({onLiveMarker, onHeld, onOverride}); this finishes the split.
main.ts: 19835 -> 19823 lines (origin/main 19864). Same calls, same order.
Review: kshitijk4poor 5411222842 (shape: main.ts growth, duplicated hold
state machine, UpdateHoldWire mirror).
Tests: update-marker-gate.test.ts holdTicker (red on start sha: no export).
What: attachOrReserveSpawn re-validates a ready backend whose /api/health
identity stayed unreadable (timeout, transport error, non-2xx, bad body)
for the whole wait budget with the token-only handshake instead of taking
the spawn gate. A backend that answers a different commit, or answers
without one, is still refused.
Why: d4e0c1347d5 made a transient read 'unknown' and retried it, but at the
end of the budget the loop fell through to the spawn reservation, so a
correct but persistently slow /api/health still produced a second backend
beside the first. Only a definitive non-matching commit is a mismatch.
Review: kshitijk4poor 5411222842 (host-attach identity read regression).
Red: start sha fails 'an identity unreadable for the whole budget keeps the
token attach' (reservation taken). Green: backend-discovery 21/21.
desktop-installation.ts::reclaimDeadRepairLock read the lock, judged its
holder dead, re-read and compared, then unlinked BY NAME: a peer that
reclaimed the same dead lock and created its own in between (after our
compare, before our unlink) lost its fresh lock, letting a third launch
into the repair section beside it. Reclaim now renames the lock to a
private `.reclaim` sibling (the atomic claim: one reclaimer wins it),
unlinks it only if the moved file still holds the bytes judged dead, and
otherwise links it back under the name without clobbering a newer lock
(rename back where the filesystem has no hard links). Never throws, as
before.
Also fix global.d.ts's DesktopUpdateHold comment: UpdateHoldWire lives in
electron/update-hold-wiring.ts, not main.ts.
Review G5.