Skip to content

api.enableSearch and metadata.maskObjectFields are live REST config keys that no schema in packages/spec declares — read through as any, and stripped by any parse of their own config object #11983

Closed

Description

@os-zhuang

Found while implementing #11637 (making the REST server parse config.api instead of casting to it). Filed, not fixed — the repair is a packages/spec declaration, which is domain:spec's single-owner surface and outside #11637's declared scope.

This is the mirror image of #11637: that card is a key the spec declares and the runtime never enforces; this is a key the runtime honours and the spec never declares.

What was measured

On origin/main @ 7899f5745.

RestServer.normalizeConfig reads two keys through as any because they have no declared seat:

// packages/rest/src/rest-server.ts (normalizeConfig)
enableOpenApi: (api as any).enableOpenApi ?? true,
enableSearch: (api as any).enableSearch ?? true,
...
maskObjectFields: isObjectSchemaMaskingEnabled((metadata as any).maskObjectFields),
  • enableOpenApi is declared (packages/spec/src/api/rest-server.zod.ts), so its as any is stale residue — harmless, but it is what makes the line beside it look equally safe.
  • enableSearch is not. git grep -c enableSearch origin/main -- packages/spec/src → zero hits (reverse-checked with enableDiscovery, 1 hit in the same file, and projectResolution, 3 hits across the directory). It is nonetheless live: NormalizedRestServerConfig.api.enableSearch is typed, and registerRoutes / the discovery capability block both read it, so enableSearch: false really does turn the search surface off today.
  • metadata.maskObjectFields is the same shape, and the code says so in place: "MetadataEndpointsConfigSchema lives in packages/spec and giving this key a declared seat there is a separate change" (ADR-0106 D8).

Why it is not merely cosmetic

RestApiConfigSchema is not .strict(), and a non-strict z.object() strips what it does not declare. Measured directly:

RestApiConfigSchema.parse({ version: 'v1', enableSearch: false })
  → the returned object has no `enableSearch` property at all

So the moment anything parses this config and consumes the result, a deployment that turned search off silently gets it back on — no error, no warning, nothing to grep. That is the ADR-0104 silent-strip class the tombstones in packages/spec/src/shared/retired-key.ts exist to prevent, and it is a live trap rather than a hypothetical: #11637 had to make its parse validation-only, discarding the parsed value and normalizing from the raw input, specifically to avoid tripping it. That workaround is a cost this undeclared key imposes on every future parse at that seam.

There is also the authoring face: RestApiConfig is z.input<typeof RestApiConfigSchema>, so a TypeScript author writing api: { enableSearch: false } against the declared type gets an excess-property error today and has to cast — which is why every in-repo caller passes as any.

Not prejudged

  • Declare both keys in RestApiConfigSchema / MetadataEndpointsConfigSchema with the defaults the runtime already applies (enableSearch: z.boolean().default(true), and the ADR-0106 D8 default for maskObjectFields), then delete the three as any reads. Declared = enforced, and the seam's parse can consume its own output.
  • Retire enableSearch under ADR-0049 enforce-or-remove if the deployment-wide search opt-out is not a capability worth keeping — it would need a tombstone, since deleting an honoured key silently is the failure above.

Either way enableOpenApi's stale as any should go with it: it is declared, and the cast is what makes its undeclared neighbour look normal.


Generated by Claude Code

