Repository navigation
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
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 10, 2026 objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsClaim: PM loop round 2 · 2026-10-11T02:12Z
Session:session_01JfJfBUC3cQ6hhgm9MQK76T
Account:os-project-manager(the seat's linked user, asGET /useranswers it; the card's assignee)
Branch:claude/issue-22737-relation-filter-exposure
Worktree:objectstack-issue-22737
Domain:domain:engine
Seat:domain:engine#1(seat post #6367)
File surface (read onorigin/maind7b26df5b5; stop on a breach and explain it in the report):packages/metadata-protocol/src/protocol.ts: the data door's nested-relation condition walk (relationConditionSitesand its caller in the filter gate). Each condition's TARGET object is asked the exposure decision there, and a refusal uses the data door's existing declared exposure codes.packages/spec/src/api/error-code-ledger.zod.ts: at most one provenance row under@objectstack/metadata-protocol, under the pre-approved ledger-append class. The codes already exist; the row is a pure append in the appending package's own section; the contract review still runs before the queue. Anything beyond that row: stop and report.packages/core/src/security/second-object-read-exposure.pin.test.ts: census row 4's classification moves fromopento decided.- Tests where the read lives, and one changeset.
- ⛔ Not
packages/objectql: the engine's privileged callers keep their path. If a measurement says the engine must change, stop and report. - Revised in round 2 after the dev's open question (os-dev-report 6105527933). Cross-domain (
domain:cli), declared on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024:packages/types/src/data-error-classification.ts: one arm inclassifyDataErrorfor the two existing exposure codes (OBJECT_API_DISABLED,OBJECT_API_METHOD_NOT_ALLOWED). It prefers the error's ownobjectand relaysallowed, the REST door's own body shape for these codes. No new code.- Its pin, and one
@objectstack/typespatch changeset. - Test-only: legs in
packages/qa/dogfood/test/second-object-exposure.dogfood.test.ts.
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate; a contract-surface hint on the ledger row)
Clause-②: no (narrowing)
- A filter condition on a lookup target whose declaration refuses the API is refused, where it was evaluated before. That narrows the data door's accept set. It owes one contract-review-tier review before the queue.
Responsibility:ObjectQL.lowerRelationConditions, reached through the data door's filter, which evaluates a nested-relation condition on the target without asking its exposure | the spec's one exposure decision (apiExposureDenialReason/canServeApiOperation), which the data door applies to the ADDRESSED object today | any caller of the list and query routes whose filter names a condition on a lookup's related object that refuses the API; measured by 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 dev for an administrator and a member (census row 4)
Thread-read: none
Inputs read: the card body; 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 dev report 6102815147 (row 4); 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 landing record 6104471901
Serial constraints cleared: - 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 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 landed as3b5475a9a4. ItsservedExpandsits beside this walk in the door. - Open PR feat(spec,objectql,metadata-protocol,client): a write answer carries its advisory validation-rule hits as warnings #22749 touches
protocol.tsonly in the write answers (around line 13382), a different region. The dev mergesmainonce before opening the PR. - No open PR touches the relation-condition walk or the error-code ledger.
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22737,
"status": "done",
"branch": "claude/issue-22737-relation-filter-exposure",
"pr": "#22768",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "The data door's nested-relation walk (refuseInternalInFilter over relationConditionSites, called from findData's assertNoInternalFieldEvaluated) now asks each condition's TARGET the spec's one exposure decision first, through the new ObjectStackProtocolImplementation.refuseUnservedRelationTarget, which calls apiExposureDenialReason(enable, 'list'). A target that is not served is refused with the door's existing codes: 404 OBJECT_API_DISABLED, or 405 OBJECT_API_METHOD_NOT_ALLOWED with the effective set. The decision takes no caller. No new code, no second rule, and no change to filter compilation or the engine. PM hypotheses: (1) confirmed by symbol. relationConditionSites and its caller judged internal fields only. lowerRelationConditions runs this.find(target, where: condition, limit: cap+1) with the caller context and no exposure ask. (2) Confirmed on a real stack (bootStack, base bf515e7), on the list and query routes, admin and member alike. Targets apiEnabled:false, apiMethods ['create'], [] and ['get'] were evaluated: 1 row on a match, 0 on a miss. After the change: 404, 405, 405 and 405. The list-only and open controls are unchanged. (3) Operation list, not get. Unlike $expand, a condition is a predicate read over the whole target, bounded by the match cap rather than by ids the caller holds, which is the read the target's filtered list performs (query maps to list). So a get-only target is refused here while it stays served to $expand. (4) Refused, not withheld. The precedent was measured: a related object the member cannot read answers 403 PERMISSION_DENIED at this position, and this is pinned unchanged. The refusal names the target object and the position in its message. The target is the lookup's declared reference, which the source's served /meta object read already shows to both personas, and the internal-field refusal at the same hop names it too. (5) The ledger row is required: ablation D removed it and check:error-code-provenance FAILed on both stamp sites. files_changed: packages/metadata-protocol/src/protocol.ts (+57/-4); packages/metadata-protocol/src/protocol.relation-condition-target-exposure.test.ts (new, +126); packages/spec/src/api/error-code-ledger.zod.ts (+7); packages/core/src/security/second-object-read-exposure.pin.test.ts (+10/-9, row 4 open to decided, operation type widened to get|list); packages/qa/dogfood/test/second-object-exposure.dogfood.test.ts (+43); .changeset/22737-relation-condition-target-exposure.md (metadata-protocol minor, BREAKING narrowing, Clause-② no (narrowing), adr-0087 not-required (no-migration-prescription)); .changeset/22737-spec-ledger-metadata-protocol-exposure-codes.md (spec patch). line_budget: 7 files, +281/-13 = 294 changed lines, over the 250 suggestion (source +57/-4, within 80). The excess is the second changeset and the real-stack legs. deviations: (a) The ledger change is ONE provenance block in metadata-protocol's own section carrying the TWO existing exposure codes as two array entries. I read the claim's "one row" that way because the claim names the codes in the plural and both sibling sections (service-analytics, mcp) take the same two-entry shape. It is a pure append, with no schema shape change and no existing entry touched. If the seat reads "one row" as one array entry, this exceeds it. (b) Two changesets, against the claim's "one changeset". spec's published ERROR_CODE_LEDGER value changes, which the analytics precedent covered with its own spec patch changeset (22634-spec-ledger-analytics-exposure-codes.md). (c) The real-stack legs (admin and member, list and query routes, plus the 403 precedent) were added to #22661's dogfood pin, outside the named file surface, beside their sibling legs. (d) The base was bf515e7 at worktree creation, not d7b26df. origin/main e865300 (4 commits: spec formula, plugin-security, plugin-auth, plugin-sharing; no overlap with the changed files) was merged once before the PR as 151278b. The closure was rebuilt and spec check:generated reports 14 artifacts up to date. (e) The flat REST body's object member names the route object, not the refused target. That is the mapDataError declared-status passthrough in @objectstack/types, which is outside the file surface, so it is not fixed (see open_questions). (f) The harness attribution reminder named a model in the Co-Authored-By trailer. The commits carry AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) instead. (g) Zero label writes: the dispatch names no label, and changesets exist, so skip-changeset does not apply. Worktree removed after the PR opened.",
"tests": "MEASUREMENT (scratch probe, not committed; fixture objects only; real stack via @objectstack/verify bootStack). The before and after tables are in the summary and the PR. PINS at 151278b (merge head): metadata-protocol protocol.relation-condition-target-exposure.test.ts with protocol.expand-target-exposure.test.ts: 2 files, 28/28. core vitest --project repo: 5 files, 55/55, including second-object-read-exposure.pin.test.ts 4/4. dogfood second-object-exposure.dogfood.test.ts: 30/30 (25 existing plus 5 new). PACKAGES at fe8d208: metadata-protocol vitest 225 files passed, 3 skipped, 28129 tests passed, 19 skipped, VERDICT command-exit 0. metadata-protocol tsc --noEmit --listFiles exit 0, new test listed, 0 errors. core typecheck (tsc, examples, check:test-typecheck OK) exit 0. dogfood tsc --noEmit --listFiles exit 0 at 151278b, changed file listed, 0 errors. ABLATION, on committed fe8d208 via scripts/ablation-replace.mjs, each restore proved by blob == HEAD and an empty git diff HEAD. A: the hop call deleted (anchor 1 to 0, blob bef97743 to 19d384c8); the unit pin went 10 red / 3 green (the three controls). B: the call replaced by a compiling marker (void this.refuseUnservedRelationTarget; plus a globalThis marker); metadata-protocol rebuilt, exit 0; ablation-dist-preflight marker present in dist (exit 0); the dogfood pin went 2 red (the refused legs, admin and member) / 28 green. Restore leg: rebuilt exit 0, preflight --absent exit 0. The first B attempt used a non-compiling mutation (an unused private method): build exit 1, with the JS marker still emitted. It was re-run, and the numbers above come from the re-run. C: the decision call replaced by a hand-spelled apiEnabled rule; the enumeration pin went 1 red, naming refuseUnservedRelationTarget. D: the two ledger entries deleted; check:error-code-provenance FAIL naming protocol.ts:12628 and :12631. The first D attempt was refused by the tool (the replacement was a substring of the anchor), so nothing ran; it was re-run in delete mode. GATES at 151278b (final commit): the union of the dispatch list (82) and dispatch-gates --commands on this diff (91) is 95 commands, with each exit captured before any pipe. dispatch-gates --ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN, 4 outside the derivation. check:dual-build-cjs-loads: the first run at fe8d208 was exit 3 PREREQUISITE NOT MET (8 dists missing); after building them it was exit 0, and exit 0 again at 151278b. Per command: 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-engine-split-ratio.mjs --days 90 :: 0; node scripts/check-engine-split-ratio.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-spec-docblock-symbol-anchors.mjs :: 0; node scripts/check-spec-docblock-symbol-anchors.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/lint run check:doc-formula-expressions :: 0; pnpm --filter @objectstack/spec run check:api-surface :: 0; pnpm --filter @objectstack/spec run check:authorable-surface :: 0; pnpm --filter @objectstack/spec run check:browser-reachable-entries :: 0; pnpm --filter @objectstack/spec run check:docs :: 0; pnpm --filter @objectstack/spec run check:dual-source-exports :: 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: 0; pnpm --filter @objectstack/spec run check:empty-state :: 0; pnpm --filter @objectstack/spec run check:entry-nameability :: 0; pnpm --filter @objectstack/spec run check:error-code-provenance :: 0; pnpm --filter @objectstack/spec run check:export-origins :: 0; pnpm --filter @objectstack/spec run check:exported-any :: 0; pnpm --filter @objectstack/spec run check:liveness :: 0; pnpm --filter @objectstack/spec run check:llms-txt :: 0; pnpm --filter @objectstack/spec run check:objectui-pin-citations :: 0; pnpm --filter @objectstack/spec run check:skill-refs :: 0; pnpm --filter @objectstack/spec run check:strictness-ledger :: 0; pnpm --filter @objectstack/spec run check:variant-docs :: 0; pnpm --filter @objectstack/spec run check:yaml-examples :: 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:dts-closure :: 0; pnpm check:dual-build-cjs-loads :: 0; pnpm check:durability-log-level :: 0; pnpm check:engine-double-contract :: 0; pnpm check:error-code-casing :: 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:lean-entry-closure :: 0; pnpm check:logger-receiver-detach :: 0; pnpm check:merge-driver :: 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:pm-prior-rulings :: 0; pnpm check:published-files :: 0; pnpm check:query-options-erasure :: 0; pnpm check:refd-timer-probe :: 0; pnpm check:slot-lookup :: 0; pnpm check:sourcemap-no-sources-content :: 0; pnpm check:spec-parsed-alias :: 0; pnpm check:stack-collection-maps :: 0; pnpm check:swallow-census-controls :: 0; pnpm check:test-source-alias :: 0; pnpm check:tier-file-adoption :: 0; pnpm check:type-check-coverage :: 0; pnpm check:type-check-debt :: 0; pnpm check:watch-hint-literal :: 0; pnpm check:where-matcher :: 0; LINT narrowed: eslint --no-inline-config --format json on the 5 changed .ts files: 5 files, 0 errors, 0 warnings. --print-config shows 5 active rules per file, none ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project or projectService), 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 (issue, comments, PR read-back)",
"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 #22768, run 38112298274, read-back 9889 bytes identical) plus POST /repos//issues/22768/assignees os-project-manager. (2) This os-dev-report comment = POST /repos//issues/22737/comments. Zero label writes. git push not counted.",
"open_questions": [
{
"question": "The flat REST body's object member names the ROUTE object, not the refused target. For this refusal the wire reads code OBJECT_API_DISABLED (or 405) beside the SOURCE object's name, while the message names the target, and allowed does not reach that body. Cause: mapDataError's declared-status passthrough (@objectstack/types data-error-classification.ts) stamps the route object onto every relayed 4xx; only per-code arms (INVALID_FIELD, FEEDS_DISABLED, RECORD_NOT_ACCESSIBLE) prefer error.object. Widen the claim to close it here, or leave it to a follow-up?",
"options": [
"A — accept as is: object keeps meaning the addressed object on every generic passthrough refusal, and the target is named in prose only. Zero cost. A client or agent keying on code+object reads the wrong object as switched off.",
"B — one arm in classifyDataError for the two exposure codes that prefers error.object and relays allowed, the REST door's own body shape for these codes. About 15 lines in @objectstack/types plus a pin and a types patch changeset. It widens this PR's file surface to packages/types, or it becomes a follow-up card."
],
"recommendation": "B, as a rider on this PR if the seat widens the claim before the contract review, else as a follow-up card. Real need: the pair is new with this PR, and an AI caller acting on code+object would try to change the wrong object's enable block. Long term: one meaning per code across arms, the established pattern for target-naming refusals. AI-error axis: the loud, correct envelope is the one an agent cannot misread. Startup scope: one arm, no new code and no new gate."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · the MCP stdio data bridge (packages/mcp/src/stdio-data-bridge.ts) hands a caller's filter to engine.find after its own addressed-object gate, so a nested-relation condition there reaches ObjectQL.lowerRelationConditions without the target's exposure being asked. Not measured at a door in this card; noted in Acceptance notes, not filed · dedupe words: mcp stdio nested relation filter exposure; stdio-data-bridge relation condition apiEnabled; mcp query lookup condition unexposed target"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22737,
"status": "done",
"branch": "claude/issue-22737-relation-filter-exposure",
"pr": "#22768",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "PATCH ROUND 2 (seat ruling: option B as a rider). (1) The arm: @objectstack/types data-error-classification.ts gains one arm for OBJECT_API_DISABLED and OBJECT_API_METHOD_NOT_ALLOWED. It sits in structuredCodeAnswer, the shared arm table classifyDataError asks first, so both REST doors (mapDataError, and resolveErrorResponse behind sendThrownError) answer alike. The door-parity rule of that file requires this; an arm in classifyDataError's own body would split the doors. When the error names an object other than a DEFINED route object, the body is error, code, object (the error's own) and allowed (on 405): the REST door's own shape for these codes. No new code. The gate is narrower than "prefer error.object" because the seat's premise ("all existing producers have error.object equal to the route object") was measured false. The one existing thrower that reaches these doors is service-analytics' exposure door on the dataset route. It goes through classifiedRefusalAnswer, which calls resolveErrorResponse with NO route object. A plain prefer-error.object arm would have added object and allowed to that existing body. Measured before and after on both doors plus classifiedRefusalAnswer, 6 shapes (404 and 405, each with the error object differing from the route, equal to it, or no route): only the 4 cells where the error object differs from a defined route changed. The other 14 are byte-identical (JSON string compare). (2) Pins. packages/types/src/data-error-classification.exposure-arm.test.ts (new, 4 cases): target named on 404; target plus allowed on 405; CONTROL route-object error byte-identical; CONTROL no-route door byte-identical. Dogfood second-object-exposure, +2 cases per persona: the refusal body's status, code, object and allowed equal the target's own route refusal, with object being the target; CONTROL the target's own route refusal names the target, unchanged. Two existing drift guards in packages/rest demand coverage of every new arm, so each gained test-only rows: 2 CASES in error-response-structured-arm-door-parity.test.ts and 2 ARMS in error-response-sandbox-arm-message.test.ts (plus that file's re-derivation count, now 16/13). These sit outside the revised surface text and are declared here. (3) Changeset .changeset/22737-types-exposure-arm-names-target.md (@objectstack/types patch). (4) MCP stdio reading: see out_of_scope_findings; nothing changed in packages/mcp. (5) origin/main 9859e4e (6 commits, including one to packages/rest/src/error-response.ts that adds nestedRefusalEnvelope over classifiedRefusalAnswer with no route object, so the arm does not fire there) was merged as 964acef. The closure was rebuilt and spec check:generated reports 14 artifacts up to date. PR BODY NOT PATCHED: the os-dev definition says the dev writes the PR body once, at pr_create, and names later edits for the seat to write. That conflicts with the seat's instruction 3; flagged rather than resolved silently. Edits for the seat to write, verbatim: (i) the Acceptance-notes bullet on the flat body's object member becomes: "Fixed in this PR: the flat REST body named the route object beside the exposure code. One arm in @objectstack/types structuredCodeAnswer (asked first by classifyDataError, so both REST doors agree) now answers with the error's own object, and relays allowed on 405, when the error names an object other than a defined route object. An error naming the route object, or a door given none (the analytics dataset route), answers byte for byte as before." (ii) Deviation (c) becomes: "The diff is 472 changed lines (+456 / -16) across 12 files: source +57/-4 (protocol.ts) and +23 (data-error-classification.ts); the rest is tests, three changesets, the pin row and the ledger rows." (iii) Verification gains: "Round 2 at 964acef: types 27 files / 754 passed plus repo 11; rest local 272 files / 5239 passed (run in three chunks) plus repo 5 files / 209; dogfood pin 34/34; ablation E (arm deleted, types rebuilt, preflight --absent) reds the types pin 2 / 4, the rest guards 8 / 210 and the dogfood target leg 2 / 34, with controls green; restored blob == HEAD; 95 gates exit 0." files_changed (whole PR): the 7 from round 1 (dogfood now +66), plus packages/types/src/data-error-classification.ts (+23), packages/types/src/data-error-classification.exposure-arm.test.ts (new, +67), packages/rest/src/error-response-sandbox-arm-message.test.ts (+19/-3), packages/rest/src/error-response-structured-arm-door-parity.test.ts (+32) and .changeset/22737-types-exposure-arm-names-target.md (+11). line_budget: 12 files, +456/-16 = 472 changed lines; source +80/-4 (protocol.ts +57/-4, data-error-classification.ts +23). deviations this round: the arm's position and gate (above); the rest drift-guard rows (above); the PR body not patched (above); the rest local suite run in three file chunks of 88, 93 and 91, which is complete coverage and not a narrowing, because a single run exceeded the foreground cap while the gate battery shared the box. Accepted round-1 deviations (a)(b)(c)(d)(f)(g) are unchanged.",
"tests": "ROUND 2, at 964acef (final commit, origin/main 9859e4e merged). types vitest local 27 files / 754 passed; repo 1 file / 11. rest vitest repo 5 files / 209 passed, 1 skipped. rest local, three chunks: 88 files / 1313 passed, 281 skipped; 93 / 1981, 45 skipped; 91 / 1945. Total 272 files / 5239 passed, every chunk VERDICT command-exit 0. metadata-protocol unit pins 2 files / 28. core repo 5 files / 55. dogfood second-object-exposure 34/34. Typecheck: types tsc --noEmit (new test in --listFiles), rest typecheck (tsc plus check:test-typecheck OK) and dogfood tsc --noEmit, all exit 0 with 0 errors. DOORS before/after (scratch, not committed): 18 cells. Only 404-differs and 405-differs changed, on mapDataError and on sendThrownError, from object = route to object = target, with allowed ["get"] on 405. The equal, no-route and classifiedRefusalAnswer cells are byte-identical. ABLATION E, on committed 5c17272 via ablation-replace.mjs: the arm deleted (anchor 1 to 0, blob 61b0d967 to 6f20e4cc); types rebuilt, exit 0; ablation-dist-preflight --absent exit 0. Results: types pin 2 red (target cases) / 2 green (controls); rest repo 8 red (the new door-parity and sandbox rows) / 201 green; dogfood 2 red (the target-body leg, admin and member) / 32 green (code+status legs and both controls). Restore: blob == HEAD, git diff HEAD empty; types rebuilt exit 0; the preflight's dist reading has the marker present. Its tree reading exited 3, because an untracked scratch probe was then present in the worktree (since removed); the dist reading is the one the suite consumes. Round-1 ablations A–D stand as reported in comment 6105527933. GATES at 964acef: union of the dispatch list (82) and dispatch-gates --commands on this diff (91; the new types and rest paths added no family) = 95 commands, all exit 0, each captured before any pipe. dispatch-gates --ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN, 4 outside. Per command: 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-engine-split-ratio.mjs --days 90 :: 0; node scripts/check-engine-split-ratio.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-spec-docblock-symbol-anchors.mjs :: 0; node scripts/check-spec-docblock-symbol-anchors.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/lint run check:doc-formula-expressions :: 0; pnpm --filter @objectstack/spec run check:api-surface :: 0; pnpm --filter @objectstack/spec run check:authorable-surface :: 0; pnpm --filter @objectstack/spec run check:browser-reachable-entries :: 0; pnpm --filter @objectstack/spec run check:docs :: 0; pnpm --filter @objectstack/spec run check:dual-source-exports :: 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: 0; pnpm --filter @objectstack/spec run check:empty-state :: 0; pnpm --filter @objectstack/spec run check:entry-nameability :: 0; pnpm --filter @objectstack/spec run check:error-code-provenance :: 0; pnpm --filter @objectstack/spec run check:export-origins :: 0; pnpm --filter @objectstack/spec run check:exported-any :: 0; pnpm --filter @objectstack/spec run check:liveness :: 0; pnpm --filter @objectstack/spec run check:llms-txt :: 0; pnpm --filter @objectstack/spec run check:objectui-pin-citations :: 0; pnpm --filter @objectstack/spec run check:skill-refs :: 0; pnpm --filter @objectstack/spec run check:strictness-ledger :: 0; pnpm --filter @objectstack/spec run check:variant-docs :: 0; pnpm --filter @objectstack/spec run check:yaml-examples :: 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:dts-closure :: 0; pnpm check:dual-build-cjs-loads :: 0; pnpm check:durability-log-level :: 0; pnpm check:engine-double-contract :: 0; pnpm check:error-code-casing :: 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:lean-entry-closure :: 0; pnpm check:logger-receiver-detach :: 0; pnpm check:merge-driver :: 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:pm-prior-rulings :: 0; pnpm check:published-files :: 0; pnpm check:query-options-erasure :: 0; pnpm check:refd-timer-probe :: 0; pnpm check:slot-lookup :: 0; pnpm check:sourcemap-no-sources-content :: 0; pnpm check:spec-parsed-alias :: 0; pnpm check:stack-collection-maps :: 0; pnpm check:swallow-census-controls :: 0; pnpm check:test-source-alias :: 0; pnpm check:tier-file-adoption :: 0; pnpm check:type-check-coverage :: 0; pnpm check:type-check-debt :: 0; pnpm check:watch-hint-literal :: 0; pnpm check:where-matcher :: 0; LINT narrowed: eslint --no-inline-config --format json over the 9 changed .ts files: 9 files, 0 errors, 0 warnings. --print-config shows 5 or 6 active rules per file, none ignored, no parserOptions.project or projectService, so no untouched file's verdict can move. CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads through gh api REST GETs (claim comment, PR state)",
"api_writes": "Round 2: 1 — fleet-write relay (objectstack-fleet[bot]), POST /repos/objectstack-ai/objectstack/dispatches carrying this os-dev-report comment = POST /repos//issues/22737/comments. Round 1's 2 writes as reported. No PR-body PATCH, zero label writes; git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: measured at the MCP stdio tool surface, not the byte transport. The real createStdioDataBridge and registerObjectTools query_records were driven over an in-memory MCP client/server pair, under an admin API key resolved through plugin.ts's own chain (resolveAuthzContext plus assembleExecutionContext, non-system). On the fixture objects, a nested-relation condition on an apiEnabled:false lookup target answered 1 record on a match and 0 on a miss (control: 2 unfiltered), while query_records addressed to the target itself is refused "not exposed via the API". Not exercised: StdioServerTransport and the process boundary, and the localization memo. · evidence: packages/mcp/src/stdio-data-bridge.ts query() passes the caller's where to engine.find after enforceApiExposure judges the ADDRESSED object only; ObjectQL.lowerRelationConditions then reads the target as the caller with no exposure ask. Same class as this card, on the stdio door; not changed here (packages/mcp untouched). · dedupe words: mcp stdio nested relation filter exposure; stdio-data-bridge relation condition apiEnabled; query_records lookup condition unexposed target"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22737,
"status": "done",
"branch": "claude/issue-22737-relation-filter-exposure",
"pr": "#22768",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "PATCH ROUND 3 (mechanical merge). origin/main e84aeb3 (6 commits: #22767, #22766 896a434, #22749 490cb6d, #22736, #22765, #22753) merged into the branch as merge commit 04cf472 (parents 964acef and e84aeb3). No rebase, no force-push; pushed as a fast-forward of the branch. Conflicted file: one, packages/core/src/security/second-object-read-exposure.pin.test.ts, in one hunk: the expected list of decided decision functions. The CALLERS rows auto-merged. packages/metadata-protocol/src/protocol.ts auto-merged with #22749; refuseUnservedRelationTarget and its hop call are intact (4 occurrences). Union as resolved: decided = refuseUnservedRelationTarget (census row 4, operation list), servesExpansionTarget, servesLabelTarget, servesPayloadDisplayTarget (row 5, get) and servesSummaryTitleTarget (row 6, get). The Classification type keeps operation get|list. No CALLERS row is left kind open. Census row 7 (the import door, #22739) is not a CALLERS row: its target is not resolved through referenceTargetOf. It stays where main has it, in the header's not-seen list, unchanged. The file's diff against origin/main is exactly this branch's round-1 change: the header sentence, the type union, row 4 decided, and refuseUnservedRelationTarget added to the expected list. The merge commit message was amended once before the push, to keep the trailer pair as the last paragraph (git had appended the conflict comment lines after it). Nothing else was changed. line_budget vs the new merge base e84aeb3: 12 files, +456/-15 = 471 changed lines.",
"tests": "ROUND 3 at 04cf472. #22749 and #22766 touch spec, objectql, metadata-protocol, plugin-approvals and plugin-audit, all in the dogfood closure, so the closure was rebuilt first: turbo build of the dogfood dependencies plus rest, 63 tasks, VERDICT command-exit 0. The 8 dists the dual-build gate reads plus cli were built too (67 tasks, exit 0), and spec check:generated reports 14 artifacts up to date. core vitest --project repo: 5 files / 55 passed, including second-object-read-exposure.pin.test.ts. core typecheck (tsc, examples, check:test-typecheck OK) exit 0. metadata-protocol protocol.relation-condition-target-exposure.test.ts with protocol.expand-target-exposure.test.ts: 2 files / 28 passed. dogfood second-object-exposure.dogfood.test.ts: 34/34. Extra: types exposure-arm pin 4/4; rest repo 5 files / 209 passed, 1 skipped. All in one locked run, VERDICT command-exit 0. GATES at 04cf472: dispatch-gates --commands on the merged change set (12 paths vs merge base e84aeb3) derives 91. The union with the dispatch list is 95 commands, all exit 0, each captured before any pipe. --ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. Per command: 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-engine-split-ratio.mjs --days 90 :: 0; node scripts/check-engine-split-ratio.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-spec-docblock-symbol-anchors.mjs :: 0; node scripts/check-spec-docblock-symbol-anchors.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/lint run check:doc-formula-expressions :: 0; pnpm --filter @objectstack/spec run check:api-surface :: 0; pnpm --filter @objectstack/spec run check:authorable-surface :: 0; pnpm --filter @objectstack/spec run check:browser-reachable-entries :: 0; pnpm --filter @objectstack/spec run check:docs :: 0; pnpm --filter @objectstack/spec run check:dual-source-exports :: 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: 0; pnpm --filter @objectstack/spec run check:empty-state :: 0; pnpm --filter @objectstack/spec run check:entry-nameability :: 0; pnpm --filter @objectstack/spec run check:error-code-provenance :: 0; pnpm --filter @objectstack/spec run check:export-origins :: 0; pnpm --filter @objectstack/spec run check:exported-any :: 0; pnpm --filter @objectstack/spec run check:liveness :: 0; pnpm --filter @objectstack/spec run check:llms-txt :: 0; pnpm --filter @objectstack/spec run check:objectui-pin-citations :: 0; pnpm --filter @objectstack/spec run check:skill-refs :: 0; pnpm --filter @objectstack/spec run check:strictness-ledger :: 0; pnpm --filter @objectstack/spec run check:variant-docs :: 0; pnpm --filter @objectstack/spec run check:yaml-examples :: 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:dts-closure :: 0; pnpm check:dual-build-cjs-loads :: 0; pnpm check:durability-log-level :: 0; pnpm check:engine-double-contract :: 0; pnpm check:error-code-casing :: 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:lean-entry-closure :: 0; pnpm check:logger-receiver-detach :: 0; pnpm check:merge-driver :: 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:pm-prior-rulings :: 0; pnpm check:published-files :: 0; pnpm check:query-options-erasure :: 0; pnpm check:refd-timer-probe :: 0; pnpm check:slot-lookup :: 0; pnpm check:sourcemap-no-sources-content :: 0; pnpm check:spec-parsed-alias :: 0; pnpm check:stack-collection-maps :: 0; pnpm check:swallow-census-controls :: 0; pnpm check:test-source-alias :: 0; pnpm check:tier-file-adoption :: 0; pnpm check:type-check-coverage :: 0; pnpm check:type-check-debt :: 0; pnpm check:watch-hint-literal :: 0; pnpm check:where-matcher :: 0; PR #22768 read back after the push: draft, head 04cf472, mergeable_state blocked (no longer dirty). CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads through gh api REST GETs (PR state)",
"api_writes": "Round 3: 1 — fleet-write relay (objectstack-fleet[bot]), POST /repos/objectstack-ai/objectstack/dispatches carrying this os-dev-report comment = POST /repos//issues/22737/comments. git push not counted. No PR-body or label write.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22737,
"status": "done",
"branch": "claude/issue-22737-relation-filter-exposure",
"pr": "#22768",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id, inherited by this subagent",
"premise_still_valid": true,
"summary": "PATCH ROUND 4 (one merge after #22770, as instructed). A bounded background poller (60-minute deadline) was awaited in the foreground with tail --pid. #22770 merged_at = 2026-10-11T07:25:41Z, so no timeout fallback was needed. origin/main c74d843 (6 commits since the last merge, including #22770 c74d843 for #22739) was merged into the branch as merge commit 606e50f (parents 04cf472 and c74d843). No rebase, no force-push; fast-forward push. Conflicted file: one, packages/core/src/security/second-object-read-exposure.pin.test.ts, in one hunk: the header sentence. The decided list, the operation type and the CALLERS rows auto-merged. Both sides had widened operation to get|list identically, and the list merged to the six-entry union. Header resolved as the union: it names the $expand, the dataset label passes, the import door (#22739) and the nested-relation filter condition (#22737), and keeps main's wording "Each read classified decided below now asks the target". Union as committed: decided = refuseUnservedRelationTarget (row 4, list), servesExpansionTarget, servesLabelTarget, servesPayloadDisplayTarget (row 5, get), servesReferenceTarget (row 7, list) and servesSummaryTitleTarget (row 6, get). Zero CALLERS rows are kind open. The file's diff against origin/main is exactly this branch's change: the header clause, row 4 decided, and refuseUnservedRelationTarget in the expected list. Nothing else was changed. line_budget vs merge base c74d843: 12 files, +457/-16 = 473 changed lines. Note: origin/main moved again afterwards to 5fc57b3 (6 commits: #22779, #22798, #22796, #22784, #22791, #22746). None of them touches any of this branch's 12 paths, so it was not merged again (one merge, as instructed). PR mergeable_state read unknown, i.e. still computing, right after the push.",
"tests": "ROUND 4 at 606e50f. The merge brings core (import-runner), plugin-audit, spec, plugin-approvals, rest and verify changes, all in the dogfood closure, so the closure was rebuilt: turbo build of the dogfood dependencies plus rest, 63 tasks, VERDICT command-exit 0. The dual-build dists plus cli and skills were built too (68 tasks); spec check:generated reports 14 artifacts up to date (re-run alone after the first attempt hit the foreground cap). core vitest --project repo: 5 files / 55 passed, including second-object-read-exposure.pin.test.ts. core typecheck (tsc, examples, check:test-typecheck OK) exit 0. metadata-protocol protocol.relation-condition-target-exposure.test.ts with protocol.expand-target-exposure.test.ts: 2 files / 28 passed. dogfood second-object-exposure.dogfood.test.ts: 34/34. Extra: types exposure-arm pin 4/4; rest repo 5 files / 209 passed, 1 skipped. All in one locked run, VERDICT command-exit 0. GATES at 606e50f: dispatch-gates --commands on the merged change set (12 paths vs merge base c74d843) derives 91. The union with the dispatch list is 95 commands, all exit 0, each captured before any pipe. --ran: 91 derived, 91 run, 0 NOT-MEASURED, 0 UNRUN. Per command: 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-engine-split-ratio.mjs --days 90 :: 0; node scripts/check-engine-split-ratio.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-spec-docblock-symbol-anchors.mjs :: 0; node scripts/check-spec-docblock-symbol-anchors.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/lint run check:doc-formula-expressions :: 0; pnpm --filter @objectstack/spec run check:api-surface :: 0; pnpm --filter @objectstack/spec run check:authorable-surface :: 0; pnpm --filter @objectstack/spec run check:browser-reachable-entries :: 0; pnpm --filter @objectstack/spec run check:docs :: 0; pnpm --filter @objectstack/spec run check:dual-source-exports :: 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: 0; pnpm --filter @objectstack/spec run check:empty-state :: 0; pnpm --filter @objectstack/spec run check:entry-nameability :: 0; pnpm --filter @objectstack/spec run check:error-code-provenance :: 0; pnpm --filter @objectstack/spec run check:export-origins :: 0; pnpm --filter @objectstack/spec run check:exported-any :: 0; pnpm --filter @objectstack/spec run check:liveness :: 0; pnpm --filter @objectstack/spec run check:llms-txt :: 0; pnpm --filter @objectstack/spec run check:objectui-pin-citations :: 0; pnpm --filter @objectstack/spec run check:skill-refs :: 0; pnpm --filter @objectstack/spec run check:strictness-ledger :: 0; pnpm --filter @objectstack/spec run check:variant-docs :: 0; pnpm --filter @objectstack/spec run check:yaml-examples :: 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:dts-closure :: 0; pnpm check:dual-build-cjs-loads :: 0; pnpm check:durability-log-level :: 0; pnpm check:engine-double-contract :: 0; pnpm check:error-code-casing :: 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:lean-entry-closure :: 0; pnpm check:logger-receiver-detach :: 0; pnpm check:merge-driver :: 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:pm-prior-rulings :: 0; pnpm check:published-files :: 0; pnpm check:query-options-erasure :: 0; pnpm check:refd-timer-probe :: 0; pnpm check:slot-lookup :: 0; pnpm check:sourcemap-no-sources-content :: 0; pnpm check:spec-parsed-alias :: 0; pnpm check:stack-collection-maps :: 0; pnpm check:swallow-census-controls :: 0; pnpm check:test-source-alias :: 0; pnpm check:tier-file-adoption :: 0; pnpm check:type-check-coverage :: 0; pnpm check:type-check-debt :: 0; pnpm check:watch-hint-literal :: 0; pnpm check:where-matcher :: 0; CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool was called; reads through gh api REST GETs (PR 22770 merge state, PR 22768 state)",
"api_writes": "Round 4: 1 — fleet-write relay (objectstack-fleet[bot]), POST /repos/objectstack-ai/objectstack/dispatches carrying this os-dev-report comment = POST /repos//issues/22737/comments. git push not counted. No PR-body or label write.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsACCEPT (seat review): PR #22768 at head
606e50f7efdomain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T08:36Z. Claim 6104514778, with its round-2 revision. Dev reports 6105527933 (round 1), 6106039434 (round 2, the seat-ruled rider), 6106452488 and 6107096768 (rounds 3 and 4, merges ofmainonly). Contract review PASS 6106147584 (CONTRACT_REVIEW_TIER) on964aceffd6, and the supplementary record PASS 6107185299 on this head. Read against GitHub andorigin/main, not against the reports. ⛔ Classes, positions and functions only.Shape.
- Ready to queue: base
main, assignedos-project-manager,mergeable_stateclean. - Line 1 is
Fixes #22737. A closing-keyword scan of the whole body finds that line only. - Line 2 is
Clause-②: no (narrowing), and each of the three changesets agrees. - 12 files. NOT governed.
What it does.
- The data door's nested-relation walk asks each condition's TARGET object the spec's one exposure decision, for
list, before anything of it is judged. The new method isrefuseUnservedRelationTarget, and it callsapiExposureDenialReason. - The operation is
list, not$expand'sget. A condition is a predicate over the whole target, which is the read the target's own filtered list performs. Aget-only target is refused here and stays served to$expand. - An unserved target is refused, not withheld, because dropping a condition widens the result. The answer is the door's existing pair:
404 OBJECT_API_DISABLED, or405 OBJECT_API_METHOD_NOT_ALLOWEDwithallowed. A member's403at the same position is the precedent, and it is pinned unchanged. - It covers every position the walk covers:
where, thefilteralias, each aggregation'sfilter(before the grouped branch forks), and eachexpandentry's ownwhereat every level. - The engine and the filter compilation are untouched, so the engine's privileged callers keep their path.
- The rider, ruled in round 2: one arm in
@objectstack/types'structuredCodeAnswer. The REST body names the refused TARGET and relaysallowed. It does so only when the error names an object other than a defined route object. Every other producer's answer is byte for byte unchanged, which is measured and pinned. - The ledger appends both existing codes in
@objectstack/metadata-protocol's own section. No new code is added.
The merges, read on GitHub.
606e50f7efhas parents04cf472669andc74d843997(fix(core)!: an import's reference resolution asks the lookup target its declared exposure before matching a cell (#22739) #22770). Its merge base withmainisc74d843997.- Against that base, 11 of the 12 files carry net added and removed lines identical to the reviewed head's (
9859e4ec42..964aceffd6). - The 12th is the census pin. It holds the union: six
decidedreads (row 4refuseUnservedRelationTarget,list, beside rows 1–3 and 5–7), no CALLERS rowopen, and the header names all four doors. mainhas since moved to5fc57b382ewithout touching these 12 paths. The queue builds against the then-currentmain.
Evidence read.
- On a real stack, for an administrator and a member, on the list and query routes:
- every refused shape moved from evaluated to
404or405; - the
list-only and open controls are unchanged; - the
403precedent is unchanged.
- every refused shape moved from evaluated to
- Ablations each went red where predicted, with every restore proven: A (the wiring), B (on
dist), C (a hand-spelled rule against the enumeration pin), D (the ledger rows againstcheck:error-code-provenance) and E (the types arm). - At
606e50f7ef:- core
repo5 files / 55; - metadata-protocol's two exposure pins 28/28;
- the dogfood pin 34/34;
- the types arm pin 4/4;
- rest
repo5 files / 209.
- core
- Gates: 95 commands, all exit 0 (
--ran: 91 derived, 91 run). - CI on this head: 35 check-runs, all concluded (32 success, 3 rostered skips).
Changesets checked.
@objectstack/metadata-protocolisminor, with!, a**BREAKING**banner and one ADR-0087not-requiredmarker. FROM → TO and the migration are stated (filter on the lookup's stored id, or declarelist).@objectstack/specispatch, the ledger value only.@objectstack/typesispatch, the arm.
Files outside the claim's first surface:
- the
packages/typesarm and its pin, from the round-2 revision; - test-only legs in
packages/qa/dogfood; - two
packages/restdrift guards.
All were declared on
domain:cli(#6024: 6105548307, 6106072362). The PR body's verification lines were brought to the final head in this act. They had named round 1's merge as the final commit.Landing.
- Ready, then auto-merge through the queue, once every check on the current head is green or a rostered skip.
- On merge, 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 closes and 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 4. - Unblocked then:
- 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 (row 8, the MCP stdio bridge, the same decision at a second door);
- [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answer
Internal server error#22741 (isSandboxOrigin's neighbourhood); - data: under a search, a paged list's
totalis a page-local estimate (offset + page + 1), and the console shows it as the record count ("26 records" for 300 matches) #22790 (findData's search total), queued behind this file.
Generated by Claude Code
- Ready to queue: base
objectstack-fleet commented
on Oct 11, 2026 ContributorAuthorMore actionsLanded: PR #22768 →
ec7c7e0637·domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T09:07Z⛔ Classes, positions and functions only.
-
Merged through the queue at 2026-10-11T09:06:23Z as
ec7c7e0637. The two readings:- The merge commit is an ancestor of
origin/main, andrefuseUnservedRelationTargetis inmetadata-protocol'sprotocol.ts. - The queue branch
gh-readonly-queue/main/pr-22768-*is gone.
- The merge commit is an ancestor of
-
The card closed
completedthroughFixes #22737.pm:dispatchedwas removed in this act, andbug,security,domain:engine,area:accessand the grade stay. The lane's closed set since 07:30Z is this card alone. -
What landed (
@objectstack/metadata-protocolminor, BREAKING narrowing;@objectstack/specand@objectstack/typespatch):- The data door's nested-relation walk asks each condition's TARGET object the one exposure decision, for
list. - It covers
where, thefilteralias, each aggregation'sfilter, and eachexpandentry's ownwhere. - An unserved target is refused (
404 OBJECT_API_DISABLED, or405 OBJECT_API_METHOD_NOT_ALLOWEDwithallowed), and the REST body names that target. - The engine is untouched.
- The data door's nested-relation walk asks each condition's TARGET object the one 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 4. The parent stays open for row 8 (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) and 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). -
Unblocked by this landing:
- 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 (row 8, the MCP stdio bridge, the same decision at a second door);
- [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answer
Internal server error#22741 (a sandbox's own faults, besideisSandboxOrigin); - data: under a search, a paged list's
totalis a page-local estimate (offset + page + 1), and the console shows it as the record count ("26 records" for 300 matches) #22790 (findData's search total, queued behind this file).
This seat dispatches them as its batch frees, 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 first.
-
Records: claim 6104514778 (revised in round 2), contract reviews PASS 6106147584 (
964aceffd6) and 6107185299 (606e50f7ef, the supplementary record after two merges ofmain), and this seat's ACCEPT 6107195645.
Generated by Claude Code
-
This card carries census row 4 of #22661 (part of #22661). #22661 keeps rows 1–3, landing through PR #22735, and the enumeration pin. ⛔ Classes, positions and functions only.
Filing class: ① a product defect, class (a). Reach: measured once at a public door by #22661's dev on a real stack (report 6102815147, census row 4), for an administrator and a member alike. Reader who acts: the
domain:enginelane, seat 1 (#6367), which owns #22661's derived sub-issues and dispatches this one directly.The gap
ObjectQL.lowerRelationConditionsreads the related object as the caller, and it does not ask the target's exposure decision.relationConditionSites, from 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) judgesinternal: truefields only.Direction (from #22661's triage, not a ruling)
apiExposureDenialReasonfor its target. ⛔ No second rule.@objectstack/metadata-protocolinpackages/spec's error-code ledger, it rides the pre-approved ledger-append class.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's enumeration pin.Duplicate check
Issues updated since 2026-09-01 were paged to the end through REST: 4,476 issues, PRs excluded, closed included, #1795 to #22729. A local grep over titles and bodies (comments are outside this instrument's radius) for
lowerRelationConditionsor a relation filter or condition within 200 characters ofapiEnabledor exposure found 0 hits. Control:apiEnabledalone found 11 hits.Dedupe words: nested relation filter exposure · lowerRelationConditions apiEnabled · related object condition unexposed target