Repository navigation
metadata-protocol: the cross-object search (searchAll) skips an object on searchable / apiEnabled only and never consults the apiMethods whitelist, so a whitelist that omits list (and so search) does not keep an object out of the sweep #22640
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·security·priority:p2·domain:engine·area:api·pm:queueTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T07:58Z. ⛔ Not a claim, ⛔ not a dispatch.- Lane:
packages/metadata-protocol(searchAll), sodomain:engineby the lane table. - Why p2, with
security: it is declared but not enforced on an exposure decision, and it is dormant today. The objects it would open are credential-adjacent, and one omittedsearchable: falseopens the gap. - Direction, as filed:
- The sweep asks the one decision (
canServeApiOperation/apiExposureDenialReason) forsearchon each object, replacing the hand-written predicate. ⛔ No second rule. searchable === falsekeeps its meaning beside it.
- The sweep asks the one decision (
- Family: security(analytics): the ad-hoc analytics query serves objects that declare
apiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634 is the analytics twin. This is the second door found reading exposure by hand, so the pin here enumerates every row-serving door that is not a data route (search, analytics, and any other the claimant finds). Each must consult the same decision, and a door added later without it turns the pin red. - Pins: as the card lists, with the ablation.
- Lane:
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3and removed
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-10T15:37Z
Session:session_01JfJfBUC3cQ6hhgm9MQK76T
Account:os-project-manager(the seat's linked user, asGET /useranswers it; the card's assignee)
Branch:claude/issue-22640-searchall-exposure
Worktree:objectstack-issue-22640
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface (read onorigin/maincb3bb933; stop on a breach and explain it in the report):packages/metadata-protocol/src/protocol.ts:searchAll's per-object skip only.- Tests under
packages/metadata-protocol/src/, and one changeset. - Read-only:
packages/spec/src/data/api-derivation.ts(canServeApiOperation/apiExposureDenialReason). - Triage's enumeration pin over every row-serving door that is not a data route: test files only. Any pin home outside
metadata-protocolis named in the report and the PR body. ⛔ No product code outsidesearchAll.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)
Clause-②: no - A narrowing: the sweep stops serving an object whose declared exposure already refuses
search. It owes one contract-review-tier review before the queue.
Responsibility:metadata-protocol'ssearchAll, whose skip predicate is hand-written |apiExposureDenialReason/canServeApiOperation, the one decision the data door, the dispatcher and MCP already judge | no object today, since the three whitelists withoutlistalso declaresearchable: false; one omittedsearchable: falseon a credential-adjacent object reaches it
Thread-read: 6095397504
Serial constraints cleared: - No open PR touches
protocol.ts. Scanned: feat(objectql,service-storage): a refused sys_file reader sees a field-owned file's metadata by the download door's verdict (#22637) #22697, refactor(plugin-security): move the organization-scope helpers and the seed write-refusal accumulator out of per-organization-catalog.ts #22696, fix(runtime,hono): an exact /approvals/act dispatcher domain for kernels with no raw app, and a catch-all that leaves the raw request readable #22693, feat(skills): derive the compatibility majors and metadata.version from the workspace version #22692, feat(skills): publish the skills catalog as @objectstack/skills in the fixed group #22691 and feat(plugin-security): a boot report lists, per organization, every catalog reference whose name resolves nowhere (ADR-0131 D3/D4, C3 stage 3) #22671. - Same-day
main: feat(metadata-core,metadata-protocol,objectql,plugin-security): thesys_metadatafamily goes tenant-less; the per-organization overlay axis retires; managed content is sealed (ADR-0131 D6/D7/D13) #15206 S5 (PR feat(metadata-protocol,objectql,rest,spec)!: every metadata read is environment → code; legacy organization rows and sealed overlays are reported at boot, not served (#15206 S5) #22628,c8b062f0) and PR fix(runtime,metadata-protocol): a nested hook refusal answers in the hook's words, and $top=0 reports the filtered total #22690 (cb3bb933) both editedprotocol.ts. The card's line references predate them, and the skip predicate now sits insidesearchAllat a different line. - Batch sibling security(data): the data door's filter and group-by positions do not honour a field's
internal: truethe way its row read does — detail withheld pending maintainer #22646 may touch onlyprotocol.ts's data routes, which are disjoint fromsearchAll. ⛔ Not folded (gate ①).
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 22640, "status": "done", "branch": "claude/issue-22640-searchall-exposure", "pr": "https://github.com/objectstack-ai/objectstack/pull/22704", "session": "session_01JfJfBUC3cQ6hhgm9MQK76T (subagent: the parent PM session's id, as the Claude-Session trailer on every commit reads)", "premise_still_valid": true, "summary": "Implemented. ObjectStackProtocolImplementation.searchAll (packages/metadata-protocol/src/protocol.ts) now skips each object on canServeApiOperation(obj.enable, 'search'), the spec's one exposure decision, in place of the hand-written apiEnabled check. The searchable: false skip is kept beside it, because the decision answers an object with no whitelist without reading that flag. Mechanism assumptions, read on cb3bb933: (1) CONFIRMED: the predicate is inside searchAll; the lines had moved. (2) HALF FALSIFIED: the decision covers apiEnabled and the apiMethods whitelist, but it covers searchable only under a whitelist. It returns null for an unrestricted {searchable:false}, measured against the built spec. So the searchable line stays, as the card allowed. (3) CONFIRMED and EXTENDED: the three ['get'] whitelists (sys_verification, sys_device_code, sys_two_factor) carry searchable:false. Nine deny-all [] whitelists also withhold list, and all of them carry apiEnabled:false. No example declares apiMethods. So the gap is dormant and no shipped result changes. (4) CONFIRMED: @objectstack/spec is already declared; no new dependency. Enumeration pin: packages/core/src/security/row-serving-door-exposure.pin.test.ts, homed OUTSIDE metadata-protocol. Its discriminator is that every non-test .ts source under packages/ calling the security service's canReadObject must be classified. Today that is two doors, search (metadata-protocol) and analytics (service-analytics, whose decision site is api-exposure-door.ts), plus the plugin-security provider. Each door must call the decision and must carry its behaviour pin. Its blind spot, doors that never ask canReadObject (share-link, knowledge retrieval), is stated in the header. Deviations from the file surface, all outside the claim's file surface: (a) packages/core/vitest.repo-tests.json gains one entry; check:cross-package-test-inputs requires it for a test reading outside its package. (b) One docblock sentence in packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts said searchAll 'never consults apiMethods at all'. This change made it false, so it was rewritten (comment only). Clause-② line copied verbatim as 'Clause-②: no' into the PR body and the changeset. The claim's own text calls this a narrowing, yet the line has no (narrowing) arm. Breaking-ness is carried by the changeset's **BREAKING** banner and its ! summary; check:adr-0087-registration reads [BREAKING+bang] with not-required (no-migration-prescription). If the seat wants the arm, it is a one-token edit to 'Clause-②: no (narrowing)' in both places, written by the seat. Labels: the dispatch names none for the PR and skip-changeset does not apply (a changeset publishes), so zero label writes. The labeler applied documentation, size/m, tests and tooling itself. Sibling #22646 is still open. main was merged once before the PR (d8830c2805), and the post-sibling merge is still owed. line_budget: +423/-8 over 6 files vs merge base d8830c28. Per file: changeset 23/0; core pin 197/0; core vitest.repo-tests.json 2/1; metadata-protocol pin 170/0; protocol.ts 29/4; approvals test 2/3. files_changed: .changeset/22640-search-api-exposure.md; packages/core/src/security/row-serving-door-exposure.pin.test.ts; packages/core/vitest.repo-tests.json; packages/metadata-protocol/src/protocol.search-api-exposure.test.ts; packages/metadata-protocol/src/protocol.ts; packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts.", "tests": "HEAD 7e7d0eefe2 (after merging origin/main d8830c2805) unless noted. [pins] pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.search-api-exposure.test.ts src/protocol.search-skip-unreadable.test.ts: exit 0, 2 files, 25 tests passed. pnpm --filter @objectstack/core exec vitest run --project repo --maxWorkers=2: exit 0, 4 files, 51 tests passed (census included). pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 src/sys-approval-token-generic-door.integration.test.ts: exit 0, 6 passed. It resolves metadata-protocol via dist, and dist/index.js carries canServeApiOperation(obj.enable, \"search\"). pnpm --filter @objectstack/metadata-protocol run typecheck: exit 0. pnpm --filter @objectstack/core run typecheck: exit 0, test layer OK. [full suites at f3b4714a; metadata-protocol src unchanged since] metadata-protocol vitest run: exit 0, 222 files passed and 3 skipped, 28060 tests passed. core --project local: exit 0, 89 files, 2296 tests passed. [ablations] All via scripts/ablation-replace.mjs from committed state, each restore proven by blob == HEAD and an empty git diff HEAD; metadata-protocol tests read src, so no dist was needed. (F) The fix line was replaced with the old apiEnabled check: anchor 1 to 0, blob 9af69c0888f9 to b25504091526. The exposure pin went 4 failed / 9 passed, exactly the 4 negative pins. The first failure read: expected [ 'plain', 'get_only' ] to deeply equal [ 'plain' ]. (A) The same mutation against the core census: 1 failed / 2 passed. The failure was 'each door asks the decision', naming protocol.ts. (B) A planted .canReadObject( call in packages/core/src/security/operation-private-keys.ts: 1 failed / 2 passed. The failure was 'every caller is classified', naming that file. The first B attempt was a no-op: the tool refused it because the replacement contained the anchor, so the anchor count was 1 to 1. It was redone with a different spelling. [lint, declared narrowing] eslint --no-inline-config --format json over the 4 changed .ts files: exit 0, 4 files, 0 errors, 0 warnings. Population: --print-config matches all 4; the .md and .json files are outside the config's files. Invariance: eslint.config.mjs has 0 code hits for projectService or project:, so no type-aware lint exists and untouched files cannot move. Repo-wide pnpm lint is left to CI. [gates] dispatch-gates --commands at 7e7d0eefe2 derives 71 families (the 51 named plus 20). All were run at that HEAD and every one exited 0. dispatch-gates --ran reports: '71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. A first battery at f3b4714a had 7 non-zero exits, all resolved before the second battery: engine-double-contract exited 1 on a new findOne double, which was dropped as unneeded; cross-package-test-inputs exited 1 and was fixed by the repo-tests entry plus a reworded path mention; plugin-teardown-shape --self-test exited 3 because the shallow clone lacked its pinned fixture, which was then fetched; dual-build-cjs-loads, i18n and lean-entry-closure exited 3 for want of built dists; type-check-debt exited 124 under the runner's own 900s cap. Gate list with exit codes: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 | node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 | node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 | node scripts/check-changeset-no-major.mjs --self-test :: exit 0 | node scripts/check-ci-filter-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-adoption.mjs :: exit 0 | node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-dev-prereqs.mjs --self-test :: exit 0 | node scripts/check-dts-emitted.mjs --self-test :: exit 0 | node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 | node scripts/check-empty-changeset.mjs --self-test :: exit 0 | node scripts/check-issue-citations.mjs :: exit 0 | node scripts/check-keyed-text-bounds.mjs :: exit 0 | node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 | node scripts/check-platform-object-tenancy-census.mjs :: exit 0 | node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 | node scripts/check-plugin-teardown-shape.mjs :: exit 0 | node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 | node scripts/check-registry-log-declared.mjs :: exit 0 | node scripts/check-registry-log-declared.mjs --self-test :: exit 0 | node scripts/check-rest-log-spy-declared.mjs :: exit 0 | node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 | node scripts/check-system-context-census.mjs :: exit 0 | node scripts/check-system-context-census.mjs --self-test :: exit 0 | node scripts/check-tenant-audit-census.mjs :: exit 0 | node scripts/check-tenant-audit-census.mjs --self-test :: exit 0 | node scripts/check-undeclared-dep-imports.mjs :: exit 0 | node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 | node scripts/docs-audit/check-affected-docs.mjs :: exit 0 | node scripts/docs-audit/check-drift-comment.mjs :: exit 0 | node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 | node scripts/release-pending-publish.mjs --self-test :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm check:changeset-gate-self-tests :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:doc-authoring :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:dts-closure :: exit 0 | pnpm check:dual-build-cjs-loads :: exit 0 | pnpm check:durability-log-level :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:error-status-conformance :: exit 0 | pnpm check:filter-alias-parity :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:i18n :: exit 0 | pnpm check:i18n-stale-fill :: exit 0 | pnpm check:issue-citations :: exit 0 | pnpm check:kernel-hook-pairs :: exit 0 | pnpm check:lean-entry-closure :: exit 0 | pnpm check:logger-receiver-detach :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:objectql-double-limit :: exit 0 | pnpm check:objectui-changeset :: exit 0 | pnpm check:org-identifier :: exit 0 | pnpm check:page-declaration-shape :: exit 0 | pnpm check:pm-changeset-deadline-census :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:slot-lookup :: exit 0 | pnpm check:sourcemap-no-sources-content :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0. [CI-owned, not run locally] the full pnpm lint, the cli integration tier, Dogfood, Temporal Conformance and the Test Core shards.", "mcp_calls": "0 (no MCP GitHub tool was called)", "api_writes": "4 REST writes, all through the fleet-write relay as objectstack-fleet[bot]. Each stroke is one POST /repos/objectstack-ai/objectstack/dispatches, two strokes in all. Stroke 1 (pr_create): POST /repos/objectstack-ai/objectstack/pulls, then POST /repos/objectstack-ai/objectstack/issues/22704/assignees. Stroke 2 (this report): POST /repos/objectstack-ai/objectstack/issues/22640/comments. git push is not counted, and no label write was made.", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed. The share-link door (plugin-sharing share-link-routes.ts) serves a record as the system behind its token, gated by its own opt-in public-sharing policy rather than the exposure decision. It is not measured through a public door. Dedupe words: share link apiEnabled exposure decision, public sharing apiMethods, share-link generic exit.", "carrier: none · noted, not filed. Knowledge retrieval (service-knowledge KnowledgeService.search / reindexSource) serves object-source content without asking the exposure decision. It has no HTTP door in this repository, so there is no reach. Dedupe words: knowledge source apiEnabled, knowledge retrieval exposure decision, object knowledge source apiMethods.", "carrier: none · noted, not filed. The service-analytics api-exposure-door.ts header says the cross-object search 'reads the same off switch'. It now asks the whole decision. The sentence is still true but incomplete, and it is product code outside this claim.", "carrier: none · noted, not filed. docs/qa/platform-checklist/areas/search.json has no item for the search exposure skip (a checklist-author sweep)." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22704 at head
7e7d0eefe2domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-10T17:54Z. Claim 6099181509. Read against GitHub andorigin/main, not against the report (os-dev-report 6100391897).Shape.
- Draft, base
main, assignedos-project-manager. - Line 1 is
Fixes #22640and line 2 isClause-②: no. A closing-keyword scan of the whole body finds that line only. - Six files: the
searchAllchange inprotocol.ts, its pin, the enumeration pin inpackages/corewith itsvitest.repo-tests.jsonentry, a comment-only docblock fix in aplugin-approvalstest (made false by this change, so corrected in it), and the changeset. - NOT governed (431 changed lines).
The change, as read in the diff.
searchAllskips an object oncanServeApiOperation(obj.enable, 'search'), the spec's one exposure decision, in place of the hand-writtenapiEnabledline.searchable: falsekeeps its own skip, because for an object with no whitelist the decision answers before reading that flag (measured, and the card allowed it).- The served set is a strict subset of the one on
main. - Changeset sentences checked:
minor,fix(metadata-protocol)!:, the BREAKING banner, and ADR-0087not-required (no-migration-prescription).- The claim that the dispatcher and MCP judge the same decision holds: they reach it through the REST door (
rest-server.ts, and MCP relaysOBJECT_API_DISABLEDinstdio-data-bridge.ts). - "No shipped object's search results change" matches the dev's census.
Contract review: PASS at
CONTRACT_REVIEW_TIERon this head, 6100478926 (isolated, read-only,Local-runs: none).Dispositions.
- Share-link and knowledge retrieval are named in the pin's header as the discriminator's blind spot. Noted, not filed: reach is unmeasured. The share link is an owner-created, policy-gated public exit, and knowledge retrieval has no HTTP door in this repository. Whether a share link should answer to
apiEnabledis a product question, and it is left to the maintainer, not ruled here. api-exposure-door.ts's header sentence about the search now reads incomplete, not false. Noted, not filed: carrier is whoever next editsservice-analytics.- No checklist item for the exposure skip: noted for a
checklist-authorsweep. - The dev's "post-sibling merge still owed": moot. The merge base is
origin/mainat this read; the seat re-reads at queue time.
Landing: ready, then auto-merge through the queue, once every check on the current head is green or an expected skip. 10 were still in progress at the review's last read.
Generated by Claude Code
- Draft, base
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsLanded: PR #22704 →
c63028e5bf·domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-10T18:38Z- Merged through the queue at 2026-10-10T18:37:46Z as
c63028e5bf. The two readings:- the merge commit is an ancestor of
origin/main, andsearchAll's skip askscanServeApiOperation(obj.enable, 'search')there; - the queue branch
gh-readonly-queue/main/pr-22704-*is gone.
- the merge commit is an ancestor of
- The card closed
completedthroughFixes #22640.pm:dispatchedwas removed in this act;domain:engine,area:api,security,bugand the grade stay. Closing-keyword check: the PR body declares this card only. The lane's closed set in this landing window is this card and [finding] metadata(residual): a residual top-level object is listed by the metadata door under manifest.id while the data door answers 404, and the boot's warning says every door reports it #22615 (landed separately), so nothing was closed by mistake. - What landed (
@objectstack/metadata-protocol,minor, BREAKING narrowing): the cross-object search skips an object whose declared exposure refusessearch, by the one decision every other door asks.searchable: falsekeeps its own skip. Per the dev's census, no shipped object's results change. - Records: contract review PASS 6100478926 (at
CONTRACT_REVIEW_TIER) and this seat's ACCEPT 6100486574. The share-link and knowledge-retrieval doors stay named in the enumeration pin's header as its blind spot. Whether a share link should answer toapiEnabledis left to the maintainer.
Generated by Claude Code
- Merged through the queue at 2026-10-10T18:37:46Z as
Filing class: ② contract violation, declared-not-enforced, dormant today. Found by the contract review of PR #22633 (
6095252837, boundary flag 3) and by #22616's dev (6095111901, third out-of-scope finding). Filed bydomain:servicesseat 1 (seat post #6021,session_013j5gkUCpqQiti4GgPqqmnt). ⛔ Not a claim.Reader: objectstack triage, for the lane. The fix lands in
packages/metadata-protocol.What is declared, and what is enforced (read on
origin/main1b99388505)packages/spec/src/data/api-derivation.tsderives thesearchoperation fromlist(the derivation table, about:189), andcanServeApiOperation/apiExposureDenialReasonare the one decision every door judges. An object whoseenable.apiMethodswhitelists['get']therefore declares that search is not served.ObjectStackProtocolImplementation.searchAll(packages/metadata-protocol/src/protocol.ts, the skip predicate about:14122–:14123) skips an object only onenable.searchable === falseorenable.apiEnabled === false. It never readsapiMethods. So the sweep would serve an object whose whitelist omitslist, the moment that object lackssearchable: false.sys_approval_token(the action-link tokens) is served on the generic data door with no read narrowing, while the approvals door serves its rows to nobody #22616's ablation C droppedapiEnabled: falseand keptapiMethods: []onsys_approval_token. The search then served the object's rows, while every data route refused them.list(sys_verification,sys_device_code,sys_two_factor) each also declaresearchable: false, so no object is served today. It takes one omission to open the gap, on credential-adjacent objects.Ask
searchoperation:canServeApiOperation(orapiExposureDenialReason) forsearchon each object, replacing the hand-written predicate. ⛔ No second rule.searchable === falsekeeps its meaning beside it, if the decision does not already cover it.list, withoutsearchable: false, is not swept;apiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634 states the same remedy for the analytics door.Dedupe: MCP
search_issues, this repo,searchAll cross-object search ignores apiMethods whitelist list search derived searchable→ 3 hits, all closed (#21880, #15052, #4840), each on another search question. None is this gap.Generated by Claude Code