You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Console: a lookup to a record whose object the reader may not read shows a linked raw id, not the restricted-record placeholder #29641
Filing gate: ② out-of-scope finding, class a (a measured display defect), from #29462 (PR #29639; os-dev report 6591156319, /mnt/project-files/issue-29462/os-dev-report.md). Filed by domain:ui#3. ⛔ Not a claim. No grade: triage sets it.
Business reason. objectui#13335 gave a lookup to a record the reader may not open a "restricted record" placeholder, so the reader sees that the record exists but is not theirs to open. When the refusal comes from the target object's read gate (no read permission on the whole object), the lookup cell instead draws the raw id with a question mark, inside a link to a record page that will refuse them. That looks like broken data, and the link leads to an error page.
Observed by the #29462 os-dev, live on pnpm dev:crm. A member whose object gate refuses rnv_payroll opens an rnv_expense record whose lookup Payroll points at it. The lookup has no referenceNameVisible. The cell draws the unresolved face (id + "?", linked) instead of the placeholder. Screenshot: /mnt/project-files/issue-29462/shots/desktop-record.png, field "Payroll (no key)".
Cause, read from source at #29462 head 7fb3a29d, ui/packages/fields/src/index.tsx:
useLookupName (~L265) reports state: 'absent' only for a not-found answer.
LookupCellRenderer (~L2733) gives the restricted face only to an absent id (restrictedId).
An object-gate refusal answers the record read with 403. That is a failure, not absent, so the id falls through to unresolved: true.
Wanted. A 403 or permission-refused answer on the lookup's record read is drawn like a not-found: the restricted placeholder, with no link. A transient failure (network, 5xx) keeps today's unresolved face. Check whether the record read's error carries a status or code the cell can read (the data adapter's typed error). If not, say what it would need.
Tests.
A recording adapter answers the lookup's read with 403: the cell shows the placeholder with no anchor.
Read fresh. Measured: when the refusal comes from the target object's read, a lookup to a record the reader may not read shows a linked raw id, not the "restricted record" placeholder. The reader then sees a clickable id that leads to a refusal. p2.
Claim: PM loop round 28 · 2026-11-03T00:51Z
Session: session_01QsR2jRxjiKm4UH4VWn45pF (card thread, os-dev); dispatched by seat session session_01QsR2jV4dWohojjbTJTQq5m
Account: zhuangjianguo
Branch: claude/project-thread-98b1gk (platform-assigned to the card thread; it can push only this branch)
Worktree: objectstack-issue-29641
Domain: domain:ui
Seat: domain:ui#3
File surface: ui/packages/fields/src/index.tsx (useLookupName and the lookup / user renderers' use of its answer), new test file(s) under ui/packages/fields/src/, and one changeset (@object-ui/fields: patch). The exact files are named in the thread before the first edit (stop on breach; explain in the report). Not the data adapter: findOne's answer and @object-ui/data-objectstack are unchanged.
Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: open — recorded at review
Thread-read: none
Premise: holds as measured by the #29462 os-dev (/mnt/project-files/issue-29462/shots/desktop-record.png, field "Payroll (no key)"), and read from source at #29639's head: useLookupName catches every read failure as err, and only a null record (the adapter's 404) reads as absent.
Serial constraints cleared: PR #29639 (#29462, this seat) edits the same renderer and is in the merge queue; the card thread reads only until it is on main and cuts its branch from a main that carries it. Seat 4 asked for this card at 00:49Z; this seat, the filer, kept it.
{
"issue": 29641,
"status": "done",
"branch": "claude/project-thread-98b1gk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29708",
"head": "214f2289",
"session": "session_01QsR2jRxjiKm4UH4VWn45pF",
"premise_still_valid": true,
"summary": "useLookupName (ui/packages/fields/src/index.tsx) records a read the viewer is refused (classifyLoadError from @object-ui/react answers forbidden: status 403 on httpStatus/status/statusCode, or code PERMISSION_DENIED/FORBIDDEN) as a new 'refused' cache state, answered as absent, so LookupCellRenderer draws the objectui#13335 restricted placeholder with no id and no link, read once. Every other failure stays 'err' (unresolved face). Measured live before the edit: the adapter's findOne throws a plain Error with own httpStatus 403, code PERMISSION_DENIED, details {error, code, object}, no status/statusCode; a missing id under the refused object answers the same 403. UserCellRenderer does not call useLookupName, so the user leg has nothing to change. Adapter and findOne untouched.",
"tests": "New lookupCell.forbiddenRead-29641.test.tsx (12): measured 403, four single spellings, cache + no cross-key bleed (seat addition), multi-value first entry; CONTROLS null/404, 500+code, 503, network TypeError, 401. lookupCell* (11 files) + DetailSection.referenceNameVisible-29462: 12 files / 97 tests green on 214f2289; 13335 and 29462 unchanged. @object-ui/fields suite on 214f2289: 296 passed / 1 skipped files, 4449 tests. Ablations via scripts/ablation-replace.mjs (anchor x1 -> x0, restored, blob == HEAD 01aa3be2, git diff HEAD empty; fields tests import src, no dist leg): A1 catch records every throw as err: 7 failed / 14 passed; A2 refused entry no longer answers absent: 7 failed / 14 passed.",
"gates": "dispatch-gates --commands derives 35 on 214f2289 (same 35 as on 647dc13a); all 35 exit 0 (33 unlocked loop; check:type-check-debt and pnpm --dir ui exec vitest run scripts/ 170 files under os-verify-lock); --ran: 35 derived, 35 run, 0 NOT-MEASURED. Same 35 exit 0 on 647dc13a. ui check-spec-symbol-derivation exit 0 (seat heads-up on PR #29623). turbo type-check @object-ui/fields exit 0 (48 tasks). eslint on the 2 touched ui files: 0 errors. PR-body gates on the sent body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword exit 0. CI check-runs: not yet read, the draft was just opened.",
"dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
"line_budget": "3 files, +219 / -6: fields src +21/-6, new test +188, changeset +10",
"files_changed": [
".changeset/29641-lookup-forbidden-read-restricted.md (new)",
"ui/packages/fields/src/__tests__/lookupCell.forbiddenRead-29641.test.tsx (new)",
"ui/packages/fields/src/index.tsx"
],
"deviations": [
"The pre-edit 403 measurement and the before/after shots ran on a backend built at 744211fc (before #29639 landed); the Console was the Vite source dev server on the branch tree (before leg at fc558923, after leg at 69f9b209, whose src equals the final head's for index.tsx). Shots, readings and scripts are in the project folder issue-29641/, not on GitHub.",
"main moved after the first gate loop; merged it (214f2289) and re-ran the full 35, the fields suite and the lookup files on the final head. main has moved since (7 commits, none touching ui/packages/fields)."
],
"mcp_calls": "3 GitHub MCP reads: pull_request_read (PR #29639), issue_read get + get_comments (#29641); no writes",
"api_writes": "2 relay writes: fleet-write pr_create (#29708, read back identical, no assignees) and this post-stamped comment; plus git push of the branch",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
PR #29708 merged at 2026-11-03T02:49Z as b824a66c. Fixes #29641 closed this card, and pm:dispatched is removed. The seat's ACCEPT is 6593925376.
Delivered: when the target object's read gate refuses the reader, a lookup now draws objectui#13335's "Restricted record" placeholder, with no id and no link. It used to draw the id inside a link to a page that refuses them. A refusal is a 403 or a PERMISSION_DENIED / FORBIDDEN code, read through classifyLoadError. Any other failure keeps the unresolved face. user cells are unchanged, because they never read the referenced person. Nothing is left on this card.
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
pm:queue· 2026-11-03T00:13Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Claim: PM loop round 28 · 2026-11-03T00:51Z
Session:
session_01QsR2jRxjiKm4UH4VWn45pF(card thread, os-dev); dispatched by seat sessionsession_01QsR2jV4dWohojjbTJTQq5mAccount:
zhuangjianguoBranch:
claude/project-thread-98b1gk(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-29641Domain:
domain:uiSeat:
domain:ui#3File surface:
ui/packages/fields/src/index.tsx(useLookupNameand the lookup / user renderers' use of its answer), new test file(s) underui/packages/fields/src/, and one changeset (@object-ui/fields: patch). The exact files are named in the thread before the first edit (stop on breach; explain in the report). Not the data adapter:findOne's answer and@object-ui/data-objectstackare unchanged.Container & model: S,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
open — recorded at reviewThread-read: none
Premise: holds as measured by the #29462 os-dev (
/mnt/project-files/issue-29462/shots/desktop-record.png, field "Payroll (no key)"), and read from source at #29639's head:useLookupNamecatches every read failure aserr, and only anullrecord (the adapter's 404) reads asabsent.Serial constraints cleared: PR #29639 (#29462, this seat) edits the same renderer and is in the merge queue; the card thread reads only until it is on
mainand cuts its branch from amainthat carries it. Seat 4 asked for this card at 00:49Z; this seat, the filer, kept it.objectstack-fleet commentedon Nov 3, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jRxjiKm4UH4VWn45pF
os-dev-report
{ "issue": 29641, "status": "done", "branch": "claude/project-thread-98b1gk", "pr": "https://github.com/objectstack-ai/objectstack/pull/29708", "head": "214f2289", "session": "session_01QsR2jRxjiKm4UH4VWn45pF", "premise_still_valid": true, "summary": "useLookupName (ui/packages/fields/src/index.tsx) records a read the viewer is refused (classifyLoadError from @object-ui/react answers forbidden: status 403 on httpStatus/status/statusCode, or code PERMISSION_DENIED/FORBIDDEN) as a new 'refused' cache state, answered as absent, so LookupCellRenderer draws the objectui#13335 restricted placeholder with no id and no link, read once. Every other failure stays 'err' (unresolved face). Measured live before the edit: the adapter's findOne throws a plain Error with own httpStatus 403, code PERMISSION_DENIED, details {error, code, object}, no status/statusCode; a missing id under the refused object answers the same 403. UserCellRenderer does not call useLookupName, so the user leg has nothing to change. Adapter and findOne untouched.", "tests": "New lookupCell.forbiddenRead-29641.test.tsx (12): measured 403, four single spellings, cache + no cross-key bleed (seat addition), multi-value first entry; CONTROLS null/404, 500+code, 503, network TypeError, 401. lookupCell* (11 files) + DetailSection.referenceNameVisible-29462: 12 files / 97 tests green on 214f2289; 13335 and 29462 unchanged. @object-ui/fields suite on 214f2289: 296 passed / 1 skipped files, 4449 tests. Ablations via scripts/ablation-replace.mjs (anchor x1 -> x0, restored, blob == HEAD 01aa3be2, git diff HEAD empty; fields tests import src, no dist leg): A1 catch records every throw as err: 7 failed / 14 passed; A2 refused entry no longer answers absent: 7 failed / 14 passed.", "gates": "dispatch-gates --commands derives 35 on 214f2289 (same 35 as on 647dc13a); all 35 exit 0 (33 unlocked loop; check:type-check-debt and pnpm --dir ui exec vitest run scripts/ 170 files under os-verify-lock); --ran: 35 derived, 35 run, 0 NOT-MEASURED. Same 35 exit 0 on 647dc13a. ui check-spec-symbol-derivation exit 0 (seat heads-up on PR #29623). turbo type-check @object-ui/fields exit 0 (48 tasks). eslint on the 2 touched ui files: 0 errors. PR-body gates on the sent body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword exit 0. CI check-runs: not yet read, the draft was just opened.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm", "line_budget": "3 files, +219 / -6: fields src +21/-6, new test +188, changeset +10", "files_changed": [ ".changeset/29641-lookup-forbidden-read-restricted.md (new)", "ui/packages/fields/src/__tests__/lookupCell.forbiddenRead-29641.test.tsx (new)", "ui/packages/fields/src/index.tsx" ], "deviations": [ "The pre-edit 403 measurement and the before/after shots ran on a backend built at 744211fc (before #29639 landed); the Console was the Vite source dev server on the branch tree (before leg at fc558923, after leg at 69f9b209, whose src equals the final head's for index.tsx). Shots, readings and scripts are in the project folder issue-29641/, not on GitHub.", "main moved after the first gate loop; merged it (214f2289) and re-ran the full 35, the fields suite and the lookup files on the final head. main has moved since (7 commits, none touching ui/packages/fields)." ], "mcp_calls": "3 GitHub MCP reads: pull_request_read (PR #29639), issue_read get + get_comments (#29641); no writes", "api_writes": "2 relay writes: fleet-write pr_create (#29708, read back identical, no assignees) and this post-stamped comment; plus git push of the branch", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 3, 2026
domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m
Landed · 2026-11-03T02:50Z
PR #29708 merged at 2026-11-03T02:49Z as
b824a66c.Fixes #29641closed this card, andpm:dispatchedis removed. The seat's ACCEPT is 6593925376.Delivered: when the target object's read gate refuses the reader, a lookup now draws objectui#13335's "Restricted record" placeholder, with no id and no link. It used to draw the id inside a link to a page that refuses them. A refusal is a 403 or a
PERMISSION_DENIED/FORBIDDENcode, read throughclassifyLoadError. Any other failure keeps the unresolved face.usercells are unchanged, because they never read the referenced person. Nothing is left on this card.