You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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 card carries census row 7 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 at a public door by #22661's dev on a real stack (report 6102815147, census row 7), for an administrator and a member alike. Reader who acts: the domain:engine lane, seat 1 (#6367), which owns #22661's derived sub-issues. It waits for #22718, which is in flight in the same file (packages/core/src/utils/import-runner.ts).
The gap
The import door matches a lookup cell's display text against the target object through resolveRef (import-runner.ts), which calls the generic data door 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.
So a match stores the target's id and a miss answers a per-row reference_not_found, even for a target whose declared exposure refuses reads. That is a presence oracle.
The reference resolution asks apiExposureDenialReason for the target before matching. ⛔ No second rule.
A refused target answers the same as a target the caller cannot read. Measure and state the precedent.
Read the target through referenceTargetOf so the enumeration pin sees it.
Pins: a member and an administrator, with an unexposed target refused and an exposed target unchanged. Ablation-verified.
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 resolveRef within 300 characters of apiEnabled or exposure, or an import reference or lookup near apiEnabled or unexposed, found 0 hits. Controls: apiEnabled alone found 11 hits and resolveRef alone found 1.
Dedupe words: import reference resolution exposure · resolveRef apiEnabled false · import lookup name match unexposed object
Claim: PM loop round 2 · 2026-10-11T02:13Z
Session: session_01JfJfBUC3cQ6hhgm9MQK76T
Account: os-project-manager (the seat's linked user, as GET /user answers it; the card's assignee)
Branch: claude/issue-22739-import-ref-exposure
Worktree: objectstack-issue-22739
Domain: domain:engine
Seat: domain:engine#1 (seat post #6367)
File surface (read on origin/maind7b26df5b5; stop on a breach and explain it in the report):
packages/core/src/utils/import-runner.ts: the RefResolver that runImport builds (resolveRef, around line 625).
packages/core/src/utils/import-field-meta.ts: read a reference field's target through referenceTargetOf.
packages/core/src/security/second-object-read-exposure.pin.test.ts: census row 7 moves from open to decided, and the pin's import-door blind-spot note goes.
Tests under packages/core/src/, and one changeset.
Conditional, declared before any edit: a door-level pin in packages/rest's import ledger harness or in packages/qa/dogfood (domain:cli, test files only).
Container & model: M, mode:subagent, model: default (dispatch-gates --tier: no path-derived mandate)
Clause-②: no (narrowing)
os-dev-report
{
"issue": 22739,
"status": "done",
"branch": "claude/issue-22739-import-ref-exposure",
"pr": "#22770",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id (claim 6104521404), inherited by this subagent",
"premise_still_valid": true,
"summary": "Premise confirmed on a real stack (verify bootStack, fixture objects, admin and member, sync import door and async jobs door) at base bf515e7: resolveRef (core/src/utils/import-runner.ts) matched cells against targets declaring apiEnabled:false, a whitelist granting no read (create-only) and a get-only whitelist — a cell naming the record and a pasted id stored the id, a miss answered reference_not_found, both personas, both doors. PRECEDENT measured: a target the member cannot read (no object read) and a record hidden by row scope answer all three cells reference_not_found. Fix: servesReferenceTarget (new module function in import-runner.ts) asks the spec decision canServeApiOperation(target enable, 'list') before matching; the enable block is read through the protocol's getMetaItem (new OPTIONAL member of ImportProtocolLike; a protocol without it, i.e. plugin-auth identity import, is not judged); asked once per target per import; refused: every cell reference_not_found (match, miss, pasted id alike), target never read; unreadable declaration: withheld (fail-closed). Operation list, not get: the resolver performs a predicate find, which DATA_ACTION_TO_API_OPERATION maps to list; get would keep serving name matches on a get-only target. Decision takes no caller, so the connector pull (system context, real protocol) is judged too, as #22661's reads judge every caller. import-field-meta.ts reads the target through referenceTargetOf, so the #22661 enumeration pin classifies it decided (operation list); its import blind-spot note is gone. toFailedResult untouched. After: refused targets answer exactly the precedent's shape; served targets unchanged; both personas, both doors. Side effect, measured: a user field written without reference now targets sys_user on import (a user email cell failed reference_not_found before, resolves now); export/template sides follow from the same map, NOT MEASURED on a stack. files_changed: .changeset/22739-import-reference-target-exposure.md (core minor, BREAKING narrowing, Clause-② no (narrowing), adr-0087 not-required marker); packages/core/src/utils/import-runner.ts; packages/core/src/utils/import-field-meta.ts; packages/core/src/utils/import-runner-reference-exposure.test.ts (new); packages/core/src/security/second-object-read-exposure.pin.test.ts; packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts (new); packages/rest/src/export-format.test.ts. line_budget: 7 files, +329 / -22 vs merge base (source +59/-5 across import-runner.ts and import-field-meta.ts, within 80; total over the 250 suggestion because of the two behaviour pins the dispatch asked for: core unit 91, door-level dogfood 126, plus a 27-line changeset). deviations: (a) file-surface breach: packages/rest/src/export-format.test.ts — its presentation-keys fixture put reference on a number field, which the arbiter reads as no target; fixture type changed to lookup (2 literals, test-only); (b) line budget over, reason above; (c) origin/main merged twice (e6e2b42, 679f2ae), the second because main moved verify bootStack and core security code under the dogfood pin; post-merge rebuild and re-run green; (d) the jobs door member leg needs a read grant on the member's own import jobs, absent from the #22661 fixture member — added in the new test file, shared fixture untouched; (e) the first before-leg of the user-field probe was void (core DTS build failed on an unused import, preflight not proven) — re-run with a compiling mutation and dist preflight; (f) one core test invocation passed --maxWorkers after a bare --, which vitest dropped; the whole local project ran as intended; (g) zero label writes: the dispatch names none and a changeset exists; (h) PR body correction owed (seat to write, dev does not PATCH): the measurement table's first row names deny-all among the real-stack targets, but the deny-all whitelist was pinned only in the core unit test; the row should read: off switch / whitelist without list (create-only, get-only). Worktree node_modules removed and worktree removed (no --force) after the PR opened.",
"tests": "MEASUREMENT (scratch probes, deleted, never committed; fixture objects only): before bf515e7 / after a24d5df core build, admin+member x sync+jobs x 7 targets x 3 cells — table in PR body. Precedent legs unchanged before and after. PINS: core import-runner-reference-exposure.test.ts 13/13; dogfood import-reference-exposure.dogfood.test.ts 11/11 plus #22661 second-object-exposure 25/25 (36/36 at 679f2ae after a post-merge rebuild); core second-object-read-exposure.pin.test.ts 4/4. ABLATION (scripts/ablation-replace.mjs WRAP mode on committed 22bfc27, anchors 1 to 0, blobs changed, every restore blob==HEAD and git diff HEAD empty): A resolveRef wiring bypassed: core pin 9 red/4 green; A2 operation get: 4 red/9 green; A3 catch fails open: 1 red; D import-field-meta back on the raw carrier: enumeration pin 2 red; C = A on dist: core rebuilt, ablation-dist-preflight marker present exit 0, dogfood pin 4 red/7 green (withheld leg, both doors, both personas); restore rebuilt, preflight --absent exit 0 (dist and tree clean), rerun 11/11. PACKAGES: core at 679f2ae: test 92 files/2346 passed, test:repo 5/55, typecheck exit 0 (both new tests in tsconfig.test.json program per --listFiles); dogfood typecheck exit 0 (new file in program). Importers (public face changed): rest, plugin-auth, service-automation typecheck exit 0 at e6e2b42; rest: the 38 test files reaching import/export/template doors 38/38 (25 at e6e2b42 incl. one red fixed by the fixture change, re-run with the 13 others at eed1cbf); plugin-auth admin-import-users 2 files/50; service-automation connector-pull 3 files/22. Second merge touched none of these packages' sources. LINT narrowed: eslint --no-inline-config on the 6 changed .ts files at eed1cbf (unchanged at 679f2ae); population from --print-config, 5-6 active rules each, none ignored; --format json 6 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting (parserOptions ecmaVersion/sourceType only), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. GATES at 679f2ae after a full workspace build: dispatch-gates --commands --repo objectstack-ai/objectstack derived 69 (the dispatch's 51 are a subset; same list at both heads); --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN (derived zero, every exit recorded). check:dual-build-cjs-loads was exit 3 PREREQUISITE NOT MET on the first battery, exit 0 after the full build. Each: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; node scripts/check-dev-prereqs.mjs --self-test :: exit 0 ; node scripts/check-dts-emitted.mjs --self-test :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-issue-citations.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; node scripts/release-pending-publish.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm --filter @objectstack/spec run check:empty-state :: exit 0 ; pnpm --filter @objectstack/spec run check:liveness :: exit 0 ; pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 ; pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:kernel-hook-pairs :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 CI not awaited (in_progress at report time).",
"mcp_calls": "0 — no MCP GitHub tool 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 #22770, run 38112790620, read-back 8684 bytes identical) + POST /repos//issues/22770/assignees os-project-manager; (2) this os-dev-report comment = POST /repos//issues/22739/comments. Zero label writes. git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · plugin-auth identity import (admin-import-users.ts) hands runImport a hand-written protocol with no getMetaItem and reads under the system identity, so its lookup cells are matched unjudged; privileged admin door with platform-object targets; noted in PR Acceptance notes and the changeset, not filed",
"carrier: 承接者:无 · user fields without reference now target sys_user on the export and import-template doors too (same map); measured on import only; export/template NOT MEASURED; noted in PR Acceptance notes, not filed"
]
}
REWORK (seat review): PR #22770 at head 679f2ae01c
domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-11T04:54Z. Claim 6104521404. Read against GitHub and origin/main, not against the report. ⛔ Classes, positions and functions only.
What holds, and is not reworked:
servesReferenceTarget asks the spec's one decision, canServeApiOperation(enable, 'list'), once per target per import, before any match.
A refused target answers every cell exactly as the measured precedent does: a target the caller cannot read gets reference_not_found for a match, a miss and a pasted id alike. It leaves no presence signal.
A declaration that cannot be read withholds (fail-closed).
The one item to rework: import-field-meta.ts widens the import door's accept set.
reference: referenceTargetOf(f) changes WHICH fields the import, export and template doors treat as references, not just how the target is read.
A user field that declares no reference now gets the arbiter's implicit target. Measured by the dev: a cell that answered reference_not_found on main now resolves. On the export and template doors it is not measured.
A published accept set that widens is a Clause-② change however small. It does not belong in this card, whose line is Clause-②: no (narrowing).
The rework:
The set of fields the import door treats as references stays exactly what it is on main: a field that carries an explicit reference string. Read that field's target through referenceTargetOf. For example: typeof f.reference === 'string' ? referenceTargetOf(f) : undefined.
Pin the control: a user field without reference behaves exactly as on main at the import door. Ablate it.
The residual narrowing: a non-reference type that carries reference now has no target, as the arbiter answers.
Measure whether any shipped producer declares such a field (a census with a control).
If none does, state it in the changeset's FROM → TO as part of this narrowing, and keep the test-only fixture change in packages/rest.
If one does, stop and report.
Re-derive and re-run the gates, merge main once, and report.
Owed by this seat, not the dev:
The PR body's measurement row correction, the dev's flag (h).
A separate card for the user-field gap. The import door disagrees with the spec's arbiter, and fixing it is a widening.
os-dev-report
{
"issue": 22739,
"status": "done",
"branch": "claude/issue-22739-import-ref-exposure",
"pr": "#22770",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id (claim 6104521404), inherited by this subagent",
"premise_still_valid": true,
"summary": "REWORK round (verdict 6105610630), one item, done; the body of the fix (servesReferenceTarget, list, precedent-shaped refusal, fail-closed, toFailedResult untouched) is unchanged. (1) import-field-meta.ts now reads reference: typeof f.reference === 'string' ? referenceTargetOf(f) : undefined: the set of fields the import/export/template doors treat as references is exactly main's (a field declaring a reference string), and that field's target is read through the arbiter, so the #22661 enumeration pin still classifies the caller decided (4/4). (2) Control pin added to core import-runner-reference-exposure.test.ts: a user field without reference has no target in the map and its cell reaches the write unresolved (no sys_user lookup, no exposure ask), as on main; a user field declaring reference is resolved; a non-reference type declaring reference names no target. Ablated (guard removed: 1 red / 13 green). (3) Residual narrowing census (AST over every git-tracked non-test .ts/.tsx/.mts/.js/.mjs/.json under packages/ and examples/, fixture and test dirs excluded; 3636 files at 0d326bf, 3638 at c06ec74, identical result): 0 shipped field declares reference on a non-reference type (0 object literals; the one literal pairing type with reference is driver-sql FIELD_KEY_STORAGE_CLASS, a key-classification table, not a field; 0 Field.non-reference-helper calls carrying reference); 22 undetermined hits are all schema/transform/generated-form/liveness code, none a field declaration; CONTROL 30 reference-typed literals and 80 Field.lookup/masterDetail/user/tree calls; positive control: a planted literal, a planted Field.text call and a planted non-literal type were all found. None, so the narrowing is stated in the changeset FROM/TO: the only population whose answer moves is the legacy, schema-refused type spelling reference (still listed in the doors' own type tables), which now names no target; every other non-reference type was never resolved by these doors. packages/rest export-format.test.ts fixture change kept. (4) Gates re-derived (same 69) and re-run at the final head; main merged. files_changed (vs merge base): .changeset/22739-import-reference-target-exposure.md; packages/core/src/utils/import-runner.ts; packages/core/src/utils/import-field-meta.ts; packages/core/src/utils/import-runner-reference-exposure.test.ts; packages/core/src/security/second-object-read-exposure.pin.test.ts; packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts; packages/rest/src/export-format.test.ts. Rework commits: f06912e (guard + control pin), 7610e85 (changeset), merges 0d326bf and c06ec74. line_budget: 7 files, +367 / -21 vs merge base (source +60/-5 in import-runner.ts and import-field-meta.ts, within 80; total over 250 for the two behaviour pins, now 122 + 126 lines, and a 33-line changeset). deviations: (a) origin/main merged TWICE this round: 0d326bf at the start, then c06ec74 because main's #22766 edited the enumeration pin (a textual conflict on the decided-list assertion, resolved by uniting both lists: servesExpansionTarget, servesLabelTarget, servesPayloadDisplayTarget, servesReferenceTarget, servesSummaryTitleTarget); the pin header's "All three now ask" became "Each read classified decided below now asks" inside that merge commit; everything re-verified at c06ec74; (b) the changeset cites the census at 0d326bf; re-run at c06ec74 gives the same result, not re-cited; (c) for the main reading a throwaway detached worktree at origin/main was created, built and removed; (d) PR body corrections owed beyond flag (h) (seat writes, dev does not PATCH): the Acceptance-notes bullet on user fields without reference (it described the retired widening) should say the field set is main's and the gap is carried by a separate card; the bullet on a non-string reference carrier throwing no longer holds (guarded, reads as no target as on main); the Legacy type reference bullet now describes the residual narrowing; the What-changed bullet on buildFieldMetaMap should say target read through the arbiter only for a field declaring reference; Verification numbers are superseded by this report's tests field. Worktree node_modules removed and worktree removed (no --force).",
"user_field_gap_on_main": "origin/main 996aa86 (detached worktree, dogfood closure built under the verify lock). COMMAND: PROBE_OUT=FILE pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/zz-probe-22739-user.dogfood.test.ts (scratch, never committed): a fixture object with a user field written WITHOUT reference (owner) and one written with Field.user, which declares reference sys_user (assignee); the signed-in administrator imports one row per field through POST /api/v1/data/OBJECT/import, each cell holding the administrator's own email. RESULT: owner row ok:false, code reference_not_found, field owner, error "Owner: no sys_user record has id" followed by the quoted cell (the raw text reached the write as an id and the engine refused it), nothing stored; assignee row ok:true, created, stored the administrator's user id. CONTROL LEGS on the same build: referenceTargetOf({type:user}) answers sys_user (and sys_user with reference written); buildFieldMetaMap({fields:{owner:{type:user}}}).get(owner).reference answers undefined. So the import door disagrees with the spec's arbiter for a user field written without reference. The rework build answers identically (same probe on this branch before the second merge, core dist carrying the guard, checked by grep: the same two rows), and the core control pins it.",
"tests": "REWORK PINS: core import-runner-reference-exposure.test.ts 14/14 (13 + the user-field control); enumeration pin 4/4; dogfood import-reference-exposure 11/11 plus #22661 second-object-exposure 25/25 (36/36). ABLATION on committed f06912e (scripts/ablation-replace.mjs WRAP, anchors 1 to 0, each restore blob==HEAD and git diff HEAD empty): G guard removed (reference: referenceTargetOf(f)) gives the control 1 red / 13 green; D import-field-meta back on the raw carrier gives the enumeration pin 2 red / 2 green; control run at HEAD 4/4. Round-1 ablations A, A2, A3, C stand (import-runner.ts unchanged). PACKAGES at c06ec74 after a full workspace build (72 tasks): core test 93 files/2349 passed, test:repo 5/55, typecheck exit 0 (incl. check:test-typecheck); dogfood the two pins 36/36, typecheck exit 0; rest typecheck exit 0 and its 38 test files reaching the import/export/template doors 38/38 (1223 passed, 22 skipped); plugin-auth admin-import-users 2 files/50; service-automation connector-pull 3 files/22. Same battery at 7610e85 (before the second merge) all exit 0, core 92/2347. LINT narrowed at c06ec74: eslint --no-inline-config on the 6 changed .ts files; population from --print-config (5-6 active rules each, none ignored); --format json 6 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move; repo-wide lint is CI's. GATES at c06ec74 (origin/main not ahead at reconcile time): dispatch-gates --commands derived 69 (same list as round 1; the dispatch's 51 a subset); --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN, no stale-tree note. (The 7610e85 battery was also 69/69 exit 0 but reconciled with a STALE TREE note, which is what exposed main's edit to the enumeration pin.) Each: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; node scripts/check-dev-prereqs.mjs --self-test :: exit 0 ; node scripts/check-dts-emitted.mjs --self-test :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-issue-citations.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; node scripts/release-pending-publish.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm --filter @objectstack/spec run check:empty-state :: exit 0 ; pnpm --filter @objectstack/spec run check:liveness :: exit 0 ; pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 ; pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:kernel-hook-pairs :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool called this round; reads went through gh api REST GETs",
"api_writes": "1 this round — fleet-write relay (objectstack-fleet[bot]) POST /repos/objectstack-ai/objectstack/dispatches executing this os-dev-report comment = POST /repos//issues/22739/comments. Round 1 had 2 (pr_create with assignee, and report comment 6105593563). Zero label writes. git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:engine seat 1 (it files the separate card, per verdict 6105610630) · the import door treats a user field written without reference as no reference while the spec's arbiter gives it sys_user; reading on main in user_field_gap_on_main; fixing it is a widening (Clause-②), not filed by the dev",
"carrier: 承接者:无 · plugin-auth identity import (admin-import-users.ts) hands runImport a hand-written protocol with no getMetaItem and reads under the system identity, so its lookup cells are matched unjudged; privileged admin door with platform-object targets; noted in PR Acceptance notes and the changeset, not filed"
]
}
ACCEPT (seat review): PR #22770 at head c06ec74a87
domain:engine seat 1 (#6367) · session_01JfJfBUC3cQ6hhgm9MQK76T · 2026-10-11T06:58Z. Claim 6104521404. Dev reports 6105593563 (round 1) and 6106294041 (rework). REWORK 6105610630, closed. Contract review PASS 6106439614 (CONTRACT_REVIEW_TIER, same head). Read against GitHub and origin/main, not against the reports. ⛔ Classes, positions and functions only.
Shape.
Ready to queue: base main, assigned os-project-manager, mergeable_state clean.
Line 1 is Fixes #22739. A closing-keyword scan of the whole body finds that line only.
Line 2 is now Clause-②: yes (narrowing), edited by this seat after the contract review. No accept set widens, but the exported ImportProtocolLike gains an optional getMetaItem?. That is additive and type-level, and the fleet spells it yes. The changeset's line follows on this PR's next head.
Seven files. NOT governed.
The REWORK, closed.
The fields the import, export and template doors treat as references are exactly main's: those declaring a reference string. Each such target is read through referenceTargetOf.
A user field without reference is unchanged, and pinned (ablation G: 1 red).
The residual narrowing (the legacy, schema-refused type: 'reference' spelling) was measured: an AST census found 0 producers, with controls and a positive control. It is stated in FROM → TO.
servesReferenceTarget asks canServeApiOperation(enable, 'list'), the spec's one decision, read through the protocol's getMetaItem. It asks once per target per import, before any read.
A refused target answers every cell reference_not_found, the measured precedent for a target the caller cannot read. No presence signal is left, and nothing of the target is read.
A declaration that cannot be read withholds (fail-closed). The decision takes no caller, so the connector pull is judged too.
On a real stack, for an administrator and a member, on the sync and jobs doors:
the before-readings reproduce census row 7;
the after-readings answer as the precedent;
the served controls are unchanged.
Ablations each went red where predicted, with every restore proven: A, A2 and A3 (wiring, operation, fail-open), C (on dist), D (the raw carrier) and G (the guard).
At c06ec74a87:
core 93 files / 2,349 tests and its repo project 5 / 55;
the dogfood pins 36 / 36;
the importers (rest 38 import/export/template files, plugin-auth, service-automation) green;
typechecks exit 0.
Gates: 69 derived, 69 run.
CI on this head: 42 check-runs, all concluded (37 success, 5 rostered skips).
Changeset checked.@objectstack/coreminor, !, a **BREAKING** banner and one ADR-0087 not-required marker. FROM → TO covers both moved populations (the unserved target, and the legacy spelling), and the bindings match the diff.
Files outside the claim.packages/rest/src/export-format.test.ts (test-only, domain:cli): its fixture declared reference on a number field, a key inert on that type. The PR body's wording was corrected in this act.
Ruled, not filed in this fire (this fire's filing quota is spent): plugin-auth's identity import.
admin-import-users.ts hands runImport a protocol without getMetaItem, although it holds deps.getMetaItem. So its reference cells are matched without the exposure ask.
Merged through the queue at 2026-10-11T07:25:41Z as c74d843997. The two readings:
The merge commit is an ancestor of origin/main.
servesReferenceTarget is in import-runner.ts, and import-field-meta.ts reads a declared reference through referenceTargetOf (the guarded form).
The queue branch gh-readonly-queue/main/pr-22770-* is gone.
The card closed completed through Fixes #22739. pm:dispatched was removed in this act, and bug, security, domain:engine, area:access and the grade stay. The lane's closed set since 06:40Z is this card alone.
What landed (@objectstack/core, minor, BREAKING narrowing; Clause-②: yes (narrowing) for the optional ImportProtocolLike.getMetaItem?):
An import's reference resolution asks the TARGET object's declared exposure for list before matching.
A refused target answers every cell reference_not_found, the precedent for a target the caller cannot read.
The set of fields treated as references is unchanged.
Activity
objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 2 · 2026-10-11T02:13Z
Session:
session_01JfJfBUC3cQ6hhgm9MQK76TAccount:
os-project-manager(the seat's linked user, asGET /useranswers it; the card's assignee)Branch:
claude/issue-22739-import-ref-exposureWorktree:
objectstack-issue-22739Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface (read on
origin/maind7b26df5b5; stop on a breach and explain it in the report):packages/core/src/utils/import-runner.ts: theRefResolverthatrunImportbuilds (resolveRef, around line 625).packages/core/src/utils/import-field-meta.ts: read a reference field's target throughreferenceTargetOf.packages/core/src/security/second-object-read-exposure.pin.test.ts: census row 7 moves fromopento decided, and the pin's import-door blind-spot note goes.packages/core/src/, and one changeset.packages/rest's import ledger harness or inpackages/qa/dogfood(domain:cli, test files only).Container & model:
M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: no (narrowing)
Responsibility:
resolveRef, which matches a cell's display text by calling the generic data door'sfindDataon the target; that door judges no exposure for the ADDRESSED object | the REST route and the dispatcher, which judge the route object's exposure only | an importing user, administrator or member, who learns a target record's presence by match or miss; 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 (census row 7)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 7); [finding] import: a hook body that CRASHES reaches an import row as its native fault text (hook NAME threw: TypeError: …), whilePOST /data/:objectand/createManyanswer500 INTERNAL_ERRORand withhold it #22718's landing record 6104475711Serial constraints cleared:
POST /data/:objectand/createManyanswer500 INTERNAL_ERRORand withhold it #22718 (PR fix(core): an import row for a sandboxed body the door answers as a fault reads as that fault #22740) landed asd7b26df5b5in this file, which was this card'sBlocked-by:.Internal server error#22741 (toFailedResult, the same file) follows this card in this lane.import-runner.ts,import-coerce.tsorimport-field-meta.ts.Generated by Claude Code
fix(core): an import's reference resolution asks the lookup target it…
test(core,dogfood): pin the import reference resolution's target expo…
docs(changeset): the measured user-field reading on the import door (#…
test(rest): the presentation-keys fixture declares a reference type, …
objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{
"issue": 22739,
"status": "done",
"branch": "claude/issue-22739-import-ref-exposure",
"pr": "#22770",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id (claim 6104521404), inherited by this subagent",
"premise_still_valid": true,
"summary": "Premise confirmed on a real stack (verify bootStack, fixture objects, admin and member, sync import door and async jobs door) at base bf515e7: resolveRef (core/src/utils/import-runner.ts) matched cells against targets declaring apiEnabled:false, a whitelist granting no read (create-only) and a get-only whitelist — a cell naming the record and a pasted id stored the id, a miss answered reference_not_found, both personas, both doors. PRECEDENT measured: a target the member cannot read (no object read) and a record hidden by row scope answer all three cells reference_not_found. Fix: servesReferenceTarget (new module function in import-runner.ts) asks the spec decision canServeApiOperation(target enable, 'list') before matching; the enable block is read through the protocol's getMetaItem (new OPTIONAL member of ImportProtocolLike; a protocol without it, i.e. plugin-auth identity import, is not judged); asked once per target per import; refused: every cell reference_not_found (match, miss, pasted id alike), target never read; unreadable declaration: withheld (fail-closed). Operation list, not get: the resolver performs a predicate find, which DATA_ACTION_TO_API_OPERATION maps to list; get would keep serving name matches on a get-only target. Decision takes no caller, so the connector pull (system context, real protocol) is judged too, as #22661's reads judge every caller. import-field-meta.ts reads the target through referenceTargetOf, so the #22661 enumeration pin classifies it decided (operation list); its import blind-spot note is gone. toFailedResult untouched. After: refused targets answer exactly the precedent's shape; served targets unchanged; both personas, both doors. Side effect, measured: a user field written without reference now targets sys_user on import (a user email cell failed reference_not_found before, resolves now); export/template sides follow from the same map, NOT MEASURED on a stack. files_changed: .changeset/22739-import-reference-target-exposure.md (core minor, BREAKING narrowing, Clause-② no (narrowing), adr-0087 not-required marker); packages/core/src/utils/import-runner.ts; packages/core/src/utils/import-field-meta.ts; packages/core/src/utils/import-runner-reference-exposure.test.ts (new); packages/core/src/security/second-object-read-exposure.pin.test.ts; packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts (new); packages/rest/src/export-format.test.ts. line_budget: 7 files, +329 / -22 vs merge base (source +59/-5 across import-runner.ts and import-field-meta.ts, within 80; total over the 250 suggestion because of the two behaviour pins the dispatch asked for: core unit 91, door-level dogfood 126, plus a 27-line changeset). deviations: (a) file-surface breach: packages/rest/src/export-format.test.ts — its presentation-keys fixture put reference on a number field, which the arbiter reads as no target; fixture type changed to lookup (2 literals, test-only); (b) line budget over, reason above; (c) origin/main merged twice (e6e2b42, 679f2ae), the second because main moved verify bootStack and core security code under the dogfood pin; post-merge rebuild and re-run green; (d) the jobs door member leg needs a read grant on the member's own import jobs, absent from the #22661 fixture member — added in the new test file, shared fixture untouched; (e) the first before-leg of the user-field probe was void (core DTS build failed on an unused import, preflight not proven) — re-run with a compiling mutation and dist preflight; (f) one core test invocation passed --maxWorkers after a bare --, which vitest dropped; the whole local project ran as intended; (g) zero label writes: the dispatch names none and a changeset exists; (h) PR body correction owed (seat to write, dev does not PATCH): the measurement table's first row names deny-all among the real-stack targets, but the deny-all whitelist was pinned only in the core unit test; the row should read: off switch / whitelist without list (create-only, get-only). Worktree node_modules removed and worktree removed (no --force) after the PR opened.",
"tests": "MEASUREMENT (scratch probes, deleted, never committed; fixture objects only): before bf515e7 / after a24d5df core build, admin+member x sync+jobs x 7 targets x 3 cells — table in PR body. Precedent legs unchanged before and after. PINS: core import-runner-reference-exposure.test.ts 13/13; dogfood import-reference-exposure.dogfood.test.ts 11/11 plus #22661 second-object-exposure 25/25 (36/36 at 679f2ae after a post-merge rebuild); core second-object-read-exposure.pin.test.ts 4/4. ABLATION (scripts/ablation-replace.mjs WRAP mode on committed 22bfc27, anchors 1 to 0, blobs changed, every restore blob==HEAD and git diff HEAD empty): A resolveRef wiring bypassed: core pin 9 red/4 green; A2 operation get: 4 red/9 green; A3 catch fails open: 1 red; D import-field-meta back on the raw carrier: enumeration pin 2 red; C = A on dist: core rebuilt, ablation-dist-preflight marker present exit 0, dogfood pin 4 red/7 green (withheld leg, both doors, both personas); restore rebuilt, preflight --absent exit 0 (dist and tree clean), rerun 11/11. PACKAGES: core at 679f2ae: test 92 files/2346 passed, test:repo 5/55, typecheck exit 0 (both new tests in tsconfig.test.json program per --listFiles); dogfood typecheck exit 0 (new file in program). Importers (public face changed): rest, plugin-auth, service-automation typecheck exit 0 at e6e2b42; rest: the 38 test files reaching import/export/template doors 38/38 (25 at e6e2b42 incl. one red fixed by the fixture change, re-run with the 13 others at eed1cbf); plugin-auth admin-import-users 2 files/50; service-automation connector-pull 3 files/22. Second merge touched none of these packages' sources. LINT narrowed: eslint --no-inline-config on the 6 changed .ts files at eed1cbf (unchanged at 679f2ae); population from --print-config, 5-6 active rules each, none ignored; --format json 6 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting (parserOptions ecmaVersion/sourceType only), so no untouched file's verdict can move; repo-wide pnpm lint is CI's. GATES at 679f2ae after a full workspace build: dispatch-gates --commands --repo objectstack-ai/objectstack derived 69 (the dispatch's 51 are a subset; same list at both heads); --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN (derived zero, every exit recorded). check:dual-build-cjs-loads was exit 3 PREREQUISITE NOT MET on the first battery, exit 0 after the full build. Each: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; node scripts/check-dev-prereqs.mjs --self-test :: exit 0 ; node scripts/check-dts-emitted.mjs --self-test :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-issue-citations.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; node scripts/release-pending-publish.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm --filter @objectstack/spec run check:empty-state :: exit 0 ; pnpm --filter @objectstack/spec run check:liveness :: exit 0 ; pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 ; pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:kernel-hook-pairs :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 CI not awaited (in_progress at report time).",
"mcp_calls": "0 — no MCP GitHub tool 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 #22770, run 38112790620, read-back 8684 bytes identical) + POST /repos//issues/22770/assignees os-project-manager; (2) this os-dev-report comment = POST /repos//issues/22739/comments. Zero label writes. git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · plugin-auth identity import (admin-import-users.ts) hands runImport a hand-written protocol with no getMetaItem and reads under the system identity, so its lookup cells are matched unjudged; privileged admin door with platform-object targets; noted in PR Acceptance notes and the changeset, not filed",
"carrier: 承接者:无 · user fields without reference now target sys_user on the export and import-template doors too (same map); measured on import only; export/template NOT MEASURED; noted in PR Acceptance notes, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
REWORK (seat review): PR #22770 at head
679f2ae01cdomain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T04:54Z. Claim 6104521404. Read against GitHub andorigin/main, not against the report. ⛔ Classes, positions and functions only.What holds, and is not reworked:
servesReferenceTargetasks the spec's one decision,canServeApiOperation(enable, 'list'), once per target per import, before any match.reference_not_foundfor a match, a miss and a pasted id alike. It leaves no presence signal.list(a predicate read over the target) is right, and matches 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 reading.toFailedResultis untouched.The one item to rework:
import-field-meta.tswidens the import door's accept set.reference: referenceTargetOf(f)changes WHICH fields the import, export and template doors treat as references, not just how the target is read.userfield that declares noreferencenow gets the arbiter's implicit target. Measured by the dev: a cell that answeredreference_not_foundonmainnow resolves. On the export and template doors it is not measured.Clause-②: no (narrowing).The rework:
main: a field that carries an explicitreferencestring. Read that field's target throughreferenceTargetOf. For example:typeof f.reference === 'string' ? referenceTargetOf(f) : undefined.userfield withoutreferencebehaves exactly as onmainat the import door. Ablate it.referencenow has no target, as the arbiter answers.packages/rest.mainonce, and report.Owed by this seat, not the dev:
user-field gap. The import door disagrees with the spec's arbiter, and fixing it is a widening.Generated by Claude Code
fix(core): the import treats as a reference only a field that declare…
docs(changeset): the field set does not widen; the residual narrowing…
objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{
"issue": 22739,
"status": "done",
"branch": "claude/issue-22739-import-ref-exposure",
"pr": "#22770",
"session": "session_01JfJfBUC3cQ6hhgm9MQK76T — the dispatching seat's id (claim 6104521404), inherited by this subagent",
"premise_still_valid": true,
"summary": "REWORK round (verdict 6105610630), one item, done; the body of the fix (servesReferenceTarget, list, precedent-shaped refusal, fail-closed, toFailedResult untouched) is unchanged. (1) import-field-meta.ts now reads
reference: typeof f.reference === 'string' ? referenceTargetOf(f) : undefined: the set of fields the import/export/template doors treat as references is exactly main's (a field declaring a reference string), and that field's target is read through the arbiter, so the #22661 enumeration pin still classifies the caller decided (4/4). (2) Control pin added to core import-runner-reference-exposure.test.ts: a user field without reference has no target in the map and its cell reaches the write unresolved (no sys_user lookup, no exposure ask), as on main; a user field declaring reference is resolved; a non-reference type declaring reference names no target. Ablated (guard removed: 1 red / 13 green). (3) Residual narrowing census (AST over every git-tracked non-test .ts/.tsx/.mts/.js/.mjs/.json under packages/ and examples/, fixture and test dirs excluded; 3636 files at 0d326bf, 3638 at c06ec74, identical result): 0 shipped field declares reference on a non-reference type (0 object literals; the one literal pairing type with reference is driver-sql FIELD_KEY_STORAGE_CLASS, a key-classification table, not a field; 0 Field.non-reference-helper calls carrying reference); 22 undetermined hits are all schema/transform/generated-form/liveness code, none a field declaration; CONTROL 30 reference-typed literals and 80 Field.lookup/masterDetail/user/tree calls; positive control: a planted literal, a planted Field.text call and a planted non-literal type were all found. None, so the narrowing is stated in the changeset FROM/TO: the only population whose answer moves is the legacy, schema-refused type spelling reference (still listed in the doors' own type tables), which now names no target; every other non-reference type was never resolved by these doors. packages/rest export-format.test.ts fixture change kept. (4) Gates re-derived (same 69) and re-run at the final head; main merged. files_changed (vs merge base): .changeset/22739-import-reference-target-exposure.md; packages/core/src/utils/import-runner.ts; packages/core/src/utils/import-field-meta.ts; packages/core/src/utils/import-runner-reference-exposure.test.ts; packages/core/src/security/second-object-read-exposure.pin.test.ts; packages/qa/dogfood/test/import-reference-exposure.dogfood.test.ts; packages/rest/src/export-format.test.ts. Rework commits: f06912e (guard + control pin), 7610e85 (changeset), merges 0d326bf and c06ec74. line_budget: 7 files, +367 / -21 vs merge base (source +60/-5 in import-runner.ts and import-field-meta.ts, within 80; total over 250 for the two behaviour pins, now 122 + 126 lines, and a 33-line changeset). deviations: (a) origin/main merged TWICE this round: 0d326bf at the start, then c06ec74 because main's #22766 edited the enumeration pin (a textual conflict on the decided-list assertion, resolved by uniting both lists: servesExpansionTarget, servesLabelTarget, servesPayloadDisplayTarget, servesReferenceTarget, servesSummaryTitleTarget); the pin header's "All three now ask" became "Each read classified decided below now asks" inside that merge commit; everything re-verified at c06ec74; (b) the changeset cites the census at 0d326bf; re-run at c06ec74 gives the same result, not re-cited; (c) for the main reading a throwaway detached worktree at origin/main was created, built and removed; (d) PR body corrections owed beyond flag (h) (seat writes, dev does not PATCH): the Acceptance-notes bullet on user fields without reference (it described the retired widening) should say the field set is main's and the gap is carried by a separate card; the bullet on a non-string reference carrier throwing no longer holds (guarded, reads as no target as on main); the Legacy type reference bullet now describes the residual narrowing; the What-changed bullet on buildFieldMetaMap should say target read through the arbiter only for a field declaring reference; Verification numbers are superseded by this report's tests field. Worktree node_modules removed and worktree removed (no --force).","user_field_gap_on_main": "origin/main 996aa86 (detached worktree, dogfood closure built under the verify lock). COMMAND: PROBE_OUT=FILE pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/zz-probe-22739-user.dogfood.test.ts (scratch, never committed): a fixture object with a user field written WITHOUT reference (owner) and one written with Field.user, which declares reference sys_user (assignee); the signed-in administrator imports one row per field through POST /api/v1/data/OBJECT/import, each cell holding the administrator's own email. RESULT: owner row ok:false, code reference_not_found, field owner, error "Owner: no sys_user record has id" followed by the quoted cell (the raw text reached the write as an id and the engine refused it), nothing stored; assignee row ok:true, created, stored the administrator's user id. CONTROL LEGS on the same build: referenceTargetOf({type:user}) answers sys_user (and sys_user with reference written); buildFieldMetaMap({fields:{owner:{type:user}}}).get(owner).reference answers undefined. So the import door disagrees with the spec's arbiter for a user field written without reference. The rework build answers identically (same probe on this branch before the second merge, core dist carrying the guard, checked by grep: the same two rows), and the core control pins it.",
"tests": "REWORK PINS: core import-runner-reference-exposure.test.ts 14/14 (13 + the user-field control); enumeration pin 4/4; dogfood import-reference-exposure 11/11 plus #22661 second-object-exposure 25/25 (36/36). ABLATION on committed f06912e (scripts/ablation-replace.mjs WRAP, anchors 1 to 0, each restore blob==HEAD and git diff HEAD empty): G guard removed (reference: referenceTargetOf(f)) gives the control 1 red / 13 green; D import-field-meta back on the raw carrier gives the enumeration pin 2 red / 2 green; control run at HEAD 4/4. Round-1 ablations A, A2, A3, C stand (import-runner.ts unchanged). PACKAGES at c06ec74 after a full workspace build (72 tasks): core test 93 files/2349 passed, test:repo 5/55, typecheck exit 0 (incl. check:test-typecheck); dogfood the two pins 36/36, typecheck exit 0; rest typecheck exit 0 and its 38 test files reaching the import/export/template doors 38/38 (1223 passed, 22 skipped); plugin-auth admin-import-users 2 files/50; service-automation connector-pull 3 files/22. Same battery at 7610e85 (before the second merge) all exit 0, core 92/2347. LINT narrowed at c06ec74: eslint --no-inline-config on the 6 changed .ts files; population from --print-config (5-6 active rules each, none ignored); --format json 6 files, 0 errors, 0 warnings; eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move; repo-wide lint is CI's. GATES at c06ec74 (origin/main not ahead at reconcile time): dispatch-gates --commands derived 69 (same list as round 1; the dispatch's 51 a subset); --ran: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN, no stale-tree note. (The 7610e85 battery was also 69/69 exit 0 but reconciled with a STALE TREE note, which is what exposed main's edit to the enumeration pin.) Each: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 ; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 ; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 ; node scripts/check-changeset-no-major.mjs --self-test :: exit 0 ; node scripts/check-ci-filter-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs :: exit 0 ; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-adoption.mjs :: exit 0 ; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 ; node scripts/check-comment-mask-corpus.mjs :: exit 0 ; node scripts/check-dev-prereqs.mjs --self-test :: exit 0 ; node scripts/check-dts-emitted.mjs --self-test :: exit 0 ; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 ; node scripts/check-empty-changeset.mjs --self-test :: exit 0 ; node scripts/check-issue-citations.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs :: exit 0 ; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs :: exit 0 ; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs :: exit 0 ; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 ; node scripts/check-registry-log-declared.mjs :: exit 0 ; node scripts/check-registry-log-declared.mjs --self-test :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs :: exit 0 ; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 ; node scripts/check-system-context-census.mjs :: exit 0 ; node scripts/check-system-context-census.mjs --self-test :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs :: exit 0 ; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 ; node scripts/docs-audit/check-affected-docs.mjs :: exit 0 ; node scripts/docs-audit/check-drift-comment.mjs :: exit 0 ; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 ; node scripts/release-pending-publish.mjs --self-test :: exit 0 ; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 ; pnpm --filter @objectstack/spec run check:empty-state :: exit 0 ; pnpm --filter @objectstack/spec run check:liveness :: exit 0 ; pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 ; pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 ; pnpm check:changeset-gate-self-tests :: exit 0 ; pnpm check:cross-package-test-inputs :: exit 0 ; pnpm check:dispatcher-error-vocabulary :: exit 0 ; pnpm check:doc-authoring :: exit 0 ; pnpm check:driver-memory-census :: exit 0 ; pnpm check:dts-closure :: exit 0 ; pnpm check:dual-build-cjs-loads :: exit 0 ; pnpm check:engine-double-contract :: exit 0 ; pnpm check:error-code-casing :: exit 0 ; pnpm check:gitlink-declared :: exit 0 ; pnpm check:issue-citations :: exit 0 ; pnpm check:kernel-hook-pairs :: exit 0 ; pnpm check:lean-entry-closure :: exit 0 ; pnpm check:logger-receiver-detach :: exit 0 ; pnpm check:nul-bytes :: exit 0 ; pnpm check:objectql-double-limit :: exit 0 ; pnpm check:objectui-changeset :: exit 0 ; pnpm check:org-identifier :: exit 0 ; pnpm check:page-declaration-shape :: exit 0 ; pnpm check:pm-changeset-deadline-census :: exit 0 ; pnpm check:published-files :: exit 0 ; pnpm check:query-options-erasure :: exit 0 ; pnpm check:refd-timer-probe :: exit 0 ; pnpm check:slot-lookup :: exit 0 ; pnpm check:sourcemap-no-sources-content :: exit 0 ; pnpm check:test-source-alias :: exit 0 ; pnpm check:tier-file-adoption :: exit 0 ; pnpm check:type-check-coverage :: exit 0 ; pnpm check:type-check-debt :: exit 0 ; pnpm check:watch-hint-literal :: exit 0 ; pnpm check:where-matcher :: exit 0 CI not awaited.",
"mcp_calls": "0 — no MCP GitHub tool called this round; reads went through gh api REST GETs",
"api_writes": "1 this round — fleet-write relay (objectstack-fleet[bot]) POST /repos/objectstack-ai/objectstack/dispatches executing this os-dev-report comment = POST /repos//issues/22739/comments. Round 1 had 2 (pr_create with assignee, and report comment 6105593563). Zero label writes. git push not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: domain:engine seat 1 (it files the separate card, per verdict 6105610630) · the import door treats a user field written without reference as no reference while the spec's arbiter gives it sys_user; reading on main in user_field_gap_on_main; fixing it is a widening (Clause-②), not filed by the dev",
"carrier: 承接者:无 · plugin-auth identity import (admin-import-users.ts) hands runImport a hand-written protocol with no getMetaItem and reads under the system identity, so its lookup cells are matched unjudged; privileged admin door with platform-object targets; noted in PR Acceptance notes and the changeset, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
ACCEPT (seat review): PR #22770 at head
c06ec74a87domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T06:58Z. Claim 6104521404. Dev reports 6105593563 (round 1) and 6106294041 (rework). REWORK 6105610630, closed. Contract review PASS 6106439614 (CONTRACT_REVIEW_TIER, same head). Read against GitHub andorigin/main, not against the reports. ⛔ Classes, positions and functions only.Shape.
main, assignedos-project-manager,mergeable_stateclean.Fixes #22739. A closing-keyword scan of the whole body finds that line only.Clause-②: yes (narrowing), edited by this seat after the contract review. No accept set widens, but the exportedImportProtocolLikegains an optionalgetMetaItem?. That is additive and type-level, and the fleet spells ityes. The changeset's line follows on this PR's next head.The REWORK, closed.
main's: those declaring areferencestring. Each such target is read throughreferenceTargetOf.userfield withoutreferenceis unchanged, and pinned (ablation G: 1 red).type: 'reference'spelling) was measured: an AST census found 0 producers, with controls and a positive control. It is stated in FROM → TO.userfield written withoutreferencefailsreference_not_foundon import, while the spec's arbiter gives itsys_user#22785, which now carriesBlocked-by: #22739.What it does.
servesReferenceTargetaskscanServeApiOperation(enable, 'list'), the spec's one decision, read through the protocol'sgetMetaItem. It asks once per target per import, before any read.reference_not_found, the measured precedent for a target the caller cannot read. No presence signal is left, and nothing of the target is read.toFailedResultis untouched; [finding] import: a sandbox's own fault (CPU budget, wall-clock ceiling) reaches an import row as its debug wrapper, where the data doors answerInternal server error#22741 follows in this file.Evidence read.
dist), D (the raw carrier) and G (the guard).c06ec74a87:core93 files / 2,349 tests and its repo project 5 / 55;rest38 import/export/template files,plugin-auth,service-automation) green;Changeset checked.
@objectstack/coreminor,!, a**BREAKING**banner and one ADR-0087not-requiredmarker. FROM → TO covers both moved populations (the unserved target, and the legacy spelling), and the bindings match the diff.Files outside the claim.
packages/rest/src/export-format.test.ts(test-only,domain:cli): its fixture declaredreferenceon anumberfield, a key inert on that type. The PR body's wording was corrected in this act.Ruled, not filed in this fire (this fire's filing quota is spent): plugin-auth's identity import.
admin-import-users.tshandsrunImporta protocol withoutgetMetaItem, although it holdsdeps.getMetaItem. So its reference cells are matched without the exposure ask.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's class, and "reads run under the system identity" is not the family's exemption.sys_user's own reference targets. This seat files it as ap3sub-issue 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 in its next fire.Landing.
$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's checklist ticks row 7.mainafter this lands.Generated by Claude Code
Merge origin/main into claude/issue-22737-relation-filter-exposure
objectstack-fleet commentedon Oct 11, 2026
Landed: PR #22770 →
c74d843997·domain:engineseat 1 (#6367) ·session_01JfJfBUC3cQ6hhgm9MQK76T· 2026-10-11T07:27Z⛔ Classes, positions and functions only.
c74d843997. The two readings:origin/main.servesReferenceTargetis inimport-runner.ts, andimport-field-meta.tsreads a declaredreferencethroughreferenceTargetOf(the guarded form).gh-readonly-queue/main/pr-22770-*is gone.completedthroughFixes #22739.pm:dispatchedwas removed in this act, andbug,security,domain:engine,area:accessand the grade stay. The lane's closed set since 06:40Z is this card alone.@objectstack/core,minor, BREAKING narrowing;Clause-②: yes (narrowing)for the optionalImportProtocolLike.getMetaItem?):listbefore matching.reference_not_found, the precedent for a target the caller cannot read.$expand, or the dataset door's dimension-label pass, reads it — detail withheld pending maintainer #22661's checklist (6102873261) ticks row 7. The parent stays open for row 4 (security(data): a nested-relation filter condition on a lookup target is evaluated without asking the target's exposure (census row 4 of #22661) #22737, PR fix(metadata-protocol)!: a nested-relation filter condition asks the related object its declared exposure (#22737) #22768, which re-mergesmainnow) and 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,Blocked-by: #22737).Internal server error#22741 (p3,toFailedResult, the same file) and import: auserfield written withoutreferencefailsreference_not_foundon import, while the spec's arbiter gives itsys_user#22785 (p2, theuser-field widening, Clause-②). Both are dispatched by this seat next. Their files are disjoint.Generated by Claude Code
docs(changeset): the pending #22739 note no longer names the identity…