Skip to content

Require confirmed provider updates#137

Merged
yaacovcorcos merged 6 commits into
mainfrom
agent/confirmed-provider-update-action
Jul 26, 2026
Merged

Require confirmed provider updates#137
yaacovcorcos merged 6 commits into
mainfrom
agent/confirmed-provider-update-action

Conversation

@yaacovcorcos

@yaacovcorcos yaacovcorcos commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • show Update in Settings > Providers only when a successful version check confirms that the installed CLI is behind a known latest version
  • keep queued/running Updating presentation visible independently of update-availability evidence
  • withhold disk-cached update availability at startup until the current session completes a fresh check, while preserving cached installation/authentication state
  • preserve manual update guidance, managed Antigravity routing, repair/setup/connect behavior, custom binary paths, and provider enable/visibility policy
  • harden the confirmed-update boundary so the updater child process can only ever be spawned against a target that was probed, certified, and re-validated under the settings write lock (closes six concurrency/TOCTOU windows)

This fixes the observed Cursor and Antigravity contradiction where a row could offer Update while the summary said no provider updates were detected, and makes the update path safe under concurrent settings writes, refreshes, duplicate requests, and interruption.

Behavior contract

An action now requires all of the following: provider available, advisory behind_latest, known installed version, known latest version, completed check timestamp, and a supported update path. Unknown, failed, unavailable, stale cached, unsupported, hidden, disabled, and update-check-disabled states do not expose Update. An already queued/running update remains visibly disabled as Updating even if advisory evidence changes.

Concurrency & security hardening

The updater child process is only ever spawned against a target that was probed, certified, and re-validated while holding the settings write lock. Six windows were closed:

  • chore: establish selective upstream maintenance #1 Immutable snapshot + serialized refresh — a single serialized refresh (refreshSemaphore) captures one immutable probe/settings snapshot, threads it through load → revalidate → project, and drops confirmed targets whose embedded authority no longer matches at the commit boundary.
  • fix: target owned monitor repository #2 Validate + capture under the write lockupdateProvider re-validates the confirmed target and captures the exact updater command into immutable locals while holding withSettingsWriteLock, so a concurrent settings write cannot change what gets spawned between validation and capture. The child is spawned outside the lock (spawn + await share a single update-timeout budget), so a slow or hung spawner.spawn can only stall its own request — it can never pin the global settings write lock.
  • fix: prove integration base #3 Request-owned update state — a per-request owner token means a losing duplicate request cannot clobber the in-flight update's running state; owner-scoped writes only land while the request still owns the slot.
  • fix: enforce public Scient identity #4 Interruption-safe cleanup — interruption lands a terminal failed state and the hung child is stopped via a scoped kill finalizer.
  • chore: harden automation and upstream verification #5 Cheap hot readsgetStatuses/stream reads re-derive only a cheap authority key (revision counters) instead of the full maintenance context, so hot reads never re-probe provider CLIs or re-resolve the runtime.
  • Pre-public-release inherited-core readiness audit #6 Monotonic runtime-target revision — runtime target identity is tracked by a monotonic per-provider counter (ProviderRuntimeManager.getRevision) that advances only on real target mutations (transient install progress is excluded). PROVIDER_KINDS is derived from ProviderKind.literals so new providers are covered automatically.

Authority-key derivation is byte-identical between the hot path (getProviderAuthorityKey) and the full context (getProviderMaintenanceContext).

Tests

Deterministic regression tests were added for each finding (no sleeps; determinism via Deferred, raw Promise, TestClock.withLive, and scheduler drains), including:

  • a mutation-sensitive hot-read guard that counts runtime resolve/getSnapshot calls across getStatuses (fails if the hot path reverts to the expensive maintenance context),
  • a mutation-sensitive lock-release guard proving the settings write lock is released before the unlocked spawn (a concurrent settings write does not block behind a parked spawn), while the child is still spawned against the install captured under the lock,
  • a hung-process guard proving a provider process whose await never completes is stopped by the scoped kill finalizer and bounded by the update timeout, reported as a soft failure (never a hang),
  • succeeded / unchanged happy-path guards proving the post-update refreshNow re-probes the CLI (rather than reusing the pre-update version) and that the re-probed advisory drives the terminal succeeded vs. unchanged state and message,
  • duplicate-rejection state preservation and interruption→failed guards,
  • a per-provider revision-isolation test.

Verification

  • bunx oxfmt --check (clean)
  • bun run lint (0 errors; pre-existing warnings only)
  • server tsc --noEmit (clean); web tsc --noEmit (clean)
  • complete server bun run test: 2378 passed, 10 skipped
  • bun run build: 5/5 successful

Follow-ups (out of scope, low-severity)

