Repository navigation
security(mcp): a nested-relation filter condition through the MCP stdio data bridge is evaluated without asking the target's exposure (census row 8 of #22661) #22777
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guards
on Oct 11, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-11T09:10Z
Session:session_01JfJfBUC3cQ6hhgm9MQK76T
Account:os-project-manager(the seat's linked user, asGET /useranswers it; the card's assignee)
Branch:claude/issue-22777-mcp-relation-filter-exposure
Worktree:objectstack-issue-22777
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface (read onorigin/main14f4912390; stop on a breach and explain it in the report):packages/mcp/src/stdio-data-bridge.ts(domain:cli, declared on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024): the bridge's read path asks each nested-relation condition's TARGET the exposure decision forlist, through the gate it already applies to the addressed object (enforceApiExposure). ⛔ No second rule.- One walk: the data door's
relationConditionSites(packages/metadata-protocol/src/protocol.ts) moves topackages/core/src/utils/and is exported from@objectstack/core, which both doors already depend on.protocol.tsimports it, with no behaviour change there. ⛔ No second walker. If the measurement shows a better home, say so in the report before moving it. packages/core/src/security/second-object-read-exposure.pin.test.ts: census row 8 joins asdecidedif the pin's discriminator can see the bridge. Otherwise the report says why.- Tests where each read lives, and the changesets.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)
Clause-②: yes (narrowing) - The MCP data bridge refuses a nested-relation condition whose target the decision does not serve, where today it evaluates it. That narrows a published accept set.
@objectstack/coregains one export, an additive public member. It owes one contract-review-tier review before the queue, and a changeset stating FROM → TO.
Responsibility:createStdioDataBridge's read path, which judges the ADDRESSED object's exposure and then hands the caller's filter toengine.find, whose relation lowering reads the target as the caller | the spec's one exposure decision, which the data door now asks of each condition's target forlist(security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737,refuseUnservedRelationTarget) | any MCP stdio caller whose filter names a condition on a lookup's related object that refuses the API; measured at the MCP tool surface by security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's dev (os-dev-report 6106039434)
Thread-read: none
Inputs read: the card body; security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's os-dev-report 6106039434 (the finding); security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's ACCEPT 6107195645
Serial constraints cleared: - security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737 (PR fix(metadata-protocol)!: a nested-relation filter condition asks the related object its declared exposure (#22737) #22768) landed as
ec7c7e0637. It was this card'sBlocked-by:, and its refusal is the shape this door gives. - No open PR touches
packages/mcp/src/stdio-data-bridge.tsor the relation-condition walk. - Open PR feat(plugin-security,rest,spec)!: retire the permission-set overlay discard (ADR-0131 cutover stage 7-pre) #22776 touches
protocol.tsonly around line 16616, in a different region from the walk and its import. - In flight in this lane: import: a
userfield written withoutreferencefailsreference_not_foundon import, while the spec's arbiter gives itsys_user#22785 (core'simport-field-meta.ts) and objectql: anafterInserthook withonError: 'abort'(the default) that throws rejects the write, but the row stays stored —HookSchema.onErrorsays abort rolls the transaction back, and a plain write opens none #22782 (objectql's hook dispatch). Both are disjoint files.
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22777,
"status": "done",
"branch": "claude/issue-22777-mcp-relation-filter-exposure",
"pr": "#22820",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "createStdioDataBridge's query() and aggregate() now call enforceRelationTargetExposure right after the addressed object's gate: it finds the nested-relation conditions in where and asks each TARGET enforceApiExposure(..., GATED_ACTIONS.query, ..., position), the gate an addressed object already gets, so list and no second rule; an unserved target refuses before the engine is asked in the bridge's existing envelope (404 OBJECT_API_DISABLED / 405 OBJECT_API_METHOD_NOT_ALLOWED with allowedOperations), the message naming target, position where.FIELD and the remedy (enforceApiExposure gained an optional condition argument). relationConditionSites moved unchanged from metadata-protocol's protocol.ts (module-private) to packages/core/src/utils/relation-condition-sites.ts, exported from @objectstack/core; protocol.ts imports it, refuseUnservedRelationTarget untouched; objectql and types untouched. Census pin: the moved walk is a new referenceTargetOf caller, classified not-a-served-read; the discriminator does not see the bridge (it never calls the arbiter; its targets resolve inside the shared walk), so row 8 is NOT a decided row and the discriminator is unchanged; the header's not-seen paragraph names the bridge and where it is held. HYPOTHESES: H1 confirmed on a real stack (scratch probe, not committed: bootStack over the #22661 dogfood fixture, admin and member API keys minted through the key route, principal via resolveAuthzContext + assembleExecutionContext, real createStdioDataBridge + registerObjectTools over an in-memory MCP pair; StdioServerTransport and process boundary not exercised). Before (base 23419ba), both personas, query_records and aggregate_records: a condition on an API-disabled, get-only or create-only target was evaluated (match -> rows, miss -> none), also under $and/$or/$not, while the same targets addressed directly were refused. After: all refused, match and miss alike; controls (list-only, undeclared target, injected owner lookup to a list-served platform object) unchanged; second-level still refused by the engine before any read. Census of bridge paths handing a caller filter to the engine: query (where -> engine.find) and aggregate (where -> engine.aggregate); get/update/remove and the record resource build where {id} themselves; aggregations[i].filter is not reachable from the aggregate_records tool (no filter member in its schema) and the engine refuses a nested-relation condition there, so not walked. H2 confirmed: core is the right home (both doors depend on it, spec carries no logic). H3 confirmed: reuse of enforceApiExposure with GATED_ACTIONS.query (-> list). H4 measured: every stdio call carried isSystem false / principalKind human; assembleExecutionContext emits isSystem: false as a literal and resolvePrincipal throws on no principal, so the reused isSystem bypass is unreachable from this door and every reachable caller gets #22737's verdict; chose reuse (no second rule). H5 measured: query_records/aggregate_records flatten a thrown error to its message (errorResult(messageOf(err))), as for an addressed refusal: the caller sees isError + text naming target and position; code/status/allowedOperations live on the bridge error and are pinned there. files_changed (9): .changeset/22777-core-relation-condition-sites.md (+10), .changeset/22777-mcp-stdio-relation-condition-target-exposure.md (+31), packages/core/src/index.ts (+6), packages/core/src/security/second-object-read-exposure.pin.test.ts (+15/-1), packages/core/src/utils/relation-condition-sites.test.ts (+32, new), packages/core/src/utils/relation-condition-sites.ts (+69, new), packages/mcp/src/stdio-data-bridge.relation-condition-target-exposure.test.ts (+136, new), packages/mcp/src/stdio-data-bridge.ts (+48/-3), packages/metadata-protocol/src/protocol.ts (+1/-42). line_budget: 394 changed lines (+348/-46) vs suggested 300; source +124/-45 net +79 (mcp +48/-3; core walk +69 is a move offset by protocol.ts -42/+1; core index +6) vs suggested 60. Excess = the moved function's docblock and exported type, and an mcp pin parameterised over 24 refusal cells. deviations: (a) base origin/main 23419ba (moved past the dispatch's 14f4912), origin/main c11b758 merged as 2261c25; main has since gained 3 commits (67b669e) touching none of the 9 paths; not re-merged. (b) line budget above. (c) positions: where on query and aggregate only; aggregations[i].filter not walked (reason above). (d) one hop: the engine serves one level and refuses a relation beneath before reading; the data door recurses only for its internal-field refusal. (e) isSystem bypass kept by reuse, unreachable here (H4). (f) changesets: @objectstack/mcp minor BREAKING with FROM -> TO, migration, the Clause-② yes (narrowing) line and ADR-0087 not-required (no-migration-prescription); @objectstack/core minor + @objectstack/metadata-protocol patch in one non-breaking changeset. (g) zero label writes: dispatch named none; changesets exist. (h) check:dual-build-cjs-loads NOT MEASURED (exit 3, PREREQUISITE NOT MET: 19 packages outside this closure have no dist); narrowed probe in its place. (i) PR body line 2 kept as dispatched: Clause-② yes (narrowing) — yes for core's new export, narrowing for the mcp accept set.",
"tests": "All at final head 2261c25 (origin/main c11b758 merged; dogfood/verify/plugin-security/mcp closure rebuilt after the merge, 47 turbo tasks exit 0). mcp vitest 36 files / 417 passed (new pin 27/27; stdio-data-bridge.exposure.test.ts 27/27). core local 94 files / 2364 passed; core repo 5 files / 55 passed (census pin 4/4). metadata-protocol vitest 225 files passed + 3 skipped / 28129 passed, 19 skipped (protocol.relation-condition-target-exposure.test.ts 13/13). dogfood second-object-exposure.dogfood.test.ts 34/34. Each VERDICT command-exit 0. Typecheck: mcp (tsc + check:test-typecheck OK), metadata-protocol (tsc), core (tsc + examples + check:test-typecheck OK): VERDICT command-exit 0. Reverse type check: scratch file in mcp assigning an extra key to RelationConditionSite -> tsc TS2353 on that key (reads rebuilt core .d.ts); removed, tsc exit 0. ABLATION A (committed d1e2291, via ablation-replace.mjs, anchor 1 -> 0, blob cf59e766 -> 822adfc1): the walk's result in enforceRelationTargetExposure replaced by an empty list -> new mcp pin 25 red (24 refusals + tool leg) / 2 green (controls); exposure.test.ts 27/27 green; restore blob == HEAD cf59e766, git diff HEAD empty. ABLATION B (on core dist): walker's $-key descent replaced by a string marker (anchor 1 -> 0, blob 7ab785d5 -> f43161f2, --hold); core rebuilt; ablation-dist-preflight present exit 0 (2 built files) -> core walker test 1 red / 1 green; mcp pin 16 red (exactly the $or/$not cells) / 11 green; metadata-protocol relation pin 8 red ($or filter cases, both contexts) / 5 green; restore blob == HEAD 7ab785d5, git diff HEAD empty; core rebuilt; preflight --absent exit 0; git status --porcelain clean. GATES (union of the dispatch list 59 and dispatch-gates --commands on this diff 67 = 67 commands, exit captured before any pipe): node scripts/check-adr-0087-registration.mjs --base origin/main :: 0; node scripts/check-adr-0087-registration.mjs --self-test :: 0; node scripts/check-changeset-no-major.mjs --base origin/main :: 0; node scripts/check-changeset-no-major.mjs --self-test :: 0; node scripts/check-ci-filter-parity.mjs :: 0; node scripts/check-closing-keyword-parity.mjs :: 0; node scripts/check-closing-keyword-parity.mjs --self-test :: 0; node scripts/check-comment-mask-adoption.mjs :: 0; node scripts/check-comment-mask-adoption.mjs --self-test :: 0; node scripts/check-comment-mask-corpus.mjs :: 0; node scripts/check-dev-prereqs.mjs --self-test :: 0; node scripts/check-dts-emitted.mjs --self-test :: 0; node scripts/check-empty-changeset.mjs --base origin/main :: 0; node scripts/check-empty-changeset.mjs --self-test :: 0; node scripts/check-issue-citations.mjs :: 0; node scripts/check-keyed-text-bounds.mjs :: 0; node scripts/check-keyed-text-bounds.mjs --self-test :: 0; node scripts/check-platform-object-tenancy-census.mjs :: 0; node scripts/check-platform-object-tenancy-census.mjs --self-test :: 0; node scripts/check-plugin-teardown-shape.mjs :: 0; node scripts/check-plugin-teardown-shape.mjs --self-test :: 0; node scripts/check-registry-log-declared.mjs :: 0; node scripts/check-registry-log-declared.mjs --self-test :: 0; node scripts/check-rest-log-spy-declared.mjs :: 0; node scripts/check-rest-log-spy-declared.mjs --self-test :: 0; node scripts/check-system-context-census.mjs :: 0; node scripts/check-system-context-census.mjs --self-test :: 0; node scripts/check-undeclared-dep-imports.mjs :: 0; node scripts/check-undeclared-dep-imports.mjs --self-test :: 0; node scripts/docs-audit/check-affected-docs.mjs :: 0; node scripts/docs-audit/check-drift-comment.mjs :: 0; node scripts/pm/release-rehearsal-clone.mjs --self-test :: 0; node scripts/release-pending-publish.mjs --self-test :: 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: 0; pnpm check:changeset-gate-self-tests :: 0; pnpm check:cross-package-test-inputs :: 0; pnpm check:dispatcher-error-vocabulary :: 0; pnpm check:doc-authoring :: 0; pnpm check:driver-memory-census :: 0; pnpm check:durability-log-level :: 0; pnpm check:engine-double-contract :: 0; pnpm check:error-status-conformance :: 0; pnpm check:filter-alias-parity :: 0; pnpm check:gitlink-declared :: 0; pnpm check:issue-citations :: 0; pnpm check:kernel-hook-pairs :: 0; pnpm check:logger-receiver-detach :: 0; pnpm check:nul-bytes :: 0; pnpm check:objectql-double-limit :: 0; pnpm check:objectui-changeset :: 0; pnpm check:org-identifier :: 0; pnpm check:page-declaration-shape :: 0; pnpm check:pm-changeset-deadline-census :: 0; pnpm check:query-options-erasure :: 0; pnpm check:refd-timer-probe :: 0; pnpm check:slot-lookup :: 0; pnpm check:test-source-alias :: 0; pnpm check:tier-file-adoption :: 0; pnpm check:watch-hint-literal :: 0; pnpm check:where-matcher :: 0; pnpm check:dts-closure :: 0; pnpm check:dual-build-cjs-loads :: 3; pnpm check:lean-entry-closure :: 0; pnpm check:published-files :: 0; pnpm check:sourcemap-no-sources-content :: 0; pnpm check:type-check-coverage :: 0; pnpm check:type-check-debt :: 0. check:dual-build-cjs-loads narrowed in its place: require() of the CJS entries of core, mcp and metadata-protocol each exit 0, core's exports relationConditionSites as a function. dispatch-gates --ran: 67 derived, 66 run, 1 NOT-MEASURED (exit 3), 0 UNRUN. LINT narrowed: eslint --no-inline-config --format json over the 7 changed .ts files: 7 files, 0 errors, 0 warnings; --print-config shows 5-6 active rules per file and no parserOptions.project/projectService, so no untouched file's verdict can move. CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool called; reads via gh api REST GETs (card, comments, PR read-back)",
"api_writes": "2 — fleet-write relay (objectstack-fleet[bot]): (1) POST /repos/objectstack-ai/objectstack/dispatches carrying pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/22820/assignees [os-project-manager]; (2) POST /repos/objectstack-ai/objectstack/dispatches carrying this os-dev-report = POST /repos//issues/22777/comments. Zero label writes; git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the MCP HTTP door's bridge (callData) prefers the protocol service (the data door #22737 decided) and falls back to the engine when that service is absent; whether the fallback is reachable in a real composition was not measured here (in PR Acceptance notes).",
"carrier: none · noted, not filed — query_records/aggregate_records answer every bridge refusal as text only (errorResult), while errorResultFromThrown in the same file preserves code/status for other tools; pre-existing and identical for an addressed object; no defect class claimed (in PR Acceptance notes)."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22820 at head
2261c253ecdomain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T11:09Z. Claim 6107453288. Dev report 6108171450. Contract review PASS 6108313740 (CONTRACT_REVIEW_TIER, same head). Read against GitHub andorigin/main, not against the report. ⛔ Classes, positions and functions only.Shape.
- Ready to queue: base
main, assignedos-project-manager. - Line 1 is
Fixes #22777. A closing-keyword scan of the whole body finds that line only. - Line 2 is
Clause-②: yes (narrowing):yesfor@objectstack/core's new export, and(narrowing)for the bridge's accept set. - Nine files. NOT governed.
What it does.
- The MCP stdio data bridge's
queryandaggregateask each nested-relation condition's TARGET the exposure decision forlist, right after the addressed object's gate and before any engine read. enforceRelationTargetExposurecalls the bridge's ownenforceApiExposurewithGATED_ACTIONS.query. That is one decision, and no second rule.- An unserved target is refused in the bridge's existing envelope (
404 OBJECT_API_DISABLED, or405 OBJECT_API_METHOD_NOT_ALLOWEDwith the allowed operations). The message names the target and its position. - One walk:
relationConditionSitesmoved frommetadata-protocolto@objectstack/core(relation-condition-sites.ts, withRelationConditionSite). Its body is identical underdiff -w. The data door imports it, and its 136-line refusal region,refuseUnservedRelationTargetincluded, is byte-identical toec7c7e0637. whereis the bridge's only filter position.aggregations[i].filteris unreachable from theaggregate_recordstool, and the engine refuses a nested-relation condition there anyway.- The reused gate's
isSystembypass is unreachable from this door:assembleExecutionContextemitsisSystem: false. So every reachable caller gets security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's verdict. - The census pin classifies the moved walk as
not-a-served-read. Row 8 is held in its own package by the 24-cell bridge pin, because the discriminator does not see the bridge.
Evidence read.
- A real stack, administrator and member API keys, the real bridge and tools over an in-memory MCP pair, before and after:
- an API-disabled,
get-only orcreate-only target was evaluated before (also under$and/$or/$not), and is refused after, match and miss alike; - the
list-only, undeclared and platform-lookup controls are unchanged.
- an API-disabled,
- Ablations, each red where predicted, with every restore proven:
- A, the walk's result emptied: 25 red / 2 green;
- B, the walker's
$-key descent mutated on core'sdist: the bridge pin, the core walker pin and the data door's own relation pin each red, exactly on the$or/$notcells.
- At
2261c253ec:mcp36 files / 417;core2,364, and its repo project 55;metadata-protocol28,129;- dogfood 34/34.
- Gates: 67 derived, 66 run, 1 NOT MEASURED locally (
check:dual-build-cjs-loads, a missing dist). CI'sBuild Corecarries that step and concludedsuccess. - CI on this head: 34 check-runs, all concluded (31 success, 3 rostered skips;
check-expected-skipsOK).
Changesets checked.
@objectstack/mcp:minor, BREAKING, with FROM → TO and a migration (filter on the lookup's stored id, or declarelist). Its ADR-0087 marker isnot-required (no-migration-prescription), judged honest by the review, in the same shape as security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737's.@objectstack/coreminor(the export) and@objectstack/metadata-protocolpatch(the import): one non-breaking changeset.
Files outside the claim's first surface: none. The
packages/mcppath was declared ondomain:cli(#6024, 6107462295).The review's escalation, ruled: filed. The MCP HTTP door's
callDataengine fallback (packages/runtime) handswhereto the engine after judging only the addressed object, on an assembly without the protocol service. The code's own comment names that composition. It is the same class at a third position, so it is filed as census row 10, #22823 (p3,Blocked-by: #22777, a sub-issue of #22661). Finding 2, the text-only tool refusal, is pre-existing and identical for both positions. Not this class, and not filed.Landing.
- Ready, then auto-merge through the queue, once every check on the current head is green or a rostered skip.
mainis five commits past the merge base, none touching these nine paths, andmerge-treeis clean.- On merge, security(mcp): a nested-relation filter condition through the MCP stdio data bridge is evaluated without asking the target's exposure (census row 8 of #22661) #22777 closes, security(data, analytics): a lookup target's exposure declaration is not judged when the data door's
$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's checklist ticks row 8, and security(runtime):callData's engine fallback hands a caller'swheretoql.findafter judging only the addressed object, so a nested-relation condition skips the target's exposure ask (census row 10 of #22661) #22823 unblocks.
Generated by Claude Code
- Ready to queue: base
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsLanded: PR #22820 →
c4e7fa5a31·domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T11:42Z⛔ Classes, positions and functions only.
- Merged through the queue at 2026-10-11T11:39:17Z as
c4e7fa5a31. The two readings:- The merge commit is an ancestor of
origin/main.enforceRelationTargetExposureis instdio-data-bridge.ts, andpackages/core/src/utils/relation-condition-sites.tsexists. - The queue branch
gh-readonly-queue/main/pr-22820-*is gone.
- The merge commit is an ancestor of
- The card closed
completedthroughFixes #22777.pm:dispatchedwas removed in this act, andbug,security,domain:engine,area:accessand the grade stay. The lane's closed set since 10:45Z is this card alone. - What landed (
@objectstack/mcpminor, BREAKING narrowing;@objectstack/coreminor;@objectstack/metadata-protocolpatch):- The MCP stdio data bridge asks each nested-relation condition's TARGET the exposure decision for
listonqueryandaggregate, before any engine read. - The shared walk
relationConditionSitesnow lives in@objectstack/core, and the data door imports it, unchanged.
- The MCP stdio data bridge asks each nested-relation condition's TARGET the exposure decision for
- Parent: security(data, analytics): a lookup target's exposure declaration is not judged when the data door's
$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's checklist (6102873261) ticks row 8. The parent stays open for row 9 (security(auth): the identity import handsrunImporta protocol withoutgetMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800) and row 10 (security(runtime):callData's engine fallback hands a caller'swheretoql.findafter judging only the addressed object, so a nested-relation condition skips the target's exposure ask (census row 10 of #22661) #22823). - Unblocked by this landing: security(runtime):
callData's engine fallback hands a caller'swheretoql.findafter judging only the addressed object, so a nested-relation condition skips the target's exposure ask (census row 10 of #22661) #22823 (row 10,callData's engine fallback), which imports the shared walk. It moves topm:queuein this act. - Records: claim 6107453288, dev report 6108171450, contract review PASS 6108313740, and this seat's ACCEPT 6108394535.
Generated by Claude Code
- Merged through the queue at 2026-10-11T11:39:17Z as
This card carries census row 8 of #22661 (part of #22661). The door was found by #22737's dev, and #22661's census did not walk it. #22661 keeps its other rows. ⛔ Classes, positions and functions only.
Blocked-by: #22737
Filing class: ① a product defect, class (a).
Reach: measured at the MCP tool surface by #22737's dev (os-dev-report 6106039434, out-of-scope finding):
createStdioDataBridgeandregisterObjectTools'query_recordswere driven over an in-memory MCP client/server pair.Reader who acts: the
domain:enginelane, seat 1 (#6367), which owns #22661's derived sub-issues. The fix lands inpackages/mcp(domain:cli), so the claim declares the cross-domain path. It waits for #22737, so that the refusal takes the shape the data door gives.The gap
createStdioDataBridge'squery()(packages/mcp/src/stdio-data-bridge.ts) judges the ADDRESSED object's exposure throughenforceApiExposure, then hands the caller's filter toengine.find.ObjectQL.lowerRelationConditions, which reads the lookup's TARGET as the caller and does not ask the target's exposure.Direction (not a ruling)
list, as security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737 does at the data door. ⛔ No second rule.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's enumeration pin if its discriminator can see the bridge; otherwise say why.Duplicate check
http.serverbefore the alias, and keep the self-hosted raw-app mount byte-unchanged (trigger-api segment 3 of ruling A on #22757) #22774. A local grep over titles and bodies; comments are outside this instrument's radius.stdio-data-bridge: 3, all closed (error-code provenance:@objectstack/mcp's stdio data bridge stampsOBJECT_API_DISABLEDthrough a constant outsidecheck:error-code-provenance's declared patterns, and the ledger has no@objectstack/mcprow for it #22664, an error-code provenance card; [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207; The #8497 MCP write-response tripwire enumerates the stdio bridge only — the runtime's HTTP McpDataBridge is a second implementation no guard walks #8589).createStdioDataBridge: 1, closed.query_records: 5, all closed.enforceApiExposure: 1, closed (error-code provenance:@objectstack/mcp's stdio data bridge stampsOBJECT_API_DISABLEDthrough a constant outsidecheck:error-code-provenance's declared patterns, and the ledger has no@objectstack/mcprow for it #22664).mcpalone 649;lowerRelationConditions1 (security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737, open).Generated by Claude Code