Activity

  1. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 26, 2026
  2. self-assigned this
    on Aug 26, 2026
  3. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    Claim: PM loop round R8, wave 1
    Session: session_012xGvxcwPRTJfA7RfjXEYA4
    Branch: claude/issue-11983-declare-live-rest-config-keys
    Worktree: objectstack-issue-11983
    Domain: domain:spec
    File surface: packages/spec/src/api/rest-server.zod.ts + the metadata-endpoints config schema in the same package (declare api.enableSearch, metadata.maskObjectFields with the runtime's current defaults), spec authorable baselines if gates demand, packages/rest/src/rest-server.ts (delete the three as any reads at :3065/:3066/:3101), config-shape tests, one changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: claude-fable-5 — clause-② content limb (widens the declared config surface); dispatch-gates @ f93df4d: no path-derived tier mandate, clause-② suspect line flags packages/spec/src/api/rest-server.zod.ts ⇢ packages/spec/src/**
    Clause-②: yes
    Serial constraints cleared: in-flight #12573 (cli lane) holds packages/rest TEST files + test-typecheck-debt.json, and its dispatch order explicitly forbids its dev from editing rest-server.ts — surfaces disjoint by construction; this card in turn must not touch that ledger, its 12 listed test files, or src/rest.test.ts (fenced separately by open PR #12421). No spec-lane in-flight siblings. H17 on-hold trigger files (#8345/#8346: field.zod.ts / view.zod.ts / currency data) — no intersection.


    Generated by Claude Code

  4. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    Dev claim (executing seat): running as a mode:subagent dev inside PM session session_012xGvxcwPRTJfA7RfjXEYA4 (R8 wave 1).
    Branch: claude/issue-11983-declare-live-rest-config-keys (pushed; this branch is my distinguishing identity)
    Worktree: objectstack-issue-11983 @ base f93df4d
    Scope per claim comment 5428472607: declare api.enableSearch + metadata.maskObjectFields in packages/spec with the runtime's current defaults, delete the three as any reads in packages/rest/src/rest-server.ts, config-shape tests, one changeset. Not touching packages/rest/test-typecheck-debt.json, its 12 listed test files, or packages/rest/src/rest.test.ts.


    Generated by Claude Code


    Generated by Claude Code

  5. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    os-dev-report

    {
      "issue": 11983,
      "status": "done",
      "branch": "claude/issue-11983-declare-live-rest-config-keys",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12605",
      "premise_still_valid": true,
      "summary": "Declared api.enableSearch in RestApiConfigSchema (z.boolean().default(true), describe states the runtime meaning) and deleted all three as-any config reads in packages/rest normalizeConfig; NormalizedRestServerConfig.api.enableSearch is now a required boolean and the two now-dead '?? true' read sites were simplified. Premise partially overtaken: metadata.maskObjectFields was ALREADY declared on main (MetadataEndpointsConfigSchema, default true, ADR-0106 D8, with pin tests) before this run — only its rest-side cast was stale — so this PR declares one key, not two. The card's flip measurement is pinned: parse of enableSearch:false now RETURNS the key. Stale prose in the #11637 pin file and in assertDeclaredApiConfig's doc updated to the declared reality; the parse there stays validation-only for the omitted keys (requireAuth #3963, projectResolution #11999). Draft PR per Clause-2; no ready-flip, no auto-merge, no labels touched.",
      "tests": "All at final head b3357c6e: spec build + check:generated all 14 artifact gates green (regenerated authorable-surface/api.json, authorable-defaults/api.json, references/api/rest-server.mdx via gen:docs); spec typecheck green (check:test-typecheck: OK); spec src/api/rest-server.test.ts 53 passed incl. the three new [#11983] pins (verbose listing captured); rest typecheck green (debt ledger unchanged, 13 files / 37 errors held); rest targeted tests (discovery-search-capability-agreement, rest-config-parse-not-cast, discovery-per-request-protocol, meta-object-fls) 123 passed; gates check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher + check:query-options-erasure (baseline verified against dfebfc8), check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset all green, each read from the gate's own verdict line with exit captured before any pipe. Reverse A (predicted red): zod file reverted to origin/main from committed state, mutation confirmed on disk (grep count 0), exactly the 2 parse pins red / 51 green, restored from HEAD with empty git diff HEAD. Reverse B (predicted red): typed read mutated to enableSearchX (confirmed on disk), tsc red TS2551 suggesting enableSearch — proves rest reads the REBUILT spec d.ts — restored from HEAD, tree clean. No ablation-through-dist legs (spec tests import source; rest reads freshly rebuilt dist, proven by reverse B). Gate derivation: dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-fed paths, derived at my commit; dispatched families all run; full-farm convergence is CI's per dispatch.",
      "mcp_calls": 7,
      "gates_run": "spec check:generated (14/14), spec typecheck, rest typecheck, spec+rest targeted vitest, check:authz-resolver, check:route-envelope, check:spec-parsed-alias, check:nul-bytes, check:where-matcher, check:query-options-erasure, check:engine-double-contract, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset — all green at b3357c6e",
      "files_changed": [".changeset/declare-live-rest-config-keys.md", "content/docs/references/api/rest-server.mdx", "packages/rest/src/rest-config-parse-not-cast.test.ts", "packages/rest/src/rest-server.ts", "packages/spec/authorable-defaults/api.json", "packages/spec/authorable-surface/api.json", "packages/spec/src/api/rest-server.test.ts", "packages/spec/src/api/rest-server.zod.ts"],
      "deviations": ["maskObjectFields declaration found already landed on main — PR declares enableSearch only and deletes all three casts (premise half-overtaken, core defect still live)", "beyond the three named casts: tightened NormalizedRestServerConfig.api.enableSearch to required boolean, dropped the two now-dead '?? true' reads (enableSearch and enableOpenApi read sites), same file, zero behavior change", "edited packages/rest/src/rest-config-parse-not-cast.test.ts (prose/title only, assertion kept) — within the claimed config-shape-tests surface, not debt-fenced; declared here as a file-surface increment", "changeset bump-class precedent for the landed maskObjectFields declaration unfindable (history grafted to one squash commit) — patch used as dispatched", "rebased onto origin/main dfebfc8 before PR (4 upstream commits, no overlap, clean); union re-run at final head"],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-zhuang commented on Aug 26, 2026

    @os-zhuang
    ContributorAuthor

    ACCEPT — reviewer of record: spec seat R8, session_012xGvxcwPRTJfA7RfjXEYA4. PR: #12605 (draft, head b3357c6e).

    Checked against the PR itself, not the report narrative: 8 files match the claim plus three declared increments (normalized enableSearch tightened to required boolean with the two dead ?? true reads dropped — behavior-neutral by construction since the default materializes in normalizeConfig; rest-config-parse-not-cast.test.ts prose-only edit keeps its assertion and is outside the #12573 fence; base rebased to dfebfc8). Spot-checks: the flip pin asserts hasOwnProperty on the parsed output (the card's exact measured trap, now inverted); the discovery capability block reads the same normalized field as the mount; authorable-surface/api.json + authorable-defaults/api.json each gain exactly one row; changeset is patch × 2 packages with the silent-strip history. Reverse verification A (schema revert → exactly 2 pins red) and B (typed-read mutation → TS2551 naming enableSearch, proving rest typechecks against the rebuilt spec d.ts) both ran in the predicted direction with disk-confirmed mutations and hash-proven restores. Premise deviation accepted as measured: maskObjectFields was already declared on main (its cast was stale residue) — the card's second half was landed by someone else, correctly not re-done.

    Clause-② enqueue gate: path limb fires (packages/spec/src/api/rest-server.zod.ts); dispatch tier was claude-fable-5 = CONTRACT_REVIEW_TIER (dispatch-gates.mjs:3813), so the tier condition is satisfied. Landing path: ready → merge queue once EVERY check on the head is green (not just required); CI convergence is this seat's watch.


    Generated by Claude Code

  7. removed their assignment
    on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions