fix(dashboard): stop a stale agent-config snapshot resurrecting app MCP bridges - #7465
Conversation
Design Review (Fable 5) — ✅ PASSDesign-level review of All the design questions I probed — the colon-lexical region boundary, the deliberate census exclusion, the None/{} distinction, the deferred root cause — are resolved coherently in the diff, documented in the spec update, and the residual risks (silent drop feedback, superseded-definition half, optimistic concurrency) are already named and tracked by the author in #7470. Nothing survives the kill-filter. Design-Verdict: PASS Narrow, well-bounded closure of the present-axis resurrection; costs stated, alternatives weighed, residual halves explicitly filed rather than silently left open. [DESIGN-REVIEWED] 7fac608 |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All claims verified: the kept-contract test exists at First-Principles-Verdict: PASS A reported resurrection defect (#7089) fixed at the mechanism, with its one removed capability declared, priced, and given a surviving path. What this change shipsIntent: stop a stale dashboard editor snapshot from re-creating MCP bridges the platform removed — a FIX.
Root cause is honestly placed: the deeper cause (a whole-file snapshot PUT over a multi-writer file) is named, its remaining half — a submitted row superseding a platform correction — is counted (1 unfixed sibling, self-healing) and filed as #7470 rather than smuggled in. No duplicate mechanism exists: the declared-name census was considered and rejected on a concrete ground (it would rescue the scrubbed row), not on preference. No new config key, flag, or public surface ships; zero one-way doors beyond the declared editor restriction, which stays reversible. [FIRST-PRINCIPLES-REVIEWED] 7fac608 |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsI've analyzed the candidate and the surrounding code paths. The candidate concerns the docstring/comment claim that Falsifying it:
This is below the 80 bar and is at root a comment-accuracy observation, which this pipeline does not treat as a finding. Dropped. I re-derived the core logic (namespace No findings. [OPUS-REVIEWED] 7fac608 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
e51d0ba to
38b87a3
Compare
|
Dispositions for both advisory lanes on Design Review -- "the deferred optimistic-concurrency/etag follow-up (or the proposed ratchet test) is worth filing now so per-axis rules stop accreting in this handler." Agreed, filed: #7470. It carries the remaining content axis (a stale snapshot reverting a live bridge to a superseded definition), both candidate shapes with the contract reversal priced, and the ratchet test as the cheap gate to take even if neither shape is chosen. Worth correcting one thing in my own PR body while I am here: I wrote "third defect in one span" in the Pattern harvest section. It is the second (#6664 absent, #7089 present) -- fixed in the rewritten section. First Principles -- "Shrink What I deliberately kept rather than moved, because a reader of the code has to know it to not undo it:
What moved out entirely: the cost paragraph, the host-exclusion paragraph and the order-independence paragraph are now one sentence each or live only in section 1a. Re-verified after the trim: 60/60 across The |
38b87a3 to
8c2aee5
Compare
|
GPT 5.6 BLOCKING on I checked reachability rather than taking it on the label, and the finding is stronger than "a keyless spec is conceivable": the state is produced by this handler itself.
So a spec can sit keyless while an old editor tab still carries Fix is the one GPT named. A missing key now answers if "mcpServers" not in on_disk:
return {} # definite: holds no bridge
servers = on_disk["mcpServers"]
return servers if isinstance(servers, dict) else NoneA Two tests, both mutation-verified against
The absent-axis rule is unaffected: it tests Gates on Separately: |
8c2aee5 to
e3f0f23
Compare
e3f0f23 to
64cbe9f
Compare
|
GPT 5.6 advisory on The finding is right and it matters more than a wording nit, because that sentence is the one a reader takes the rule from. It read "may the client CREATE a name in the app-namespace region? It may not", which is unconditional, while the implementation answers nothing when the installed spec is unreadable and lets the submission land -- a path documented two paragraphs lower and pinned by Now: "Not while the installed spec is READABLE", plus one sentence saying why the qualifier is load-bearing rather than a hedge -- an unreadable spec answers nothing, so the submission lands as it did pre-fix. The spec's section 1a table already carried both rows, so the two are consistent again in both directions. Also confirming the state this head arrives in, since the previous one is where the lanes converged: on Gates on |
…CP bridges PUT /api/agent/config persists a whole-file snapshot the client read earlier. #6975 made on-disk state authoritative for a name the submission OMITS; a namespaced name the submission still CONTAINS was written back verbatim, so an editor tab that loaded while an app was installed and running re-created that app's bridge on save after the app had been uninstalled or disabled. Startup reconciliation only re-registers ENABLED apps, so nothing removed it again and the bridge stayed live and callable. Add _drop_unbacked_app_entries: a submitted mcpServers name containing ':' that the installed spec does not hold is dropped, because only _register_mcp_servers writes the <app>:<server> region. Absence from disk is a verdict reached three ways -- uninstall, disable, or a deliberate skip of an HTTP server with no live port whose illustrative URL would break every kiro session -- which is why the declared-name census is not consulted here and why this rule performs no manifest I/O and cannot fail a PUT. Host-owned names are excluded, including edition extras whose key contains ':'. Both directions now read the spec once, through _on_disk_mcp_servers, so they cannot disagree about their baseline; None (unreadable) and {} (read, empty) are kept distinct because only the new rule's verdict depends on the difference. Where the name is on disk and submitted, the submitted row still wins: the editor-snapshot-wins contract kept in #5899 is not reversed here.
64cbe9f to
7fac608
Compare
|
GPT 5.6 advisory on I verified the claim in code rather than accepting it: The reframing it suggested is the right one, so the region is now described as reserved from this endpoint rather than as having one writer. Both writers are named, and the sentence that matters is stated positively: a name either of them actually placed on disk is present, and therefore untouched -- what this rule withholds is only the ability to INTRODUCE such a name through the raw editor, where the client's copy is indistinguishable from a resurrection. Worth being explicit that this changed no behaviour and exposed no gap. The rule was already keyed on presence on disk, never on which writer put it there, and the cost paragraph already said the MCP page is the path that adds a server "after which this rule sees it on disk and leaves it alone" -- consistent with the sync writer existing. Only the justification clause overstated. The same correction is applied to spec section 1a so the two do not drift. On the loop: this is my last push for an advisory-only finding. Each of the last two pushes drew a fresh prose advisory (keyless-key BLOCKING -> readable-spec qualifier -> single-writer claim), and all four lanes have returned green on every one of those heads. Continuing to re-roll a full matrix per sentence is not convergence. Any further NON-blocking advisory I will disposition in writing here without amending; I will push again only for a genuine blocking finding or a red attributable to Gates on |
bolichen97
left a comment
There was a problem hiding this comment.
CI fully green, all findings dispositioned. Approved.
What is the problem?
PUT /api/agent/configpersists a whole-file snapshot the dashboard's raw agenteditor read earlier. #6975 made on-disk state authoritative for a name the
submission omits -- an app bridge registered after the client's read is no
longer clobbered. The mirror case stayed open: a namespaced name the submission
still contains was written back verbatim.
So an editor tab that loaded while app
demowas installed and running holdsdemo:notesin its snapshot. Uninstall or disable the app --_deregister_mcp_serversremoves the bridge -- then press Save in that old tab, and the entry comes back.
Why this issue matters to the user
The resurrected entry is a live MCP server in the config kiro-cli reads, so the
removed app's code becomes launchable again through it. And it does not go away:
reconcile_enabled_app_resourcesonly re-registers enabled apps, so startupreconciliation never revisits the namespace of an app that was disabled or
uninstalled. Nothing logged, nothing to notice -- the entry simply persists until
someone hand-edits the file.
The same shape reaches a third, worse state.
_register_mcp_serversdeliberatelyrefuses to write an HTTP server with no resolvable live port, and scrubs any stale
row for it, because a manifest's illustrative port is a reachable-looking dead URL
that kiro-cli dials on every request: one connect failure there is retried and
then surfaced as a hard error, breaking every kiro session on the host rather than
just that app's tools. A stale snapshot wrote that scrubbed row straight back.
How our fix solves it
The chain is: the PUT trusted the client's copy of a name in a region the client
does not author, and the platform's
<app>:<server>region is written only by_register_mcp_servers. So absence from disk is a verdict, not a gap:case above).
_drop_unbacked_app_entriesdrops a submittedmcpServersname containing:that the installed spec does not hold. Three consequences worth naming:
owner because it decides whether to keep something the client asked to remove;
every candidate here is one the client is adding, which the on-disk map answers
alone. Consulting the census would also rescue the third case, writing back
exactly the URL the registration path scrubbed. It also means this rule does no
manifest I/O and cannot raise
app_ownership_unreadable, so it adds no new wayfor a PUT to fail.
:. The host axis is unchanged._on_disk_mcp_servers,so they cannot disagree about their baseline.
None(unreadable) and{}(read,no servers) are kept distinct, because only the new rule's verdict turns on the
difference: against
{}a namespaced submission is an addition, againstNonenothing is known and nothing is decided.
What this deliberately does not do: where the name is on disk and submitted,
the submitted row still wins untouched. That is the editor-snapshot-wins contract
kept in #5899 and re-affirmed for #6664, and reversing it is a separate ruling --
see the last section.
The cost, stated plainly: a name containing
:can no longer be introduced throughthis raw editor. The MCP page is the path that adds a server, after which the name
is on disk and this rule leaves it alone. Nothing becomes unremovable -- an entry
already on disk stays deletable through the absent-axis rule. The alternative was
to trust a colon-shaped key the client invented, which is indistinguishable from
the resurrection this exists to stop. Dropped names are logged at WARNING.
What tests we did
New section (d) in
test/test_agent_config_merge_on_write.py, all through the livehandler:
test_a_stale_snapshot_cannot_resurrect_an_uninstalled_apps_bridge-- theheadline case; the user's own plain entry in the same submission is untouched.
test_a_stale_snapshot_cannot_resurrect_a_disabled_apps_bridge-- also declaresthe same name in the user's own mcp.json, pinning that a scope declaration does
not rescue it.
test_a_stale_snapshot_cannot_rewrite_a_deliberately_skipped_http_bridge--installed, enabled and declaring the server, still dropped; this is the row that
pins the census out of this axis.
test_both_directions_decide_from_one_baseline_in_a_single_put-- one PUT thatdrops a stale addition and preserves an absent owned bridge.
test_a_readable_spec_with_no_servers_still_drops_a_namespaced_addition--{}is an answer.test_an_unreadable_spec_still_persists_a_namespaced_entry-- best-effort holds,so this endpoint stays the repair path for a corrupt spec.
test_a_plain_client_entry_the_client_adds_is_still_persistedandtest_a_namespaced_edition_extra_the_client_resubmits_is_left_alone-- scopeguards.
Mutation-verified against base: the five assertions of new behaviour all fail on
mainwith the intended messages, and the three scope guards pass on base as theymust. Full file 44/44 green, and 110/110 across the three test files that exercise
this endpoint (
test_agent_config_merge_on_write.py,test_api_agent_config_put_succeeds.py,test_json_object_body_guard.py).test_app_owned_entry_present_in_the_snapshot_is_updatedis green and unmodified,which is the mechanical proof that the kept contract was not reversed. flake8,
isort, black and mypy clean on the touched files.
docs/system-specs/modules/app-kit-platform.mdgains section 1a with thepresent-axis decision table, so the spec does not disagree with the code.
Any other suggestions on the work?
and the submission carries a superseded definition of it (an older port or
command), the submitted row wins and reverts a correction the registration path
had made. Closing that reverses the twice-kept editor-snapshot-wins contract, so
it needs a maintainer ruling rather than a fix decided in review. It is also
materially less severe: unlike resurrection it self-heals, because an enabled
app's bridge is re-registered with its live port on the next gateway start.
halves at once by version/etag-ing the snapshot and having the editor reload on a
stale write. That is a new API contract plus a client reload path -- feature work,
and not something to fold into a defect fix. Carried in agent-config PUT: a stale snapshot can revert a live app bridge to a superseded definition #7470 alongside the
substitution shape, so the two can be ruled on together.
editor can show "2 entries were not saved" would need
_commit_agent_configtoreturn more than its current
changedflag, whose single-return shape isload-bearing in that function's documented contract; worth doing separately if
the UX matters.
Pattern harvest
Rule candidate: review-prompt
Pattern: a whole-file PUT classifies the shared region in only ONE direction, so
the mirror direction persists a stale snapshot verbatim
This is the second defect in one span from the same root: a PUT that persists a
client snapshot over a file the gateway also writes. #6664 was the absent
direction, #7089 the present direction, and both were found by review rather than
by a gate. The mechanical form: for any endpoint accepting a whole-file snapshot
of a file with more than one writer, require the handler to classify every key of
the shared region in BOTH directions -- present in the submission and absent from
it -- and pin the classification as a table. The absent half alone reads as
complete and is not, which is exactly how this axis survived #6975's review.
A cheaper enforcement while that is unbuilt: a ratchet test asserting that the
region a non-client writer owns (here
mcpServerskeys containing:) is decidedby on-disk state on both axes, so adding a new owned region without a
present-axis rule fails the suite instead of shipping.
Closes #7089