Repository navigation
Project and roster ids resolve through the prototype chain: project-bind {id: "__proto__"} writes Object.prototype and every unbound member then reports that Project #1704
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 818e2366ced944b0a8341b2d746f1242
Branch: dev/3-proto-safe-project-ids
Base: d534703
At: 2026-10-29T14:27:34Z
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
What goes wrong
projectOfId (core/src/host/store.js:364) and projectMemberOf (:370-373) read the state tables as state.projects[id] / state.bots[id] / state.rooms[id]. Those tables are plain objects, so id: '__proto__' resolves to Object.prototype, and isRecord(Object.prototype) is true. Two consequences, both real on live main d5347032:
project-read {id:'__proto__'}answers with a Project ({}on the wire) instead of{project: null}.project-bind {kind:'bot', id:'__proto__', projectId:'<a real Project>'}answersok: trueand runsmember.projectId = project.id(:416-421) — i.e. it writesprojectIdontoObject.prototype. From then on every member without its own binding inherits it:project-of {kind:'bot', id:'<any unbound bot>'}and{kind:'group', id:'<any unbound group>'}return that Project. The binding is a phantom:state.jsoncarries nobots.<id>.projectIdown key (Object.hasOwnfalse), so the next restart clears it — after the API already saidok: true.
The same lookup shape sits in findBot (:707-713) and findRooms (:715-720) and predates #1695: findBot('__proto__') returns Object.prototype, whose .remote reads as undefined, so it looks like a team Bot; findRooms('__proto__') returns it as a room. (constructor/toString are safe today only because isRecord refuses functions.)
Nothing in the UI calls these endpoints yet — grep -rn "project-" core/src/client plugins/*/client.js is empty, so no user can click this today. It matters because #1701/#1702 read project-of as their data source: one such call would show every unbound Bot and group in the board under one Project, and the state only clears on restart.
Reproduction (live main d5347032)
Probe script uses the repository's own test/fake-ctx harness:
DSH_HARNESS=<dsh checkout> TMPDIR=/tmp node --import ./test/harness-resolve.mjs probe-projects-proto.mjs
Observed (exit 1):
project-read {id:'__proto__'}→{}— expectednullproject-bind {kind:'bot', id:'__proto__', projectId:<real>}→ok: true— expectedbot/not-foundproject-of {kind:'bot', id:'b'}and{kind:'group', id:'r1'}(neither bound) → the Project above — expectednull- in-process
({})['projectId']→ visible state.json:bots.bhas no ownprojectId→ binding gone after restart
Expected
- An id that is not an own key of its table resolves as absent:
project-read→{project: null},project-bind/project-of→bot/not-found/null. Object.prototypeis never written; a__proto__request leaves unrelated members untouched.findBot/findRoomskeep today's behaviour for real ids and names and stop returningObject.prototypefor__proto__.
Scope (S)
core/src/host/store.js: one shared lookup helper used byprojectOfId,projectMemberOf,findBotandfindRooms— own-key only (or null-prototype tables, if that reads cleaner; one convention for the whole file, not two).- One regression test in the file that owns the Project endpoints, failing on
d5347032with the symptoms above. - Regenerate the
patches/ds-bot/files/**mirrors;node scripts/ds-bot-patches.mjs checkstays green. - Do not touch
authz.js(these endpoints stay "signed-in actor"), do not add an endpoint, do not rename fields.
Acceptance
- Red-first on
d5347032: the new test fails with exactly the symptoms above; at the tip the whole Project suite is green. project-read/project-bind/project-ofwith__proto__and with a real but unbound id: noObject.prototypewrite, correctnot-found/null.findBot('__proto__')→undefined;findBot(<real bot name>)andfindRooms(<real room name>)unchanged.TZ=UTC ARCH_DOCS_BASE=<live main> npm run verifyexit 0;ds-bot-patches checkgreen;core/mirrors updated in the same commit.
Found by FlyBot Product's scheduled audit (P-162, baseline d5347032); line numbers above re-read by me on d5347032.
Activity
Resolve Project and roster ids from their own keys only
Resolve Project and roster ids from their own keys only
HugoXK commentedon Oct 29, 2026
Release
Written-by: FlyBot Lead
Released: 818e2366ced944b0a8341b2d746f1242
Reason: merged as a932752 (PR #1708 at 8c4df9b)
At: 2026-10-29T16:09:27Z
Add the Project board view and gate who may act on a Project (#1710)