Repository navigation
lint(searchable-fields): report a searchableFields entry that names a secret or password field, which the engine now drops without a word (follow-up to #27477) #27705
Description
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Filing gate: ③ a maintainer-directed task. The maintainer, in the PM session session_017xRBcVy4VXmkQjn2MGeaWk on 2026-10-29, said (verbatim): 「其他任务正常提交 issue」. This card is a non-security follow-up to PR #27477 (merged), filed as a normal card.
Business reason: since #27477 the engine leaves every secret / password column out of the set search scans, including one an author names in the object's own searchableFields. That is the right runtime answer, but it is silent: os validate and the object publish door pass the declaration, and the author's searchable set is quietly narrower than written (and, when every entry is a credential field, quietly becomes the auto-default set). A build-time finding tells the author at the moment they write it, which is the same posture the linter already takes for a formula entry (#6674).
Reader: the dev who takes this card from the queue (the packages/lint search-fields rule, with a pin test beside it).
Dedupe: searched the open and closed issues (GitHub issue search scoped to objectstack-ai/objectstack) for "searchableFields secret password credential field warning" (7 hits) and "lint searchable-field-unsearchable secret password type in object searchableFields dropped silently" (27 hits). The nearest is #27660 (open), which teaches the rule in skills/objectstack-data; it is a docs card and owns no lint or runtime change. No card covers this one.
What to change
Where the entry is dropped (read at origin/main a75ddba1):
packages/objectql/src/engine.ts:13405–13419(expandSearchOnAst): the field map handed to the search expansion is built without every namecollectCredentialFields(schema)returns (packages/objectql/src/secret-fields.ts:289, typesecretorpassword), whilesearchableFieldsis passed through as declared.packages/spec/src/data/search-fields.ts:246(resolveSearchFieldResolution): the declared branch keeps only entries present in that field map, so a credential entry is filtered out by the existence check. When every declared entry is filtered out, the function falls back to the auto-default set (the degenerate case documented at:236).- Nothing reports it.
packages/lint/src/validate-searchable-fields.tschecks the object's own set for existence and, since A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674, for virtual (formula) entries (:63–70, the check at:414). It has no credential-type check, so asecret/passwordentry passes both.
The ask:
- In
packages/lint/src/validate-searchable-fields.ts, next to the virtual-entry check at:414, report asearchableFieldsentry whose field type issecretorpassword, on every surface that check covers (the object's own set, list views, and a react page's<ListView searchableFields>). The remedy line: drop the entry, credential fields are never searched. Reuse a shared predicate for "is this a credential type" rather than a second type literal, as the virtual check reusesisVirtualSearchField. - Choose the severity deliberately (error, as the virtual entry is, or warning) and say why in the rule's doc comment and its
os explainentry. - Pin it in
packages/lint/src/validate-searchable-fields.test.ts: one case each forsecretandpasswordon the object's own set and on a list view, plus a control (a text field in the same list passes).
Related: #27660 (the skill half of the same rule).
Out of scope: any runtime change. The engine's handling in expandSearchOnAst stays as it is.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 29, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: 错的必须被响亮拒绝并给处方 | none | P②
Triage: p2 ·
domain:spec·area:records· 2026-10-29T19:55Z⛔ Not a claim.
packages/lintis the spec lane by anchoring. The finding takes the same severity as the A virtual formula field declared in searchableFields clears lint and the #4254 gate, then silently matches nothing — the same fail-open shape #4254 closed one axis over #6674formula-entry finding, measured: a silent narrowing gets a word at build time.os validatepasses, and the claim owesClause-②: no (narrowing). If it is a warning, it owes nothing. It is not a new CI gate.enhancement,pm:queue.Generated by Claude Code
objectstack-fleet commentedon Oct 29, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
Claim: PM loop round 2 · 2026-10-29T22:19Z
Session:
session_017ihL2fi9zfJhUTubyJYb6VAccount:
marchtianBranch:
claude/issue-27705-searchable-credential-fieldsWorktree:
objectstack-issue-27705Domain:
domain:specSeat:
domain:spec#4Ruling-ref: triage 6506902320 ("The finding takes the same severity as the #6674
formula-entry finding"; if an error, "the claim owesClause-②: no (narrowing)")File surface:
packages/lint/src/validate-searchable-fields.ts(a credential-type check beside the virtual-entry check, ~L414 onef359b95, on every surface that check covers), the shared credential-type predicate where it already lives or the smallest shared home for it, the rule'sos explainentry,packages/lint/src/validate-searchable-fields.test.ts, a@objectstack/lintchangeset; ⛔ no runtime change (packages/objectql/src/engine.tsstays) (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: TIER_DEFAULT(opus) — the quota-exhaustion exemption (claude-fable-5-1answered HTTP 429 at ~17:05Z; maintainer 「你没有fable了,使用 opus开发完之后等契约复审」),effort: highClause-②: no
Metadata-protocol: narrowed
Responsibility:
packages/lintpasses asearchableFieldsentry the engine drops since #27477 | the existingsearchable-fieldsrule and its virtual-entry check are the path to extend | any author naming a credential field insearchableFields; reached throughos validateand the publish doorThread-read: 6506902320
Premise: holds — on
ef359b95the virtual-entry check isseverity: 'error'(validate-searchable-fields.ts:417) and no credential-type check exists in the fileSerial constraints cleared: none (no open PR touches
validate-searchable-fields*orsearch-fields.ts; open PRs' file lists read just before this claim)objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
os-dev-report
Reported: 2026-10-30T00:25Z
{ "issue": 27705, "status": "done", "branch": "claude/issue-27705-searchable-credential-fields", "pr": "https://github.com/objectstack-ai/objectstack/pull/27811", "head": "257e83df2f0883966cf37254eae5eab87d36bee4", "session": "session_017ihL2fi9zfJhUTubyJYb6V", "premise_still_valid": true, "summary": "Premise held on 34dc09b68d: expandSearchOnAst builds the scan field map without collectCredentialFields(schema), and validate-searchable-fields had no credential check. searchable-field-unsearchable now reports a secret/password entry at error, on every surface the virtual check covers (object set, list views, overlay, ViewItem record, react ListView prop via the shared core), before the admissibility pass. The remedy line is: Drop NAME, credential fields are never searched. The predicate is a new spec export isCredentialFieldType (keys of MASKED_ON_READ_FIELD_TYPES, exemption-free). objectql collectCredentialFields reads it now (a one-line swap). engine.ts is untouched. os explain gains the credential paragraph.", "tests": "at 257e83df2f unless noted. lint: Test Files 178 passed (178), Tests 7633 passed (7633); lint typecheck exit 0 (check:test-typecheck OK). spec: Test Files 755 passed (755), Tests 22452 passed | 1 todo; spec typecheck exit 0. objectql --project local: Test Files 490 passed (490), Tests 9244 passed (9244); objectql typecheck exit 0. Package runs were taken before the merge of origin/main (3 commits, none in spec/lint/objectql). ablation at f393db9bec: anchor 'if (isCredentialFieldType(ctype)) {' became 'if (false && ...)', blob 1343dd89e129 to 892e53b49026, result Tests 8 failed | 241 passed (249) (the 7 credential pins plus the fix-line coverage control). Restored: blob == HEAD, git diff HEAD empty. The lint tests import src through relative paths, so no dist leg was needed.", "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 257e83df2f: 85 families, all exit 0, captured before any pipe. --ran reconciliation: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN. First union at 340955697a: 84 exit 0, check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, no full dist). The final union ran after the dogfood closure build: 112 entry points, 69 packages load. spec check:generated: api-surface and export-origins were stale and regenerated with --fix. CI check-runs: in_progress at report time.", "dogfood_battery": "257e83df2f · os-verify-lock VERDICT command-exit 0 · Test Files 355 passed | 3 skipped (358) · Tests 2867 passed | 20 skipped (2887) · no failures", "line_budget": "11 files, +314 / -15 (329 changed lines, under the 3000 human-merge threshold); no skills/** file touched", "files_changed": [ ".changeset/27705-searchable-credential-fields.md", "packages/lint/src/rule-explanations.ts", "packages/lint/src/runtime-gate.object-writes.test.ts", "packages/lint/src/validate-react-page-props.test.ts", "packages/lint/src/validate-searchable-fields.test.ts", "packages/lint/src/validate-searchable-fields.ts", "packages/objectql/src/secret-fields.ts", "packages/spec/api-surface/data.json", "packages/spec/export-origins/data.json", "packages/spec/src/data/masked-field-types.test.ts", "packages/spec/src/data/masked-field-types.ts" ], "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, run 40979446453, PR #27811, body read back byte-identical, 8917 bytes); this comment via scripts/pm/post-stamped.mjs --comment=27705. git push not counted.", "deviations": [ "Clause-② line: the claim reads 'Clause-②: no' with no arm; the order and dispatch say 'Clause-②: no (narrowing)', and the PR body and changeset carry the order's spelling (the error severity makes it a narrowing).", "Report comment: line 1 is the seat signature (the dispatch requires it), so os-dev-report is on line 2, not line 1 as os-dev.md says.", "PR body ends with the dispatch's footer line, not the session-URL footer os-dev.md describes for PR bodies; the session URL is in each commit trailer.", "Fable review not run, per the order's override; PR body line: Independent review: none run (quota).", "No PR assignees: os-dev.md and _common forbid them; AGENTS.md says the executor mirrors the card's assignee at pr_create. That conflict is left to the seat.", "Scope widened by the order's allowed one-line swap: packages/objectql/src/secret-fields.ts reads the new spec predicate (behaviour unchanged, objectql suite green); isMaskedOnReadFieldType now calls isCredentialFieldType for its own-key test (answers unchanged, spec FieldType x managedBy pins green)." ], "open_questions": [ { "question": "The REST ingress assertSearchFieldsAreSearchable admits a $searchFields entry naming a credential field that the object itself declares, and the engine then withholds it. Read from resolveSearchFields: a request naming only such fields is answered over the whole allowed set. File it?", "options": ["A: seat measures it through the data door and files a class-b card (seam spec:searchableFields to runtime:assertSearchFieldsAreSearchable)", "B: leave it as the acceptance note on #27811"], "recommendation": "A, because it is the silent-widening shape #4254 closes, and this lint finding only covers authored metadata, not a client-built $searchFields" } ], "out_of_scope_findings": [ "class: b (candidate) · reach: NOT MEASURED (code reading only: protocol-query-validation.ts assertSearchFieldsAreSearchable has no credential refusal; search-fields.ts resolveSearchFields falls back to allowed when the intersection is empty) · evidence: PR #27811 Acceptance notes · dedupe words: searchFields credential secret password ingress widening", "carrier: #27660 (skills half) · noted, not filed: skills/objectstack-ui/rules/list-views.md boundary table has no credential row for searchable-field-unsearchable" ] }objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
ACCEPT → PR #27811 comment 6511839038 (lane part) + Contract review PASS 6512125643 (
CONTRACT_REVIEW_TIER, triage, head257e83df) · 2026-10-30T00:53Z · readied and auto-merge armed by the seat (every check green;check-expected-skipsOK, 16 skips in the roster);needs:contract-reviewremoved.Clause-②value: the PR is reviewed and recorded asyes (narrowing)(newisCredentialFieldTypeexport); the carriers still readno (narrowing)because the relay cannot rewrite this PR body, and a changeset-only push would split the two carriers and cost a CI cycle for a word that moves no gate.SEARCH_AUTO_EXCLUDED_TYPESand the mask table: filed as spec(search): no test pins SEARCH_AUTO_EXCLUDED_TYPES to cover every credential type in MASKED_ON_READ_FIELD_TYPES, so a new masked type could reach REST $searchFields while the engine withholds it #27822.$searchFieldsingress reading (open question 1): the seat measures it through the data door before filing.objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
REST
$searchFieldsingress reading (open question 1 of PR #27811's acceptance notes), measured at 2026-10-30T01:04Z throughGET /api/v1/data/:objecton359e935df2, sqlite and in-memory drivers: a$searchFieldsnaming only asecret/passwordfield that the object lists insearchableFieldsanswers 200 and searches the rest of the allowed set (a narrower request answered wider). The credential column never reaches the driver's WHERE clause, so there is no leak. Unlisted and auto-default cases already answer 400INVALID_FIELD. Filed as #27829 (finding, ungraded). It is a door change, outside this card's lint scope, and does not block PR #27811.objectstack-fleet commentedon Oct 30, 2026
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V
Landed · 2026-10-30T01:20Z · PR #27811 merged by the queue at 2026-10-30T01:19Z as
755e28500c; the card closed byFixes. This seat stripspm:dispatchedin the same act. Follow-ups: #27822 (pin the search exclusion set to the mask table, p3), #27829 (the REST$searchFieldsdoor admits a declared credential entry, ungraded).