fix(install): AgentInstallation is canonical for displayName (TASK-032) - #1503
Conversation
Sam ruled TASK-032's fork on 2026-09-02: the installation owns a display name; `User.botMetadata.displayName` is a seed for an installation that has none, never a preference over one that does. This is the code half — the schema doc is #1097. Two sites in `routes/registry/install.ts` preferred the curated User row: - `effectiveDisplayName` never consulted AgentInstallation at all, so a reinstall re-seeded the per-pod label from the shared User row and a name curated for THIS pod silently reverted. Precedence is now explicit caller > this pod's existing installation > User-row seed > registry default. - the intro-post label re-queried `User.botMetadata` and put it ahead of `installation.displayName`. That query is dropped: the installation row was resolved through the full chain a few lines above, so the second lookup could only disagree with the row just written. Task #62 / PR #408 is not reinstated. The User-row seed still beats the registry default, so installing an existing identity into a NEW pod — where no installation exists — still writes "Aria" rather than the manifest default to both AgentInstallation.displayName and AgentProfile.name. Only the both-are-curated case changes, and there the per-pod value wins, which is what the read paths (agentMessageService:1461, dmService:555) already do. The reachable shape is uninstall-then-reinstall, not install-twice: an active row 400s at the route, so a second install meets an existing row only on the reactivation path, where ADR-001 §3 identity continuity preserves it. The new test covers exactly that and fails against main's install.ts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CodeQL flags the re-install lookup at `install.ts` because `podId` reaches the query straight from the request body: a JSON object there would be interpreted as a query operator rather than a value. The route already 404s on `Pod.findById(podId)` above, so this was not reachable in practice — but the guard is upstream and invisible at the query, and the same file already spells the safe form at `pod: String(podId)`. Coercing at the query makes the operator shape unconstructable regardless of what the upstream guard does later, which is the cheaper invariant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ion-canonical-displayname
|
Head moved:
The gated behaviour is unchanged: Worth knowing about the green this replaces: all 12 rows on |
@sprint-review ran the CodeQL alert rather than reading it and the
false-positive premise it was dismissed on does not hold. The premise was
that `Pod.findById(podId)` + 404 above already rejects anything that is
not a real id. Mongoose casts an operator object in a filter position
rather than throwing, so `Pod.findById({ $ne: null })` matches the first
pod in the collection and the 404 never fires.
That matters more than the one line the alert names, because `podId`
reaches four Mongoose filters on this route unchanged — `Pod.findById`,
the already-installed `AgentInstallation.findOne`, the installation
displayName lookup, and the `AgentProfile.findOneAndUpdate` upsert KEY.
Coercing at one query left the other three.
Guard at the entry point instead, in the shape `agentName` already uses
directly above. `undefined`/`null` still fall through to the existing 404
and the self-serve 400, so no legitimate path changes status code. The
`String(podId)` from the previous commit stays: it is the form CodeQL
recognises as a barrier at the site it flagged.
The new suite carries the premise as an explicit positive control, so a
later refactor cannot turn it green for the wrong reason. Without the
guard the operator object gets past the 404 and the request 500s, so the
observable behaviour today is a crash rather than a cross-pod write.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Head moved: I reproduced their result rather than taking it: The widening is what changes the fix.
So Severity, stated against the fix rather than for it: without the guard the request 500s, it does not silently install into a matched pod. The mutation run is in the suite's history — remove the guard and only the 400 case fails, Tests: 13/13 across all three |
lilyshen0722
left a comment
There was a problem hiding this comment.
SR-GATE: APPROVED @ f114864 — re-gate after the widened fix. My prior read was at d61a6c8e; base is current 702bc6388, merge state CLEAN.
Both new behaviours are pinned by tests that discriminate. Mutation-tested at this head against real DBs, baseline 8/8 on the two new suites:
install.ts:145guard neutered toif (false) { }— 1 red,install.podid-operator-injection.test.js:110,expected 400, received 500.:467effectiveDisplayName = existingInstallation.displayNamedeleted — 1 red,install.preserves-displayname.test.js:301,expected "Aria (Sprint Desk)", received "Aria".
Your severity statement at 62667 reproduces: unguarded, the request 500s. It does not install into a matched pod.
Two notes, neither blocking.
1. The comment at :136 undercounts. It says podId reaches four Mongoose filters and names them — Pod.findById at :154, the already-installed findOne at :262, the displayName lookup at :463, the AgentProfile upsert key at :528. Enumerating the whole file, raw podId also reaches AgentInstallation.install(agentName, podId, …) at :513 and a second Pod.findById(podId).select('type members') at :648. Six sites, not four. The entry guard covers all of them so nothing is wrong with the fix — but the count is the part a future reader will trust, and :463 is the one site that was already String()-coerced, so the list is off in both directions. Suggest "every Mongoose filter on this route" rather than a number.
2. The intro-post label is now uncovered. :669 const displayName: string = installation.displayName || agent.displayName replaces a lookup that existed to keep "Cuz 🦞" out of the intro post. I agree with the argument that it is safe by construction — installation.displayName was resolved through the full chain a few lines above. But mutating that line to bare agent.displayName leaves 15/15 green across install.preserves-displayname, install.podid-operator-injection and self-serve-install, and registry.install-intro-honesty.test.js stays 8/8 because it exercises the copy helper rather than the route. Nothing would catch a future edit that reintroduces the registry default there. A single assertion on the intro post's rendered name in the re-install case would pin it.
…count @sprint-review enumerated the route and found six Mongoose filters where the comment claimed four, plus one the comment miscategorised: the installation displayName lookup is already `String()`-coerced, so it was never a raw site. Following the helpers the number is larger again — `AgentInstallation.install` is a `findOne` AND a `create`, `ensureAgentInPod` is a third `Pod.findById`, and `postMessage` fans podId out further. So the count was the wrong instrument: it is a claim about how far the reader followed the value, with no natural stopping point, and it decays on the next refactor. The guard is at the entry point and returns before all of them, so the property that holds is positional. Both comment sites restated the four, so both are corrected. Comments only — no executable line changes. 8/8 across the two suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Delta Your correction reproduces, and it was wrong in two directions at once. The comment claimed podId reaches four filters unchanged, and one of the four — Following the helpers, six is not the ceiling either. So the count was the wrong instrument, and that is the repair rather than "six". Any number there is a claim about how far the reader followed the value, and there is no natural stopping point — I stopped at the route, you stopped at the route plus two helpers, and neither boundary is stated in the comment. The property that actually holds is positional: the guard returns before all of them. The comment now says that and names the fan-out sites without counting them. Corrected in both places that restated it — the route comment and the test file's header block, which carried the same "four Mongoose filters" sentence. A number corrected in one of two copies is not corrected. |
Code half of TASK-032. @sam ruled the fork on 2026-09-02 (card 62404, reply 62427): installation is canonical; install.ts stops preferring curated botMetadata. The schema doc is #1097 — this is the change that doc names as owed.
What the ruling means in code
User.botMetadata.displayNameis a seed for an installation that has no name — never a preference over one that does. Two sites inbackend/routes/registry/install.tshad it as a preference:effectiveDisplayNamenever consultedAgentInstallationat all. Precedence wasexplicit caller > User row > registry default, so a reinstall re-seeded the per-pod label from the shared User row and a name curated for this pod silently reverted. It is nowexplicit caller > this pod's existing installation > User-row seed > registry default. The installation term is the new one.The intro-post label re-queried
User.botMetadataand put it ahead ofinstallation.displayName. That query is dropped.installation.displayNamewas resolved through the full chain a few lines above, so the second lookup could only disagree with the row just written — and dropping it removes one User query per install.Task #62 / PR #408 is not reinstated
That regression (a registry default like "Cuz 🦞" landing on every member row, because the V2 member list reads
AgentProfilefirst) stays fixed. The User-row seed still beats the registry default, so installing an existing identity into a new pod — where no installation exists — still writes "Aria" to bothAgentInstallation.displayNameandAgentProfile.name. Only the case where both are curated changes, and there the per-pod value wins, which is what the read paths already do (agentMessageService:1461,dmService:555, each with a comment saying they prefer the installation precisely to stop a sibling pod's name leaking through the shared User row). The install path was the one surface disagreeing with them.The three existing tests in
install.preserves-displayname.test.jsare unchanged and green.The reachable shape is reactivation, not install-twice
An
activeinstallation makes the route 400 (Agent already installed in this pod), so a second install only meets an existing row on the uninstall → reinstall path, where ADR-001 §3 identity continuity preserves the row and its curated name. That is what the new test constructs; a naive install-twice test 400s and never reaches the branch.Verification
install.preserves-displayname.test.js— 5/5 green, including the new case.origin/main'sinstall.tsand passes against this one, so it discriminates rather than merely compiling (5 tests collected in both runs, not 0).install-cloud-gate,self-serve-install,registry.install-runtime-type,registry.install-intro-honesty,registry.get-installed-agent,registry.admin-installations.tsc --noEmitreports zero errors on the changed file.mongodb-memory-serverload locally).Merge note
#1322 also edits
backend/routes/registry/install.tsandgit merge-treereports a content conflict with this branch. Its hunks are the capability gate at@@ -381and one line at@@ -433, adjacent to theeffectiveDisplayNameblock this PR rewrites. Both are clean againstmainindependently — whichever presses second resolves. Flagging rather than sequencing: no order is required, the second author just needs to expect it.Not in scope
personaHireService.tsneeds no change. It never calls this route — it writesAgentInstallation.displayNamedirectly (:85) and passes the same string togetOrCreateAgentUser(:104) and the intro post (:126). That path already treats the installation as canonical. An earlier note of mine said the hire path was part of this ruling; it is true that it passesdisplayNameunconditionally and false that it bears on the fork.🤖 Generated with Claude Code