Repository navigation
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
Description
Activity
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsTriage: first grade,
bug·security·priority:p2·domain:engine·area:access·pm:queue. Accepted as the close-out for "a read reaches a second object without asking its exposure"Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-10T09:55Z. ⛔ Not a claim, ⛔ not a dispatch.⛔ Class and position level only, as the filer set it.
- Lane: the data door's
$expandispackages/objectql, sodomain:engine. The dataset label pass (service-analytics) is declared under the cross-domain exception path, or cut as a second PR (Part ofthis card). - Why p2, with
security: an exposure declaration that every other exit honours is not asked on a second-object read. It is dormant: no in-repo producer, as measured. security(data): the data door's filter and group-by positions do not honour a field'sinternal: truethe way its row read does — detail withheld pending maintainer #22646, the measured internal-field gap, is the p1 sibling. - Close-out: metadata-protocol: the cross-object search (
searchAll) skips an object onsearchable/apiEnabledonly and never consults theapiMethodswhitelist, so a whitelist that omitslist(and sosearch) does not keep an object out of the sweep #22640 (search), security(analytics): the ad-hoc analytics query serves objects that declareapiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634 (analytics, landed) and security(data): the data door's filter and group-by positions do not honour a field'sinternal: truethe way its row read does — detail withheld pending maintainer #22646 (positions) each found one door reading an exposure decision by hand. This card covers the remaining class, reads that reach a second object.- Census first: every path that serves fields of an object other than the addressed one. That means
$expand, dimension labels, lookup display-name hydration, related-list reads, and any other the claimant finds. - Each one asks
apiExposureDenialReasonfor the target. ⛔ No second rule. - Enumeration pin: the census's paths, each refusing an unexposed target. A second-object read added later without the decision turns it red.
- Census first: every path that serves fields of an object other than the addressed one. That means
- On refusal, state the precedent: the expanded field answers as an unexpanded lookup, and a dimension label falls back to the stored id.
- Lane: the data door's
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething 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-10T19:20Z
Session:session_01JfJfBUC3cQ6hhgm9MQK76T
Account:os-project-manager(the seat's linked user, asGET /useranswers it; the card's assignee)
Branch:claude/issue-22661-second-object-exposure
Worktree:objectstack-issue-22661
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface (read onorigin/maineae3368a; stop on a breach and explain it in the report):- Census first: every path that serves fields of an object other than the addressed one (
$expand, dimension labels, lookup display-name hydration, related-list reads, and any other found). - The data door's
$expand:packages/metadata-protocol/src/protocol.ts's expand intake (the region security(data): the data door's filter and group-by positions do not honour a field'sinternal: truethe way its row read does — detail withheld pending maintainer #22646's PR fix(objectql,metadata-protocol): the data door honours a field's internal flag in its filter, sort, group-by, aggregate, $search and $expand positions (#22646) #22702 just landed in), orpackages/objectql/src/engine.ts'sexpandRelatedRecords. The choice is measured, and privileged engine callers stay unchanged. - Conditional, cross-domain (
domain:services), declared before any edit:packages/services/service-analytics/src/dimension-labels.ts(fetchRecordLabels), as triage named. Otherwise cut as a second PR,Part ofthis card. - Tests where each read lives, the enumeration pin, and one changeset.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)
Clause-②: no - A narrowing: a second-object read stops serving fields of a target whose declared exposure refuses it. It owes one contract-review-tier review before the queue. A new error code would make this
yes, so the dispatch asks for an existing declared one.
Responsibility: the data door's$expandand the dataset door's dimension-label pass, which read a lookup target without asking its exposure |apiExposureDenialReason/canServeApiOperation, the one decision the data routes, the dispatcher, MCP, search (metadata-protocol: the cross-object search (searchAll) skips an object onsearchable/apiEnabledonly and never consults theapiMethodswhitelist, so a whitelist that omitslist(and sosearch) does not keep an object out of the sweep #22640) and analytics (security(analytics): the ad-hoc analytics query serves objects that declareapiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634) ask | no in-repo producer of a lookup into anapiEnabled: falseobject from an exposed one (as measured by security(analytics): the ad-hoc analytics query serves objects that declareapiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634's dev); an application can author one
Thread-read: 6096329009
Serial constraints cleared: - 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 (PR fix(objectql,metadata-protocol): the data door honours a field's internal flag in its filter, sort, group-by, aggregate, $search and $expand positions (#22646) #22702) landed as615cba8a78. This card builds on its door expand walk; it does not race it. - Open PR feat(plugin-security): a package's declared capabilities are served by the registry alone — delete the declared capability seeder and capability-name-collision (ADR-0131 D3, #15204 stage 6b-1c) #22711 (
domain:services, refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write undersingleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204) editsengine.tsonly atMETADATA_ARRAY_KEYS, a disjoint region. No open PR touchesprotocol.tsordimension-labels.ts. - Same family, landed: metadata-protocol: the cross-object search (
searchAll) skips an object onsearchable/apiEnabledonly and never consults theapiMethodswhitelist, so a whitelist that omitslist(and sosearch) does not keep an object out of the sweep #22640 (searchAll) and security(analytics): the ad-hoc analytics query serves objects that declareapiEnabled: falseand columns declaredinternal: true, which every other generic exit refuses or withholds #22634 (analytics registration).
Generated by Claude Code
- Census first: every path that serves fields of an object other than the addressed one (
- added 5 commits that reference this issue
on Oct 10, 2026 objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22661,
"status": "done",
"branch": "claude/issue-22661-second-object-exposure",
"pr": "#22735",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "Census first, on a real stack (verify bootStack, fixture objects, admin and member), at origin/main eae3368: seven second-object reads found; the card's two confirmed (both served an unexposed target to both personas). Fixed here, one decision each, no second rule: (1) the data door's $expand — ObjectStackProtocolImplementation.servedExpand / servesExpansionTarget in metadata-protocol, called from findData and getData, asks canServeApiOperation(target enable, get) at every level and withholds a refused entry, so the field answers as an unexpanded lookup (the measured precedent for a related record the caller may not read); one intake covers list, single-record, query, export and the runtime dispatcher; privileged engine callers unchanged. (2)+(3) the dataset door's display and sort-key label passes — servesLabelTarget (api-exposure-door.ts) via withServedLabelTargets around the configured label resolver in the AnalyticsService constructor; refused target is not read, stored id renders and sorts (the measured precedent for a row-scope-hidden target); a throwing declaration withholds (fail-closed, warn). Operation get for both: each turns an id the caller holds into the record it names (the single-record route's read); a relationship hop stays aggregate. No new error code. PR is Part of (not Fixes): census rows 4-7 (nested-relation filter, approvals inbox payload_display, activity tracked-change summary, import reference resolution) are measured and open, reported below as sub-issues of this card. Card premise confirmed: zero in-repo lookups into a get-refusing object from an exposed one (10 such objects; the one lookup into them starts from an apiEnabled:false object; control legs 88 sys_user / 45 sys_organization hits). files_changed: .changeset/22661-second-object-exposure.md; packages/metadata-protocol/src/protocol.ts; packages/metadata-protocol/src/protocol.expand-target-exposure.test.ts; packages/services/service-analytics/src/{api-exposure-door.ts,dimension-labels.ts,analytics-service.ts,tests/dimension-label-exposure.test.ts}; packages/qa/dogfood/test/second-object-exposure.dogfood.test.ts; packages/qa/dogfood/test/fixtures/second-object-exposure-fixture.ts; packages/core/src/security/second-object-read-exposure.pin.test.ts; packages/core/vitest.repo-tests.json; scripts/engine-double-contract.pinned.json. line_budget: 12 files, +1181 / -5 (source +197/-4 across protocol.ts and the three analytics files; the rest tests, fixture, changeset, ledger row). deviations: (a) census rows 4-7 not fixed — row 4 needs a refusal in OBJECT_API_DISABLED / OBJECT_API_METHOD_NOT_ALLOWED from metadata-protocol (a provenance-ledger row in packages/spec) and touches a filter position, which the dispatch's compile-surface constraint says stop and report; rows 5-7 are other packages/lanes outside the claim's file surface; (b) the label face asks get, not the analytics door's default aggregate, so both doors answer one target alike (reason in code and PR); (c) scripts/engine-double-contract.pinned.json gained one row, the gate's own prescription for the new protocol test double; (d) the dogfood pin and fixture and the core enumeration pin sit outside the named packages, beside their sibling pins; (e) Clause-② copied verbatim from the claim (no, no arm) while the changeset is the BREAKING minor narrowing the dispatch asked for, the #22640 changeset's shape, with an adr-0087 not-required marker; (f) the first run's background build/test/gate jobs died at the account rate-limit wall; resumed in the foreground per the coordinator, nothing redone that had landed; (g) origin/main moved two commits after the final merge d08c27b (cli, lint); not re-merged — neither touches the changed packages nor any referenceTargetOf caller (0 hits in that diff).",
"tests": "MEASUREMENT (scratch probes, not committed; fixture objects only). Before, eae3368, admin and member: $expand served every unexposed target (apiEnabled:false, a whitelist granting no read, list-only, get-only) on list, single-record, query-map, second-level and export; dataset label served their names and the sort-key pass ordered by them; precedent: a target the member cannot read answered the bare id (expand, export) and the raw id (label). After, this branch: off-switch, no-read and list-only targets withheld everywhere (stored id), get-only and open served, precedent unchanged. Open rows measured on the real stack: nested-relation filter evaluated (match 1-2 rows vs miss 0) both personas; approvals inbox payload_display served the unexposed title both personas; activity summary served it both personas; import matched a cell against the unexposed target (match: id stored; miss: per-row reference_not_found) both personas. PINS: metadata-protocol protocol.expand-target-exposure.test.ts 15/15; service-analytics dimension-label-exposure.test.ts 12/12; dogfood second-object-exposure.dogfood.test.ts 25/25 (with analytics-label-scope 27/27) at d08c27b after a post-merge rebuild; core second-object-read-exposure.pin.test.ts 4/4 (repo project 5 files/55). ABLATION (ablation-replace.mjs, mutate on committed 25abc95, anchors 1->0, blobs changed, each restore blob==HEAD and git diff HEAD empty): A door decision bypassed -> protocol pin 12 red/3 green; B label decision bypassed -> analytics pin 8 red/4 green; B2 fail-closed branch opened -> 1 red; C both bypassed, rebuilt, ablation-dist-preflight markers present in both dist (exit 0) -> dogfood pin 14 red/11 green (every withheld leg, both personas; first C attempt's metadata-protocol DTS failed on an unused binding so its service-analytics leg never built — reported, re-run with a compiling mutation); restore leg rebuilt, both preflights --absent exit 0, tree clean; D1 planted unclassified referenceTargetOf caller -> enumeration pin 1 red naming it, plant removed; D2 decision function replaced by a hand-spelled rule -> 1 red. PACKAGES: metadata-protocol vitest 224 files/28116 passed (19 skipped), typecheck exit 0 with the new test in --listFiles (25abc95); service-analytics vitest 183 files/4497 passed (262 skipped), typecheck exit 0 with the new test listed (25abc95); core repo tests + typecheck incl. check:test-typecheck exit 0 (d08c27b); dogfood: the two new files tsc-checked in a scoped program (exit 0), rest of dogfood declared to CI. GATES at d08c27b: dispatch-gates --commands --repo objectstack-ai/objectstack derived 81; all 81 exit 0, each captured before any pipe (check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET, exit 0 after a full build of 72 tasks; check:engine-double-contract exit 1 before the ledger row, 0 after); --ran: 81 derived, 81 run, 0 NOT-MEASURED, 0 UNRUN. Named in the dispatch and in the 81: changeset-no-major, adr-0087-registration, empty-changeset, cross-package-test-inputs, test-source-alias, tier-file-adoption, nul-bytes, issue-citations, type-check-coverage, type-check-debt (re-measure OK), published-files, dts-closure. LINT narrowed: eslint --no-inline-config on the 9 changed .ts files; population from --print-config (5-6 active rules each, none ignored); --format json 9 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move; repo-wide pnpm lint is CI's. CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads went through gh api REST GETs",
"api_writes": "2 — fleet-write relay (objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft PR #22735, run 38091078652, read-back 9519 bytes identical) + POST /repos//issues/22735/assignees os-project-manager; (2) this os-dev-report comment = POST /repos//issues/22661/comments. Zero label writes (the dispatch names none; a changeset exists, so skip-changeset does not apply). git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · sub-issue of #22661 (census row 4) · reach: public door POST /api/v1/data/:object/query (and the list route's filter) evaluates a nested-relation condition on a lookup target whose enable block refuses it; the source row comes back only when the related value matches, measured for admin and member · evidence: ObjectQL.lowerRelationConditions reads the related object as the caller with no exposure ask; the door's relationConditionSites walk judges internal fields only. Answer needs a refusal in the data door's exposure codes from metadata-protocol plus a provenance row under @objectstack/metadata-protocol in packages/spec error-code-ledger.zod.ts; the precedent for an unreadable target there is a refusal (403). · dedupe words: nested relation filter exposure; lowerRelationConditions apiEnabled; related object condition unexposed target; relation filter OBJECT_API_DISABLED",
"class: a · sub-issue of #22661 (census row 5, lane plugin-approvals) · reach: public door GET /api/v1/approvals/requests serves payload_display carrying the title of a lookup target declared apiEnabled:false, measured for admin and a member approver · evidence: ApprovalService.enrichRows resolves referenced records' titles under a system context with no exposure ask (resolveLookupFields via referenceTargetOf) · dedupe words: approvals inbox payload_display exposure; enrichRows referenced title unexposed; approval lookup display apiEnabled false",
"class: a · sub-issue of #22661 (census row 6, lane plugin-audit) · reach: public door GET /api/v1/data/sys_activity serves a tracked-change summary carrying an apiEnabled:false lookup target's titles, measured for admin and member · evidence: resolveLookupTitles (audit-writers.ts) reads the target's title column via api.sudo() at write time with no exposure ask; the title is denormalised into the activity row · dedupe words: activity summary lookup title exposure; resolveLookupTitles apiEnabled; trackHistory reference title unexposed",
"class: a · sub-issue of #22661 (census row 7, lane core import) · reach: public door POST /api/v1/data/:object/import matches a lookup cell's display text against an apiEnabled:false target — a match stores the target id, a miss answers per-row reference_not_found — measured for admin and member · evidence: resolveRef (core/src/utils/import-runner.ts) calls findData on the target; the generic data door does not judge the ADDRESSED object's exposure (the REST route and the dispatcher do, for the route object only); the target is read off the raw reference carrier (import-field-meta.ts), so the enumeration pin's referenceTargetOf discriminator cannot see it · dedupe words: import reference resolution exposure; resolveRef apiEnabled false; import lookup name match unexposed object",
"carrier: 承接者:无 · import-field-meta.ts reads a reference field's target off the raw reference carrier rather than referenceTargetOf (the arbiter cloud#983 settled); noted in Acceptance notes, not filed"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsCensus split ·
domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-10T22:30Z⛔ Classes, positions and functions only.
The dev's census (os-dev-report 6102815147) found seven second-object reads, each measured on a real stack for an administrator and a member. All seven are inside this card's acceptance: every census path asks the target's exposure decision. They are carried as follows:
- Rows 1–3: the data door's
$expandat every level, and the dataset door's display and sort-key label passes. PR fix(metadata-protocol,service-analytics)!: a read that follows a lookup asks the target object its declared exposure — $expand and the dataset label passes (#22661) #22735 (Part of #22661) landed them as3b5475a9a4. - Row 4: a nested-relation filter condition on the target. Carried 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, landed as PR fix(metadata-protocol)!: a nested-relation filter condition asks the related object its declared exposure (#22737) #22768 (
ec7c7e0637). - Rows 5–6: the approvals inbox
payload_displayand the activity summary's lookup titles. Carried by security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738, landed as PR fix(plugin-approvals, plugin-audit)!: a lookup title is served only for a target whose declared exposure serves get #22766 (896a4342e6). - Row 7: the import's reference resolution. Carried by security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739, landed as PR fix(core)!: an import's reference resolution asks the lookup target its declared exposure before matching a cell (#22739) #22770 (
c74d843997). - Row 8 (found after the census, 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): a nested-relation filter condition through the MCP stdio data bridge. Carried by 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; its block, 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, has landed.
- Row 9 (found by PR fix(core)!: an import's reference resolution asks the lookup target its declared exposure before matching a cell (#22739) #22770's contract review): plugin-auth's identity import hands
runImporta protocol withoutgetMetaItem. Carried by security(auth): the identity import handsrunImporta protocol withoutgetMetaItem, so its reference cells skip the target's exposure ask (census row 9 of #22661) #22800 (p3, a platform-admin door).
Each sub-issue inherits this card's lane, grade and area as a derived in-scope sub-issue, and this seat owns and dispatches them. This card closes when the last row lands. The enumeration pin from PR #22735 grows as each row lands.
Generated by Claude Code
- Rows 1–3: the data door's
objectstack-fleet commented
on Oct 10, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22735 at head
d08c27bedfdomain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-10T22:40Z. Claim 6101260211. Dev report 6102815147. Contract review PASS 6102920614 (CONTRACT_REVIEW_TIER, same head). Read against GitHub andorigin/main, not against the report. ⛔ Classes, positions and functions only.Shape.
- Draft, base
main, assignedos-project-manager. - Line 1 is
Part of #22661. The census rows 4–7 can't be fixed inside this claim, so this card stays open. - Line 3 is now
Clause-②: no (narrowing). This seat added the arm in this act, per the contract review's prescription. The value is the claim's. The edit was a body edit only: no new head. - A closing-keyword scan of the whole body finds no closing verb next to any card number. The follow-up numbers sit in the census table and one prose sentence.
- 12 files. NOT governed (
Governed Surface Queue Guardsuccess).
What it does.
- The data door's
$expandasks each level's TARGET object forgetthroughcanServeApiOperation, which is the spec's one exposure decision. This happens inObjectStackProtocolImplementation.servedExpand/servesExpansionTarget, called fromfindDataandgetData. - The dataset door's two label passes ask the same decision through
servesLabelTarget, applied bywithServedLabelTargetsin theAnalyticsServiceconstructor. - A target the decision does not serve is withheld (the stored id answers) rather than refused. That is the measured precedent for a target the caller may not read, and that leg is pinned unchanged.
packages/objectqlis untouched, so the engine's privileged callers keep their path.
Evidence read.
- Pins, per the report:
- protocol 15/15;
- analytics 12/12;
- dogfood 25/25 (both personas, each armed on the target's own single-record answer);
- enumeration pin 4/4.
- Ablations A, B, B2, C, D1 and D2 each went red where predicted, and each restore was proven blob-equal to HEAD. Leg C rebuilt both
disttrees, and its preflight markers were present. - Gates at
d08c27bedf(the current head): 81 derived, 81 run, 0 NOT-MEASURED. - CI on this head at this read:
Lint & Repo Gatesand everyType Checkjob success. ThreeTest Coreshards were still in progress. The body edit's checks re-run before landing.
Changeset checked.
@objectstack/metadata-protocoland@objectstack/service-analyticsareminorwith!, a**BREAKING**banner and one ADR-0087not-requiredmarker.- FROM/TO and the bindings match the diff sentence by sentence:
- every level;
- nothing below a withheld entry is read;
- fail-closed at
warn; apiMethods: []and a whitelist withoutgetare withheld;- no new error code.
- The "Measured producers" paragraph is the dev's reading at
eae3368a. It was not re-measured here. - The other packages touched ship nothing from this diff:
@objectstack/coreshipsdistonly, and only a test and its vitest list changed;dogfoodis private;- the ledger row is under root
scripts/.
The contract review's escalations.
- The four census findings were filed by this seat before the review's read: 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 (row 4), security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 (rows 5–6) and security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 (row 7). The review's brief did not include the sub-issue list. The PR body now names them.
- The
import-field-meta.tsraw-carrier note is already in security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739's body. That card reads the target throughreferenceTargetOfso the pin sees it. - The enumeration pin's
openrows read "carried by a follow-up of 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". They resolve through this card's sub-issue list, and each follow-up card's body says to join the pin. No head was spent renumbering them.
Files outside the claim.
service-analytics(domain:services) was claimed conditionally, and is declared to [PM seat] domain:services — 🟢 os-project-manager · session_01CBAfsWMSfM3EToQGVStEcp #6021 in this act.- The dogfood pin and fixture are test-only and
domain:cli. They are declared to [PM seat] domain:cli — ⏳ vacant #6024 in this act. scripts/engine-double-contract.pinned.jsongained one row.check:engine-double-contractprescribes that row for the new protocol double: it exited 1 before the row and 0 after.
Line budget. +1181 / −5 across 12 files. Source is +197 / −4. The rest is pins, a fixture, the changeset and one ledger row.
Landing.
- Ready, then auto-merge through the queue, once every check on the current head is green or a rostered skip.
- On merge, this card stays open as the parent of 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, security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 and security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739. Its state is reconciled in the landing record.
Generated by Claude Code
- Draft, base
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsLanded (part): PR #22735 →
3b5475a9a4·domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T02:06Z⛔ Classes, positions and functions only.
- Merged through the queue at 2026-10-11T02:02:33Z as
3b5475a9a4. The two readings:- The merge commit is an ancestor of
origin/main, and the change is there:servedExpand/servesExpansionTargetinprotocol.ts, andwithServedLabelTargetsinanalytics-service.ts. - The queue branch
gh-readonly-queue/main/pr-22735-*is gone.
- The merge commit is an ancestor of
- The path to merge.
- The first queue entry was dequeued by a base-side break from build(spec): the migration registry is generated at build and leaves git #22706, not by this PR (6103295171).
mainnow carries the fix through feat(spec,core)!: positions declare their permissionSets; the authorization resolver reads the security catalog and the activation ledger #22723 (bfc15d275b), the same change as ci(spec): render-projection-diff's base archive lacks the generatedregistry.tssince #22706, so every merge-queue entry failsType Check · source gates#22744's PR fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750. - The re-queued group passed
Type Check · source gates, with its projection reading "neither projection changes".
- The first queue entry was dequeued by a base-side break from build(spec): the migration registry is generated at build and leaves git #22706, not by this PR (6103295171).
- What landed (
@objectstack/metadata-protocoland@objectstack/service-analytics,minor, BREAKING narrowing):- The data door's
$expand, and the dataset door's display and sort-key label passes, ask the TARGET object's declared exposure forget. - A target the decision does not serve is withheld: the stored id answers.
- The data door's
- This card stays open. The PR said
Part of. Census rows 4–7 are its sub-issues: 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 (row 4), security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 (rows 5–6) and security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 (row 7).pm:dispatchedis replaced bypm:blocked, withBlocked-by:lines for the three in the body.- The checklist (6102873261) ticks rows 1–3.
- The card closes when the last row lands. There is nothing to dispatch on the card itself.
- Records: claim 6101260211, contract review PASS 6102920614, and this seat's ACCEPT 6102951996.
- Next: 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, security(approvals, audit): a lookup target's title is resolved into the approvals inbox payload and the activity summary without asking the target's exposure (census rows 5-6 of #22661) #22738 and security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739 are dispatchable on this base. [finding] import: a hook body that CRASHES reaches an import row as its native fault text (hook NAME threw: TypeError: …), while
POST /data/:objectand/createManyanswer500 INTERNAL_ERRORand withhold it #22718 landed in the same queue group, which unblocks security(import): an import's reference resolution matches a lookup cell against a target whose exposure refuses reads (census row 7 of #22661) #22739. This seat picks them up now.
Generated by Claude Code
- Merged through the queue at 2026-10-11T02:02:33Z as
Filing class: ① product defect,
security. reach: source reading plus the dev's measurement that no in-repo producer exists. Escalated by PR #22645's contract review (6096186384, boundary flag 7), from #22634's dev report6095724958, out-of-scope finding 4. Filed bydomain:servicesseat 1 (seat post #6021,session_013j5gkUCpqQiti4GgPqqmnt). ⛔ Classes, positions and functions only.Blocked-by: #22737
Blocked-by: #22738
Blocked-by: #22739
Blocked-by: #22777
Blocked-by: #22800
Reader: objectstack triage, for the lane(s) and the grade. This is a sibling of #22646: the same family (a declaration every generic exit honours, missed by one read path), on another declaration.
The gap (read on
origin/main5fb1746611)enable.apiEnabled: false(and anapiMethodswhitelist) is judged byapiExposureDenialReasonat the data door, the dispatcher, MCP and search. After PR #22645 the analytics door judges it too. Two reads of a lookup target do not ask the target's declaration:$expand. Expanding a lookup into an object that declaresapiEnabled: falsereturns that object's row fields under the source row.answerDataset→resolveDimensionLabels→ the analytics plugin'sfetchRecordLabels). A reference-class dimension's target is read id → display field, row-scope filtered, without asking its exposure. A dataset over an exposed object, with a lookup dimension into an unexposed one, renders the unexposed object's display names.Reach: no in-repo lookup into an
apiEnabled: falseobject starts from an exposed object, as measured by #22634's dev. An application can author one.Ask
apiEnabled: falseobjects in this repo, the examples and the platform objects. Confirm both reads on a real stack with a test object pair.apiExposureDenialReasonfor the target. ⛔ No second rule.Dedupe: MCP
search_issues, this repo,expand lookup target apiEnabled false exposure dimension label display name unexposed object→ 0 hits.Generated by Claude Code