Skip to content

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

Closed

Description

@objectstack-fleet

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 name collectCredentialFields(schema) returns (packages/objectql/src/secret-fields.ts:289, type secret or password), while searchableFields is 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.ts checks 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 a secret / password entry passes both.

The ask:

  1. In packages/lint/src/validate-searchable-fields.ts, next to the virtual-entry check at :414, report a searchableFields entry whose field type is secret or password, 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 reuses isVirtualSearchField.
  2. Choose the severity deliberately (error, as the virtual entry is, or warning) and say why in the rule's doc comment and its os explain entry.
  3. Pin it in packages/lint/src/validate-searchable-fields.test.ts: one case each for secret and password on 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

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 错的必须被响亮拒绝并给处方 | none | P②

    Triage: p2 · domain:spec · area:records · 2026-10-29T19:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    Claim: PM loop round 2 · 2026-10-29T22:19Z
    Session: session_017ihL2fi9zfJhUTubyJYb6V
    Account: marchtian
    Branch: claude/issue-27705-searchable-credential-fields
    Worktree: objectstack-issue-27705
    Domain: domain:spec
    Seat: domain:spec#4
    Ruling-ref: triage 6506902320 ("The finding takes the same severity as the #6674 formula-entry finding"; if an error, "the claim owes Clause-②: no (narrowing)")
    File surface: packages/lint/src/validate-searchable-fields.ts (a credential-type check beside the virtual-entry check, ~L414 on ef359b95, on every surface that check covers), the shared credential-type predicate where it already lives or the smallest shared home for it, the rule's os explain entry, packages/lint/src/validate-searchable-fields.test.ts, a @objectstack/lint changeset; ⛔ no runtime change (packages/objectql/src/engine.ts stays) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: TIER_DEFAULT (opus) — the quota-exhaustion exemption (claude-fable-5-1 answered HTTP 429 at ~17:05Z; maintainer 「你没有fable了,使用 opus开发完之后等契约复审」), effort: high
    Clause-②: no
    Metadata-protocol: narrowed
    Responsibility: packages/lint passes a searchableFields entry the engine drops since #27477 | the existing searchable-fields rule and its virtual-entry check are the path to extend | any author naming a credential field in searchableFields; reached through os validate and the publish door
    Thread-read: 6506902320
    Premise: holds — on ef359b95 the virtual-entry check is severity: 'error' (validate-searchable-fields.ts:417) and no credential-type check exists in the file
    Serial constraints cleared: none (no open PR touches validate-searchable-fields* or search-fields.ts; open PRs' file lists read just before this claim)

  3. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
      ]
    }
  4. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    ACCEPT → PR #27811 comment 6511839038 (lane part) + Contract review PASS 6512125643 (CONTRACT_REVIEW_TIER, triage, head 257e83df) · 2026-10-30T00:53Z · readied and auto-merge armed by the seat (every check green; check-expected-skips OK, 16 skips in the roster); needs:contract-review removed.

  5. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

    REST $searchFields ingress reading (open question 1 of PR #27811's acceptance notes), measured at 2026-10-30T01:04Z through GET /api/v1/data/:object on 359e935df2, sqlite and in-memory drivers: a $searchFields naming only a secret / password field that the object lists in searchableFields answers 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 400 INVALID_FIELD. Filed as #27829 (finding, ungraded). It is a door change, outside this card's lint scope, and does not block PR #27811.

  6. objectstack-fleet commented on Oct 30, 2026

    @objectstack-fleet
    ContributorAuthor

    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 by Fixes. This seat strips pm:dispatched in the same act. Follow-ups: #27822 (pin the search exclusion set to the mask table, p3), #27829 (the REST $searchFields door admits a declared credential entry, ungraded).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions