fix(connections): migrate the materialized pre-launch opt-out - #9600
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS A real launch-day trap, fixed at the true ambiguity (when the byte was written), with a bounded one-shot boundary, gated durably on the persist outcome, and every failure path pinned. [DESIGN-REVIEWED] 3244da3 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All checks complete. My verification confirmed: the superseded-defaults registry deliberately never rewrites (recorded decision at First-Principles-Verdict: PASS Verify the #9149-era premise: no pre-launch surface ever let a user deliberately write What this change shipsInventory (5 items) — 5 justifiedIntent: make upgrading installs stop booting with a silently empty Connections gallery caused by pre-launch builds materializing
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 3244da3 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsOne advisory finding — nothing blocks: the FINDING — src/kiro_crew/config/loader.py:3916 — [OPUS-REVIEWED] 3244da3 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
Pre-launch builds wrote connections_ui: false into every config.json they saved -- the key was the opt-in gate then, so a stored false was the default's noise, never a choice. The default-on launch (#9149) reads explicit false as the deliberate opt-out, so every upgrading install boots with a silently empty gallery (hit the owner's own gateway on launch day). The two readings are the same bytes; what tells them apart is WHEN the false was written. A one-shot boundary marker records that: the first load that finds no marker strips a stored false as a delta through the existing _persist_config_migration machinery (in-memory value corrected the same load), then records connections_ui_migrated.json beside config.json -- a sidecar, not a schema field, for the same reason as the superseded-defaults ack file. Every load after the marker honours false as the opt-out it now is. Explicit true, the only deliberate pre-launch act, is never touched. A degraded load skips both strip and marker and retries on the first clean load, matching the existing write-back gate (#4057). This deliberately does NOT go through the superseded-defaults report registry: that mechanism exists for keys where stale-default and deliberate-choice are permanently indistinguishable. Here the pre-launch false had no deliberate meaning at all, so a bounded rewrite is sound where the generic one (tried and reverted, #5244) was not. Truth table in test_connections_ui_flag.py: strip-once with delta isolation, marked false honoured, true untouched, absent key marker-only, second load a no-op.
f41306b to
3244da3
Compare
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsFINDING -- src/kiro_crew/config/loader.py:3916 -- merged False positive or not applicable? A repository writer can comment: |
|
Disposition: FIXED — span=f88546c82bde (marker written on lock-contention deferral) Correct catch, and the exact failure mode named (marker-without-strip freezes the stale Contended-lock test added red-first: |
|
Disposition: FIXED — span=964a6ea5f43f (loader.py:3954 — marker write ignores Same root defect as the design lane's watch item, fixed once: the persist call's return value is captured ( |
|
CI triage (head |
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: root cause is that pre-launch builds materialized connections_ui: false into every saved config as default noise, and post-launch the same bytes read as a deliberate opt-out; a one-shot marker-gated migration strips exactly that stale false once, leaves true alone, and honours every later false forever.
chenmingwei23
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix - one-shot config migration stripping pre-launch materialized connections_ui:false so it is not misread as a post-launch opt-out; root cause clear, config/loader.py plus its test only.
dwu96
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: fix (2 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: fix with a clear root cause — pre-launch builds materialized connections_ui: false into every saved config, so post-launch that byte pattern read as a deliberate opt-out; a one-shot sidecar marker strips exactly the stale false once and honours every later false, config-loader migration plus its tests only.
What
One-shot migration for the launch upgrade trap: pre-launch builds materialized
connections_ui: falseinto everyconfig.jsonthey saved, and after #9149 that stale key reads as the deliberate opt-out — every upgrading install boots with a silently empty Connections gallery. (Hit the project owner's own gateway on launch day; diagnosis was manual key deletion.)How
The two readings of
falseare byte-identical; what distinguishes them is when the key was written. A sidecar boundary marker (connections_ui_migrated.json, besideconfig.json— same placement rationale as the superseded-defaults ack file) records the first post-launch load:false→ stale noise: stripped as a delta through the existing_persist_config_migrationmachinery (re-checked against the on-disk document inside the write lock, other keys untouchable), in-memory value corrected the same load, marker recorded.true→ the only deliberate pre-launch act: never touched; marker recorded.false→ the deliberate post-launch opt-out: honored, forever._persist_config_migrationreturns False with nothing written; the marker defers WITH the strip (gated on the persist outcome) and the whole migration retries on the next load. Marker-without-strip would freeze the stalefalseas a deliberate opt-out forever.Why not the superseded-defaults registry
That mechanism (report, never rewrite — #5244's lesson) exists for keys where stale-default and deliberate-choice are permanently indistinguishable. Pre-launch
connections_ui: falsehad no deliberate meaning at all — the flag was the opt-in gate andfalsewas both default and only behavior — so a bounded rewrite with a recorded boundary is sound where the generic rewrite was not. The one lossy edge: a user who upgraded in the launch→migration window and deliberately opted out gets flipped on once and must opt out again; the migration cannot distinguish that write, and the population is bounded by the window.Verification
test/test_connections_ui_flag.py(red-first): strip-once with delta isolation (auto_updatesurvives untouched), markedfalsehonored,trueuntouched, absent-key marker-only, second-load no-op, contended-persist defers the marker with the strip — suite 15/15.test_config_loader.py+test_config_baseline.py+test_config_meta_stamp.py: 529 passed (no schema change — the marker is a sidecar, so no baseline regeneration).test_config_superseded_defaults.py: 46 passed (registry untouched).test_sandbox_governance_mask.py+test_security_posture.py: 588 passed (new data-home leaf is a non-sensitive sidecar, same class as the ack file).Pattern harvest
Rule candidate: semgrep — a boolean-returning
persistence/write helper called for its side effect with the return value
discarded —
_persist_config_migration(...)already encoded"deferred, nothing written" as
False, and the defect was ignoring it whilewriting a dependent durable record (the marker). The general shape: any
"record that step X happened" write must be gated on step X's own reported
outcome, not on having reached the code path that requested it. Same family
as fsync-then-rename ordering bugs; a semgrep rule flagging bare-statement
calls to known bool-returning writers would have caught this at authoring
time.