Repository navigation
security(search): a field-narrowed search still matches through the name field's pinyin companion #21880
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsPath: business objects and records — searchable, with consistent permissions | search.field-scoped-narrowing | P2
Triage: first grade —
bug·security·priority:p3·domain:engine·area:access·pm:queue(findingremoved). The companion clause follows the effective search-field setTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-05T13:51Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes and positions only. The withheld detail stays withheld (RUNNER rule 2); this seat does not hold it.Triage: lands in the engine's search expansion (not
searchAll) ⇒domain:engine; rationale: the narrowing to queryable fields is applied before the expansion adds the companion clause, so the clause escapes it.- Direction (no boundary moves, so not a decision card). The engine drops the companion clause whenever the field it mirrors is outside the effective search-field set. That is the card's first option: one gate, the set the narrowing already computed. ⛔ No second eligibility rule for the companion.
- Why p3. The reviewer graded it low. It applies only with the optional companion enabled, and it predates 17.6.0.
- Also owed (public tests), as the card lists: a row-scoped object where the member sees only their own matching row, and a term present only in a field hidden from the member, which yields no hit.
- Serial: PR fix(metadata-protocol): global search skips objects and fields the caller cannot read #21879 (search: a member's global search answers 403 for the whole request when its scope includes any object the member cannot read — the console shows "No results found" #21836), on the same search path.
- The withheld detail goes to the claiming seat from
session_018zT8d8NpiQ1ExhuNd5TxY6on the maintainer's word.
Generated by Claude Code
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsand removed
on Oct 5, 2026 objectstack-fleet commented
on Oct 5, 2026 ContributorAuthorMore actionsClaim: PM loop round 38 · 2026-10-05T23:54Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21880-search-companion-scope
Worktree:objectstack-issue-21880
Domain:domain:engine
Seat:domain:engine#1
Provenance: the seat asked the maintainer in this seat's session (2026-10-05) whether to take this card, which had stayed inpm:queuewith no claim since triage. The maintainer answered, verbatim: 「并发3」. This lane runs three concurrent claims, and this card is the only eligiblepm:queuecard. The withheld detail (RUNNER rule 2, held insession_018zT8d8NpiQ1ExhuNd5TxY6) is not requested and not needed: triage's direction (5995863103) is public and names the fix. The PR describes the fix only in the terms already public on this card.
File surface (atorigin/maindcb11c2ec9), per triage's grade and direction 5995863103:packages/objectql/src/search-filter.ts,expandSearchToFilter's companion clause (about:180–:185). The engine drops the companion clause whenever the field it mirrors is outside the effective search-field set. That is one gate: the setresolveSearchFieldsalready computed. The mapping from companion to source fields comes fromsearch-companion.ts(resolveSearchCompanionSources), touched only if the mapping must be read there.- ⛔ No second eligibility rule for the companion.
- ⛔ Not in
searchAll.
- Pins, as triage lists them:
- a unit pin in
objectql: a field-narrowed search does not match through the companion of a field outside the set; - public dogfood cases (declared on [PM seat] domain:cli — 🟢 os-elon-musk · session_01BmsuLyUeuG5CNpZFMH1jzS #6024): a row-scoped object, where the member sees only their own matching row; and a term present only in a field hidden from the member, which yields no hit.
- a unit pin in
.changeset/21880-*.md(@objectstack/objectqlpatch).
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: no- No contract accept set or export moves. The search no longer matches through a column the caller's search-field set excludes, the same narrowing fix(metadata-protocol): global search skips objects and fields the caller cannot read #21879 applied to the source fields. Its sibling search: a member's global search answers 403 for the whole request when its scope includes any object the member cannot read — the console shows "No results found" #21836 shipped
Clause-②: noatpatch.
Thread-read: 5995863103
Serial constraints cleared: at 2026-10-05T23:54Z: - Triage's serial PR fix(metadata-protocol): global search skips objects and fields the caller cannot read #21879 (search: a member's global search answers 403 for the whole request when its scope includes any object the member cannot read — the console shows "No results found" #21836) landed as
87712ab823. - Open PRs, each file list read by
filename, paged: none touchessearch-filter.ts,search-companion.ts,plugin-pinyin-searchsource or a search dogfood test. This lane's own PR fix(objectql): the cascade skips a federated object's injected tenant anchor #21917 touchesengine.ts's cascade scan only. The release PR chore: version packages #21352 touchesplugin-pinyin-search's CHANGELOG and version line only. - This lane's other claims: finding(objectql): deleting an organization answers 500 when a federated object is provisioned, because the cascade scan probes the remote table on the platform-injected organization_id #21910 (PR fix(objectql): the cascade skips a federated object's injected tenant anchor #21917,
engine.ts) and [finding] The metadata door saves an edit to a code-defined datasource (PUT /api/v1/meta/datasource/:nameanswers 200) and the metadata read then serves it, while the datasource admin door refuses the same edit as read-only #21899 (metadata-protocol's datasource write path).
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21880,
"status": "done",
"branch": "claude/issue-21880-search-companion-scope",
"pr": "#21930",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi — the PM's session; this dev ran as its subagent (mode:subagent), identity = the branch",
"premise_still_valid": true,
"summary": "The search expansion (expandSearchToFilter) adds the pinyin companion clause only when every field the companion mirrors is inside searchFields, the effective set resolveSearchFields already computed. The mirrored fields come from resolveSearchCompanionSources, which the registry provisions the companion from and plugin-pinyin-search fills it from. There is one gate, no second eligibility rule, searchAll is not touched, and no spec file changed. With that, a field-narrowed search no longer matches through the companion of a field outside the search-field set. A search with no narrowing keeps the clause whenever the display/name field is in the object's searchable set, and a CJK term still skips it. Delivered: 10 unit pins in packages/objectql (a/b/c), the two public dogfood cases plus controls, and an @objectstack/objectql patch changeset with Clause-②: no. Draft PR #21930 is assigned to os-project-manager.",
"h2_companion_form_and_gate": "Measured at dcb11c2 and unchanged at 40618fa: ONE __search column per object. It holds the normalized form of ONE source field, the resolved display/name field. resolveSearchCompanionSources returns [display] or []; provisionSearchCompanion declares no column on []; the populate hook and the backfill read the same function. Gate: keep the clause iff every entry of resolveSearchCompanionSources({ nameField: opts.displayField, fields: opts.fields }) is in searchFields. The engine passes displayField = nameField ?? displayNameField, the same pointer resolveDisplayField reads, so this is the companion's real source. The test is every-not-some because one shared column matches through all its sources at once; today that reduces to 'the display/name field is in searchFields'. An empty list passes vacuously, which is reachable only for an author-declared __search the platform does not fill. H1 confirmed: at dcb11c2 the clause (search-filter.ts:180-185) keyed only on the column's presence. H3 held: one gate, no searchAll change, and an un-narrowed search keeps the clause (unit (b)).",
"tests": "All at final head 40618fa (origin/main faf8dce merged), under os-verify-lock. (1) pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2: Test Files 375 passed (375), Tests 7479 passed (7479), OBJECTQL_TEST_EXIT=0. (2) pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=1 test/search-companion-field-scope.dogfood.test.ts test/search-skip-unreadable.dogfood.test.ts: Test Files 2 passed, Tests 11 passed. (3) pnpm --filter @objectstack/objectql run typecheck: exit 0, including check:test-typecheck over tsconfig.test.json. pnpm --filter @objectstack/dogfood run typecheck: exit 0, after a full turbo build. --listFilesOnly counts 1 for each new test file in its program. (4) Reverse verification on committed HEAD, done twice (e7b2a3c, then 40618fa) with identical readings. node scripts/ablation-replace.mjs replaced the gate with a constant-true guard carrying marker __ABLATED_21880: anchor 1 to 0, blob 5a2afee37090 to ce8eb93139c8, wrap mode with a restore trap. Then pnpm --filter @objectstack/objectql build. ablation-dist-preflight: marker present in 4 built files. Unit src/search-companion.test.ts: 5 failed / 34 passed; the 5 are exactly the (a) cases. Dogfood file: 2 failed / 5 passed; the 2 are exactly the two hidden-field no-hit cases, and the row-scoped case and all controls stayed green. Direction: red, as expected. Restore: blob == HEAD 5a2afee37090, git diff HEAD empty. objectql rebuilt; ablation-dist-preflight --absent: marker absent from all 14 built files, working tree clean. (5) Lint narrowed to the 4 changed .ts files: eslint --no-inline-config --format json gives 4 files, 0 errors, 0 warnings. eslint --print-config resolves a config for each (in population, none ignored). eslint.config.mjs has no parserOptions.project (type-aware linting off), so the verdict on untouched files cannot move. Whole-tree pnpm lint is CI's run.",
"gates": "At 40618fa. node scripts/pm/dispatch-gates.mjs --commands (no paths) derived 78 commands. All ran, and --ran reconciled: '78 derived famil(ies) accounted for — 78 run, 0 NOT-MEASURED', all exit 0. check:dual-build-cjs-loads measured after a full turbo build; the earlier sweep gave PREREQUISITE NOT MET. 10 families were added versus the dispatch-time list and all exited 0: check-changeset-fixed, check-osv-exemptions (+--self-test), spec check:llms-txt, check:manifest-repository-directory, check:merge-driver, check:override-consistency, check:turbo-task-graph, check:vendor-export-contract-resolve, check:workspace-manifest-cycles. Artifact-roster block, 53 commands: 50 exit 0 in the sweep. The other 3 were NOT WIRED (exit 2) without PR context, then each ran with it, all exit 0: check-partof-closing-keyword with PR_BODY = the PR body; check-closing-target-claim and check-single-claim-paths with PR_NUMBER=21930. Symbol-anchor sweeps: check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors, all exit 0. check:test-source-alias went red once in an earlier sweep (new unaliased import @objectstack/plugin-pinyin-search in dogfood); the anchored source alias it prescribed fixed it, and it is green at the final head. NOT MEASURED locally, declared to CI as dispatch-gates prints them: 6 path-scheduled CI jobs, 4 type-check lanes, 6 workflow-valued families, 11 wide-population families. CI on #21930: not read; convergence is the PM's.",
"line_budget": "n/a",
"files_changed": [
".changeset/21880-search-companion-field-scope.md",
"packages/objectql/src/search-filter.ts",
"packages/objectql/src/search-companion.test.ts",
"packages/qa/dogfood/test/search-companion-field-scope.dogfood.test.ts",
"packages/qa/dogfood/package.json",
"packages/qa/dogfood/vitest.config.ts",
"pnpm-lock.yaml"
],
"deviations": [
"Branch probe git push -u origin claude/issue-21880-search-companion-scope: attempts 1-3 answered '! [remote rejected] claude/issue-21880-search-companion-scope -> claude/issue-21880-search-companion-scope (Internal Server Error)'. That is a server error, not a 403. I retried with backoff, and attempt 4 succeeded before any edit.",
"Refused by this session's permission checks, verbatim: 'Permission for this action was denied by the Claude Code auto mode classifier. Reason: [Exfil Scouting].' The command was curl -sS "$HTTPS_PROXY/__agentproxy/status" plus reading /root/.ccr/README.md, to diagnose the push 500. I did not retry it or route around it.",
"File surface beyond the claim's list. packages/qa/dogfood/package.json and pnpm-lock.yaml add the workspace dependency @objectstack/plugin-pinyin-search; the dogfood boot mounts the real PinyinSearchPlugin. packages/qa/dogfood/vitest.config.ts gains one anchored source alias, which check:test-source-alias demands (it went red without it). search-companion.ts is untouched, and the unit pins live in the existing search-companion.test.ts.",
"One PR body line is now stale: 'The other two are declared to CI'. Both later ran with PR_NUMBER=21930 and exited 0. Per the write-once rule I did not PATCH the body. Seat edit if wanted: replace that sentence with 'check-closing-target-claim and check-single-claim-paths ran after the PR opened, with PR_NUMBER=21930: both exit 0.'",
"Conflict noted, not resolved silently. The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. AGENTS.md wins: commits carry the model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends in the session-URL footer.",
"origin/main was merged twice (3dbd084, then faf8dce) before opening the PR. Every test, typecheck, gate, roster and anchor reading above was re-run on the final head 40618fa, and the reverse verification was repeated there.",
"Cleanup is done. rm -rf the worktree's node_modules, then git worktree remove /home/user/objectstack-issue-21880: exit 0, without --force, after confirming the tree was clean and the remote head equalled 40618fa. No server or background process is left running."
],
"mcp_calls": "0 — no MCP GitHub tool was called. Reads went through gh api REST GETs: issue, comments, PR read-back.",
"api_writes": "3 relay strokes, each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches, 204) executed as objectstack-fleet[bot]. (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, opening draft #21930; read-back 7777 bytes sent, 7777 stored, identical. (2) label-write --assign os-project-manager, POST /repos//issues/21930/assignees; read-back MATCHES, and no label was written. (3) This os-dev-report comment, POST /repos//issues/21880/comments. Plus git push: 9 attempts, 3 rejected with 500, 6 landed. Pushes are not REST writes.",
"open_questions": [
{
"question": "Non-blocking boundary. An author-declared __search column whose source resolveSearchCompanionSources cannot name passes the gate vacuously, so its clause is kept even under narrowing. Should that case instead fail closed?",
"options": [
"A: keep the vacuous pass, as shipped. That is the literal ruling (drop when a mirrored field is outside the set), and an un-narrowed search keeps the clause everywhere",
"B: require a non-empty source list. That fails closed for a source-less author column, but it also drops the clause on that object's un-narrowed searches"
],
"recommendation": "A. The platform never provisions a companion without a source. An author-declared __search is an ordinary field under its own field-level rules, and B would change recall for un-narrowed searches with no measured user."
}
],
"out_of_scope_findings": [
"carrier: PR #21930 Acceptance notes · noted, not filed — an un-narrowed search on an object whose effective search set omits its display/name field no longer gets the clause. Examples: a declared searchableFields without it, or an html/richtext display field. Measured over examples/ at dcb11c2: 0 objects change (showcase_account's declared set includes name; todo_task's nameField subject is text). Derived html/richtext display fields: NOT MEASURED."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded: PR #21930 →
0728cbf192onmain. It merged through the merge queue at 2026-10-06T04:31Z; it entered the queue at 2026-10-06T03:54Z. Verified at 2026-10-06T04:31Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash. It is on
origin/mainas a single-parent commit. Its diffstat is the reviewed one at211df5615d: 7 files, +449/-3. - What is on
main.objectql'ssearch-filter.tsadds the pinyin companion clause only when every field it mirrors is inside the effective search-field set (companionWithinSearchFields). An unnarrowed search keeps its companion matches. Public dogfood cases pin both directions. - The card.
Fixes #21880closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body. - From this release (
@objectstack/objectqlpatch,Clause-②: no): a search narrowed bysearchFieldsno longer matches a record through the pinyin companion of a field outside that set. - Landing path. One CI cycle waited on hourly full run: red on main (Lint & Type Check) #21924's fix (PR test(dogfood): each file's temporary cwd is created from a base the scratch-dir scan can read #21935) for the dogfood per-file cwd base; the branch merged
mainafter it landed. The non-required OSV red the PR inherited is filed as [finding] main's lockfile matches four advisories (proxy-addr 2.0.7 critical, source-map-js 1.2.1 high, sprintf-js 1.1.3 no fix, katex 0.16.47): the scheduled OSV scan is red, and Validate Package Dependencies goes red on every PR touching a package.json #21945. - Withheld detail. The withheld detail stays with the PM session that holds it. This card and its PR used only the public terms.
Generated by Claude Code
- The squash. It is on
Found by the independent code review of PR #21879, the fix for #21836. That PR narrows each swept object's search to the fields the caller may query. One search clause the engine adds is not governed by that narrowing.
session_018zT8d8NpiQ1ExhuNd5TxY6).searchFieldsnarrowing, so fix(metadata-protocol): global search skips objects and fields the caller cannot read #21879 adds no new capability. That PR's comment no longer claims more than this.searchAll.objectstack.Generated by Claude Code