These were identified during review as cosmetic/polish and are intentionally deferred to keep this security-critical change tightly scoped:

  • Terminal-failure startedAt — when the command lock fails or is interrupted, the terminal failed state is written with startedAt: null, dropping the original start time for a request that had already reached running. Also, a refreshNow failure that occurs after the update command itself succeeded is currently surfaced as a failed update rather than a succeeded-but-refresh-failed state.
  • Uncontended queued flicker — an update with no contender can briefly present queued before running. Purely cosmetic; the terminal state is unaffected.

Risk and rollback

The change centralizes the action gate, serializes the confirmed-update boundary, and invalidates only network-backed update metadata loaded from disk cache. Provider availability, version display, authentication, connection, install/repair, custom paths, and active update state are retained. The hardening is additive and preserves all previously verified behavior (byte-identical user-facing messages). Rollback is a branch revert.

@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 26, 2026
@yaacovcorcos
yaacovcorcos marked this pull request as ready for review July 26, 2026 12:48
@github-actions github-actions Bot added size:XL and removed size:L labels Jul 26, 2026
yaacovcorcos and others added 2 commits July 26, 2026 17:02
…findings

Ensure the provider updater child process is only ever spawned against a
target that was probed, certified, and re-validated under the settings write
lock, closing six concurrency/security windows in the confirmed-update
boundary:

- #1 Immutable probe/settings snapshot threaded through refresh; a single
  serialized refresh (refreshSemaphore) captures one snapshot and
  revalidates confirmed targets at the commit boundary.
- #2 updateProvider re-validates the confirmed target and captures the exact
  updater command into immutable locals while holding withSettingsWriteLock,
  so a concurrent settings write cannot change what gets spawned between
  validation and capture. The child is spawned OUTSIDE the lock (spawn + await
  share one update-timeout budget), so a slow or hung spawner.spawn — whose
  acquire is uninterruptible — can only stall its own request and can never
  pin the global settings write lock.
- #3 Request-owned update state (per-request owner token); a losing
  duplicate cannot clobber the in-flight update's running state.
- #4 Interruption-safe cleanup lands a terminal failed state and kills the
  child via a scoped finalizer.
- #5 Hot getStatuses/stream reads re-derive only a cheap authority key
  (revision counters) instead of the full maintenance context, so reads
  never re-probe CLIs or re-resolve the runtime.
- #6 Runtime target identity tracked by a monotonic per-provider revision
  counter (ProviderRuntimeManager.getRevision), excluded from transient
  install-progress churn; PROVIDER_KINDS derived from ProviderKind.literals.

Adds deterministic regression tests for each finding (no sleeps; barriers via
Deferred / TestClock.withLive / scheduler drains), including a mutation-
sensitive hot-read guard, a mutation-sensitive guard that the settings write
lock is released before the unlocked spawn, a hung-process timeout guard,
succeeded/unchanged post-update re-probe guards, and a per-provider
revision-isolation test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size:XXL and removed size:XL labels Jul 26, 2026
@yaacovcorcos

Copy link
Copy Markdown
Contributor Author

Independent review certification — head 0ddac814

The final head has been independently recertified prior to merge. Four independent read-only reviews cleared the exact merged content, alongside full CI.

  • Concurrency / security correctness — No merge-blocking defects; the security property holds. The child updater can only ever spawn against a target re-validated under the settings write lock: the fully-specified command (command/executable/args/lockKey/pathPrepend) is embedded verbatim in targetKey, so a targetKey match implies command identity, and the command is captured into immutable locals before the lock is released. Spawn/await were moved outside the lock purely for liveness — the real spawner.spawn acquire is uninterruptible, so holding the global settings write lock across a hung spawn could otherwise pin it indefinitely.
  • Test quality / false-confidence — Tests are sound and mutation-sensitive. The prior "hung spawn" test (which modeled an uninterruptible acquire with an interruptible Effect.never) was correctly removed; its honest siblings (hung-child timeout, interrupt→failed, lock-released-before-spawn, succeeded/unchanged re-probe) remain covered.
  • Holistic merge-readiness — Merge-ready. Ownership model, authority-key byte-identity between hot path and full context, lock ordering, interface propagation (all mocks updated), and the web refactor (no orphaned imports) all verified.
  • Final focused recertificationRECERTIFIED: SAFE TO MERGE. Independently re-traced the security property, TOCTOU window, timeout/scope/interrupt handling, ownership clobber-safety, test mutation-sensitivity, and merge hygiene.

CI on 0ddac814: all checks green — Format/Lint/Typecheck/Test/Browser Test/Build, Windows Process Regression, Release Smoke, and both platform smokes. mergeStateStatus: CLEAN.

Known low-severity follow-ups (documented above in the PR body, intentionally out of scope to keep this security-critical change tightly scoped): post-success refreshNow-failure labeling, and the uncontended queued UI flicker.

@yaacovcorcos
yaacovcorcos merged commit 0558ba1 into main Jul 26, 2026
10 checks passed
@yaacovcorcos
yaacovcorcos deleted the agent/confirmed-provider-update-action branch July 26, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant