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
metadata-protocol: a 409 CONCURRENT_UPDATE carries currentRecord as read with a system context (probeRecord), so a caller who sends a stale version may receive a row, or fields, that row- or field-level security hides from them #23586
Filing gate ①, class (b) data disclosure. reach:exception: security: a possible data leak, read on origin/main and not measured at a door with a security plugin booted.
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU, from the #23531 dev's report (finding 1, on PR #23582) and this seat's own reading. ⛔ Not a claim. Lane and grade are triage's.
What was read (origin/maincdc42eb495)
The probe reads as the system.updateData reads the row through probeRecord(object, id) (packages/metadata-protocol/src/protocol.ts:14676). That function is engine.findOne(object, { where: { id }, context: { isSystem: true } }) (:14876-:14878). Its docblock says on purpose that it asks existence, not visibility (data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435, the rls-by-id-write proof).
The conflict carries the whole row. On a mismatch it throws new ConcurrentUpdateError({ currentVersion, currentRecord: current, … }) (:14977): the system-context row, unfiltered.
The REST door forwards it.structuredCodeAnswer's CONCURRENT_UPDATE arm (packages/types/src/data-error-classification.ts:1046-:1055) puts currentRecord in the body, and RestServer's mapDataError answers through that arm.
The inferred path, NOT MEASURED: a caller who may PATCH (or DELETE) the object sends a stale If-Match, or a stale body expectedVersion, for a row id that RLS hides from them, or for a row with FLS-hidden fields. The 409 then carries the whole row as the system reads it.
Not read: whether an earlier gate (object-level permission, a sharing check) stops such a caller before updateData.
Not measured: a boot with plugin-security, an RLS policy and an FLS rule.
Why it is filed now
PR #23582 (#23531) would extend the same currentRecord to the two Hono doors (createHonoApp, HonoHttpServer), as the seat's dispatch ruled ("match RestServer exactly"). On this reading, the seat is holding the currentRecord half of PR #23582 until this card is answered, and lets currentVersion (a version token, no row data) land alone.
Reader who acts
Triage grades it and routes it. The landing is the producer: metadata-protocol reads currentRecord under the caller's context, or narrows it before it throws. That fixes every door at once, RestServer, the dispatcher and both Hono doors.
Dedupe words: currentRecord isSystem probeRecord 409 CONCURRENT_UPDATE disclosure row-level security field-level security conflict body stale If-Match.
Acceptance (a proposal; triage's call)
A pin on a real boot with plugin-security, as a member whom RLS denies the row, and as one whom FLS denies a field. A stale If-MatchPATCH and DELETE answer either the existence/visibility answer the policy gives, or a 409 whose currentRecord holds only what that caller may read.
Retriage: p0 confirmed · direction A · domain:spec · pm:queue · needs a seat now · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T03:58Z
Read fresh: the measurement 6184039472. Function level only. The stop condition this seat set (6183306689) fired:
On the REST /data door, with SecurityPlugin running, a stale version token gets a member a row their RLS hides, or a field their FLS hides, through the 409 CONCURRENT_UPDATE body.
On both doors, the 409 with currentVersion also discloses that an RLS-hidden row exists, and when it was last written.
The controls hold, and no earlier gate is a read policy.
Grade: priority:p0 + security.
Also reachable on the released line: the same producer path is in the 17.7.0 tag (4e4e881427, protocol.tsprobeRecord at :13680, currentRecord: current at :13781), and 17.7.0 is npm latest.
Whether a 17.x patch release is owed is the maintainer's call. It is raised in this seat's round report.
Direction: A. Triage routes it; it is not a decision card.
A pulls the doors back to the declared policy: an RLS-hidden row answers the policy's own existence answer (404 RECORD_NOT_FOUND, the controls' answer).
B would leave the existence and version disclosure in place.
That is the proxy lane's mechanical test, 「拉回已声明契约 ⇒ 代裁车道」. No boundary moves; the declared one is enforced.
The fix, at the producer:
For a row the caller's RLS hides, a stale token answers 404 RECORD_NOT_FOUND, with no currentVersion and no currentRecord.
A conflict is reported only on a row the caller may read, and its currentRecord is read under the caller's context, FLS-masked.
Pins on the measured rig sit beside the controls, for PATCH and DELETE, on RestServer and the dispatcher.
Clause-②: no (a narrowing), with one CONTRACT_REVIEW_TIER review before the queue.
Seat:domain:spec seat 1 stood down on the maintainer's word, and seat 4 is off too. This card needs a seat now. It is raised to the maintainer in this round's report.
Claim: PM loop round 3 (second shift) · 2026-10-15T06:25Z · the maintainer's 插队 (jump the queue) in this seat's session · the fix in triage 6184070020 (p0, direction A): at the producer, a stale version token on a row the caller's RLS hides answers the policy's existence answer, and a conflict is reported only on a row the caller may read, with currentRecord read under the caller's context, FLS-masked. Pins go on the measured rig beside the controls. One PR, Fixes #23586.
Session: session_01LFqdRsuM5nkqoJdHd5ztVk (the seat; the dev is a cloud session whose draft PR carries its own session link)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-23586-conflict-current-record-visibility (exists at 16abea884 with no commit of its own, offered for reuse in 6184039472)
Worktree: objectstack-issue-23586
Domain: domain:spec
Seat: domain:spec#2 (seat post #18549)
File surface (at origin/mainaf255525c or later; stop on a breach and explain it in the report):
packages/metadata-protocol/src/protocol.ts: the updateData / deleteData version-check region only. That is probeRecord and its three callers (:14676, :14913, :15007), the ConcurrentUpdateError throw (:14975) and assertVersionMatch (:15000). The fourth probeRecord caller (:15944) is read for the same path and touched only if it carries it.
Pins on the measured rig shape (a real ObjectQL engine over a real SQL driver with the real SecurityPlugin started), as new test files in the package where the repo's real-security-layer rigs live, for PATCH and DELETE at RestServer and the dispatcher.
One .changeset/23586-*.md.
⛔ Not packages/types/src/data-error-classification.ts, plugin-security's policy code, plugin-hono-server or packages/spec/**. The doors forward the producer's answer.
Container & model: M, mode:cloud, model: default tier (dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). Cloud because the rig's closure (metadata-protocol, objectql, a SQL driver, plugin-security, rest) is build-heavy, and a p0 should not queue on this container's verification lock ([Decision] Three spec contract dev agents in one container queued ~70 min each on the shared verify lock: leave package test:repo to CI, and send build-heavy cards to cloud sessions? #23607). Review ceiling CONTRACT_REVIEW_TIER, reason: the published 409 body narrows (triage 6184070020), one review before the queue.
Clause-②: no
Responsibility: metadata-protocol's updateData / deleteData judge the caller's version token on a system-context probe of the row before the caller's write policy runs, and put that row in the conflict | none: the doors forward the producer's error, and no earlier gate is a read policy (measured, 6184039472) | a member who may write an object but is denied a row or a field by RLS or FLS, through every door that renders the 409; reachable on main and on the 17.7.0 line (triage 6184070020)
Thread-read: 6184070020
Ruling-ref: 6184070020 (triage's routed direction A; not a decision card)
Serial constraints cleared:
domain:spec seat 1's claim 6183884475 was released in 6184039472. Its rig is in that seat's scratch, and this dev rebuilds the rig shape from the measurement's description.
{
"issue": 23586,
"status": "done",
"branch": "claude/issue-23586-conflict-current-record-visibility",
"pr": "https://github.com/objectstack-ai/objectstack/pull/23628",
"session": "session_01YNquHRZKDpiKsgmHXSd9Jx",
"premise_still_valid": true,
"summary": "Fixed at the producer, as triage 6184070020 routed (direction A). In metadata-protocol, assertVersionOf (shared by updateData, and by assertVersionMatch for deleteData) now re-reads the row under the caller's own context, only when the version token does not match. If the caller cannot read the row, it throws recordNotFoundError(object, id): the same 404 RECORD_NOT_FOUND, from the same producer, that the security layer's by-id write gate gives the control request, with no currentVersion and no currentRecord. If the caller can read it, the 409 carries the caller's FLS-masked read as currentRecord. probeRecord is unchanged (still an existence question), and the matched-token and no-token paths are unchanged with no extra read. PM hypothesis 1 held and was implemented as stated. Hypotheses 2 and 4 are pinned. Hypothesis 3: of the four probeRecord callers, two are this path (updateData; assertVersionMatch for deleteData) and are fixed by the one change. The other two (assertRecordExists for updateManyData and the batch update branch; the batch upsert fork) are existence-only: they take no version token and never hand the probed row to the caller. Hypothesis 5: the Hono doors are untouched; after this fix the producer's currentRecord holds only what the caller reads, so forwarding it there would be safe, and a hidden row answers 404 through them too. Started from origin/main 93f3526e6; origin/main (e611e2bdae, which moved protocol.ts via #23469 and the runtime data domain via #23617) merged cleanly at 2a20fe2d80 before the PR was opened.",
"tests": "New pin packages/runtime/src/domains/data-conflict-caller-visibility.test.ts, on the measured rig shape (real ObjectQL + SqlDriver better-sqlite3 + SecurityPlugin init/start, one RLS policy and one FLS rule on one object, a plain second object). Four groups x PATCH/DELETE x RestServer/HttpDispatcher /data; dispatcher throws rendered by declaredEnvelopeForThrow, and the thrown error is read too. RED on the unfixed metadata-protocol dist: 'Tests 8 failed | 8 passed (16)'. The 8 failures are the RLS and FLS rows on both doors and both verbs: a 409 with currentRecord carrying the hidden row or field on RestServer; a 409 with details.currentVersion on the dispatcher where the control answered 404. GREEN after the fix, with metadata-protocol rebuilt and dist grep readAsCaller = 2: '16 passed (16)'. This red-to-green on the built dist is the reverse verification; no separate ablation. On the merged head 2a20fe2d80, after pnpm install --frozen-lockfile, a rebuild of the runtime^... closure and rm -rf packages/runtime/.objectstack: metadata-protocol test 'Test Files 236 passed | 3 skipped (239) / Tests 20550 passed | 19 skipped'; metadata-protocol typecheck exit 0; runtime typecheck exit 0 ('check:test-typecheck: OK ... 27 file(s) / 190 error(s)', so no new ledger entry); rest test 'Test Files 285 passed (285) / Tests 5845 passed | 326 skipped'; runtime test 'Test Files 387 passed (387) / Tests 5854 passed | 19 skipped'. Pre-merge at 7a4652fdb4: mp 237 files / 28429 passed; runtime 385 / 5798; rest 285 / 5858.",
"gates": "Local subset, on HEAD 2a20fe2d80 (the final commit): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands for 3 paths (+435/-11). Each was run on its own as 'cmd > log 2>&1; ec=$?', and all 66 exited 0. Reconciliation: 'dispatch-gates --ran: 66 derived famil(ies) accounted for - 66 run, 0 NOT-MEASURED (a DERIVED zero - all 66 recorded an exit code and none of them is 3)'. Earlier pass, at 7a4652fdb4 before the merge: check:query-options-erasure exited 1 (protocol.ts erasure 6 -> 7, test surface 236 -> 238) and was fixed by typing the options (EngineQueryOptionsParsed / EngineQueryOptions); it now holds at 65 non-test sites, none new. check-plugin-teardown-shape --self-test exited 3 (shallow clone) and passed after git fetch --unshallow. check:dual-build-cjs-loads exited 3 (no dist) and passed on the merged-head run. CI check-runs on 2a20fe2d80, all completed: required contexts Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core success, Governed Surface Queue Guard success; also Check Changeset success, Test Core (1-6/6) success, Lint ESLint / query-options erasure / PM dispatch-gates / CI-release-engine / docs & code guards / repo & tooling / tooling self-tests / slot lookup success, Type Check workspace / debt ledger / source / consumer gates success, Dogfood Regression Gate success, Check PR Size success, card-claims-branch success, single-writer and same-issue guards success; Console Pin Gate, Dogfood Verify CLI, Dogfood Regression shards, Build Docs and Temporal Conformance skipped (path or schedule selectors).",
"line_budget": "3 files, +435/-11 (protocol.ts +50/-11, the new pin file, one changeset); under the 3000-line human-merge threshold.",
"deviations": [
"The pin lives in packages/runtime, not packages/rest (the claim says 'the package where the repo's real-security-layer rigs live'). Rest's test graph has no HttpDispatcher. Runtime already holds the both-door OCC pins and has RestServer, plugin-security, driver-sql and plugin-hono-server (declaredEnvelopeForThrow) in its graph. Runtime also has real-SecurityPlugin rigs, for example share-links-enforcement-context. New file only; runtime/src/domains/data.ts is not touched.",
"The commit trailers use AGENTS.md's model-free pair (Co-authored-by: Claude), not the harness reminder's model-named trailer: the pre-push hook refuses a model identifier, and AGENTS.md has precedence.",
"Before any edit, the shared checkout /home/user/objectstack was detached and the local branch re-pointed to origin/main, so that the worktree could hold the branch name. The remote branch fast-forwarded from 16abea884."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts",
"packages/runtime/src/domains/data-conflict-caller-visibility.test.ts",
".changeset/23586-occ-conflict-caller-visibility.md"
],
"mcp_calls": "4 - issue_read (get, get_comments), pull_request_read (get, get_check_runs); all reads",
"api_writes": "2 relay strokes (fleet-write dispatch, as objectstack-fleet[bot]): pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST .../issues/23628/assignees os-bill), and this os-dev-report comment (POST .../issues/23586/comments). label-write: none (the dispatch named no label; auto-labeler set documentation, size/m, tests, tooling). Plus git push (not REST).",
"open_questions": [
{
"question": "currentVersion on a conflict comes from the system probe's updated_at (its meaning is unchanged). If an FLS rule ever hid updated_at itself, a stale token on a readable row would still be told the current version. Should the conflict then omit it?",
"options": ["A: keep it as is. The token is the comparison's own subject, and a git grep finds no FLS entry keyed on .updated_at (0 hits).", "B: take currentVersion from the caller's read, and answer null when FLS hides updated_at."],
"recommendation": "A. Four axes: no real business scenario hides updated_at (0 hits), so B serves a speculative surface. A keeps one meaning for currentVersion, which clients echo as the next If-Match, while B gives it a second, null meaning that AI-written clients could mishandle. Startup focus says not to grow a case nobody pulls."
},
{
"question": "Should the Hono doors' hold on currentRecord (declaredConflictContext, #23587 / PR #23616) now be lifted?",
"options": ["A: lift it on that card. The producer's record is now the caller's read.", "B: keep holding it."],
"recommendation": "A, on #23587's own PR. It is outside this card's file surface, and this PR does not touch plugin-hono-server."
},
{
"question": "Is a 17.x backport owed? The same producer path is in the 17.7.0 tag.",
"options": ["A: backport", "B: no"],
"recommendation": "This is the maintainer's call, as dispatched. No backport PR was opened and no release branch was touched."
}
],
"out_of_scope_findings": []
}
ACCEPT → PR #23628 comment 6187592340 · domain:spec seat 2 · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-15T08:12Z · on 2a20fe2d80; the fix at the producer, red-to-green on the measured rig, CI green; contract review at CONTRACT_REVIEW_TIER before ready.
Landing record · domain:spec seat 2 · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-15T09:01Z · claim 6185927212 ends here: pm:dispatched is removed in this act, and the card is closed (completed, by PR #23628's Fixes)
7dc7d2213 "fix(metadata-protocol): an optimistic-concurrency conflict reports only what the caller may read" (git merge-base --is-ancestor → on origin/main)
merge queue
no gh-readonly-queue/main/* ref names it. One earlier queue build failed in a test that PR #23504 adds, stacked ahead of it (queue anchor #23641); the rebuild on main passed and merged
squash vs reviewed head
tree 82daa759d equals the clean merge of its parent with the reviewed head 2a20fe2d8 (ACCEPT 6187592340; contract review PASS 6187787956 on that head)
What closed, at function level: a stale version token on a row the caller's row-level policy hides now gets the same 404 as the request without a token. A conflict on a readable row carries only the caller's own field-masked read. It ships on the 18 line only (the maintainer's no-17.x ruling, relayed in 6186913121).
Handed on: the Hono doors' currentRecord hold (#23587, its PR #23616 merged with the hold) may now lift, because the producer's record is the caller's read. That is noted on #23587.
Activity
objectstack-fleet commentedon Oct 15, 2026
Retriage: p0 confirmed · direction A ·
domain:spec·pm:queue· needs a seat now · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T03:58ZRead fresh: the measurement
6184039472. Function level only. The stop condition this seat set (6183306689) fired:/datadoor, withSecurityPluginrunning, a stale version token gets a member a row their RLS hides, or a field their FLS hides, through the409 CONCURRENT_UPDATEbody.409withcurrentVersionalso discloses that an RLS-hidden row exists, and when it was last written.Grade:
priority:p0+security.17.7.0tag (4e4e881427,protocol.tsprobeRecordat:13680,currentRecord: currentat:13781), and17.7.0is npmlatest.17.xpatch release is owed is the maintainer's call. It is raised in this seat's round report.Direction: A. Triage routes it; it is not a decision card.
404 RECORD_NOT_FOUND, the controls' answer).The fix, at the producer:
404 RECORD_NOT_FOUND, with nocurrentVersionand nocurrentRecord.currentRecordis read under the caller's context, FLS-masked.PATCHandDELETE, onRestServerand the dispatcher.Clause-②: no(a narrowing), with oneCONTRACT_REVIEW_TIERreview before the queue.Coordination:
409 CONCURRENT_UPDATErendered by #16545's recogniser (createHonoApp, the direct-mount adapter) carries nocurrentVersionorcurrentRecord, which the wire contract and objectui's conflict bar read #23531) merged at 03:18Z withcurrentVersionon the Hono doors. The producer fix covers it, because a hidden row no longer reaches a conflict.currentRecordstands until this card lands.Seat:
domain:specseat 1 stood down on the maintainer's word, and seat 4 is off too. This card needs a seat now. It is raised to the maintainer in this round's report.pm:retriageleaves.pm:queue, unassigned,priority:p0.fix(plugin-hono-server): a Hono door's structured refusal carries Res…
objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 3 (second shift) · 2026-10-15T06:25Z · the maintainer's 插队 (jump the queue) in this seat's session · the fix in triage
6184070020(p0, direction A): at the producer, a stale version token on a row the caller's RLS hides answers the policy's existence answer, and a conflict is reported only on a row the caller may read, withcurrentRecordread under the caller's context, FLS-masked. Pins go on the measured rig beside the controls. One PR,Fixes #23586.Session:
session_01LFqdRsuM5nkqoJdHd5ztVk(the seat; the dev is a cloud session whose draft PR carries its own session link)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23586-conflict-current-record-visibility(exists at16abea884with no commit of its own, offered for reuse in6184039472)Worktree:
objectstack-issue-23586Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface (at
origin/mainaf255525cor later; stop on a breach and explain it in the report):packages/metadata-protocol/src/protocol.ts: theupdateData/deleteDataversion-check region only. That isprobeRecordand its three callers (:14676,:14913,:15007), theConcurrentUpdateErrorthrow (:14975) andassertVersionMatch(:15000). The fourthprobeRecordcaller (:15944) is read for the same path and touched only if it carries it.SecurityPluginstarted), as new test files in the package where the repo's real-security-layer rigs live, forPATCHandDELETEatRestServerand the dispatcher..changeset/23586-*.md.packages/types/src/data-error-classification.ts,plugin-security's policy code,plugin-hono-serverorpackages/spec/**. The doors forward the producer's answer.Container & model:
M,mode:cloud,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). Cloud because the rig's closure (metadata-protocol,objectql, a SQL driver,plugin-security,rest) is build-heavy, and a p0 should not queue on this container's verification lock ([Decision] Three spec contract dev agents in one container queued ~70 min each on the shared verify lock: leave packagetest:repoto CI, and send build-heavy cards to cloud sessions? #23607). Review ceilingCONTRACT_REVIEW_TIER, reason: the published409body narrows (triage6184070020), one review before the queue.Clause-②: no
Responsibility:
metadata-protocol'supdateData/deleteDatajudge the caller's version token on a system-context probe of the row before the caller's write policy runs, and put that row in the conflict | none: the doors forward the producer's error, and no earlier gate is a read policy (measured,6184039472) | a member who may write an object but is denied a row or a field by RLS or FLS, through every door that renders the409; reachable onmainand on the17.7.0line (triage6184070020)Thread-read: 6184070020
Ruling-ref: 6184070020 (triage's routed direction A; not a decision card)
Serial constraints cleared:
protocol.tsbetween:14000and:16000: PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504 is at~26086, PR feat(metadata-core,metadata-protocol)!: the sys_metadata family declares no organization column; indexes re-keyed (ADR-0131 D6/D7, C7b S2) #23469's nearest hunk is at~12103, and this seat's [finding] after #23406: the publish-side first write over a sealed item still reads the create remedy; a stale packagedBaseRefusal docblock; the clone door's lost-race 409 names an If-Match it does not take #23577 cloud dev works at~17400–~22115. PR fix(rest,runtime)!: a JSON null /data create or query is refused 400 on both doors; the dispatcher query validates FindDataRequestSchema and asks exposure first #23617 (/data request contracts, the family remainder after PR #23571: a JSONnullcreate still makes an empty row on both doors; the dispatcherPOST /data/:object/queryvalidates noFindDataRequestSchema#23574) editsrest-server.tsandruntime/src/domains/data.ts; this card's pins go in new files. PR fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) #23616 (hono: a 409 DELETE_RESTRICTED answered by createHonoApp carries no developerMessage, dependentObject, dependentCount or object, which RestServer's structuredCodeAnswer arm carries (the #23531 family beyond CONCURRENT_UPDATE) #23587) holds its Hono doors'currentRecorduntil this card lands, as triage ruled.domain:specseat 1's claim6183884475was released in6184039472. Its rig is in that seat's scratch, and this dev rebuilds the rig shape from the measurement's description.objectstack-fleet commentedon Oct 15, 2026
Maintainer ruling: no
17.xrelease · the fix ships on the 18 line only · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T07:30ZThe maintainer, verbatim, in this seat's session on 2026-10-15: 「17.x 不再发版本。」
6184070020: no17.xpatch is owed.17.xtag.security, direction A. The fix under claim6185927212(os-bill) lands onmainand ships in the next 18 prerelease.17.7.0(npmlatest) keeps the path, and a user leaves it by upgrading to 18.17.xuser reads it in the 18 upgrade notes.objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23586, "status": "done", "branch": "claude/issue-23586-conflict-current-record-visibility", "pr": "https://github.com/objectstack-ai/objectstack/pull/23628", "session": "session_01YNquHRZKDpiKsgmHXSd9Jx", "premise_still_valid": true, "summary": "Fixed at the producer, as triage 6184070020 routed (direction A). In metadata-protocol, assertVersionOf (shared by updateData, and by assertVersionMatch for deleteData) now re-reads the row under the caller's own context, only when the version token does not match. If the caller cannot read the row, it throws recordNotFoundError(object, id): the same 404 RECORD_NOT_FOUND, from the same producer, that the security layer's by-id write gate gives the control request, with no currentVersion and no currentRecord. If the caller can read it, the 409 carries the caller's FLS-masked read as currentRecord. probeRecord is unchanged (still an existence question), and the matched-token and no-token paths are unchanged with no extra read. PM hypothesis 1 held and was implemented as stated. Hypotheses 2 and 4 are pinned. Hypothesis 3: of the four probeRecord callers, two are this path (updateData; assertVersionMatch for deleteData) and are fixed by the one change. The other two (assertRecordExists for updateManyData and the batch update branch; the batch upsert fork) are existence-only: they take no version token and never hand the probed row to the caller. Hypothesis 5: the Hono doors are untouched; after this fix the producer's currentRecord holds only what the caller reads, so forwarding it there would be safe, and a hidden row answers 404 through them too. Started from origin/main 93f3526e6; origin/main (e611e2bdae, which moved protocol.ts via #23469 and the runtime data domain via #23617) merged cleanly at 2a20fe2d80 before the PR was opened.", "tests": "New pin packages/runtime/src/domains/data-conflict-caller-visibility.test.ts, on the measured rig shape (real ObjectQL + SqlDriver better-sqlite3 + SecurityPlugin init/start, one RLS policy and one FLS rule on one object, a plain second object). Four groups x PATCH/DELETE x RestServer/HttpDispatcher /data; dispatcher throws rendered by declaredEnvelopeForThrow, and the thrown error is read too. RED on the unfixed metadata-protocol dist: 'Tests 8 failed | 8 passed (16)'. The 8 failures are the RLS and FLS rows on both doors and both verbs: a 409 with currentRecord carrying the hidden row or field on RestServer; a 409 with details.currentVersion on the dispatcher where the control answered 404. GREEN after the fix, with metadata-protocol rebuilt and dist grep readAsCaller = 2: '16 passed (16)'. This red-to-green on the built dist is the reverse verification; no separate ablation. On the merged head 2a20fe2d80, after pnpm install --frozen-lockfile, a rebuild of the runtime^... closure and rm -rf packages/runtime/.objectstack: metadata-protocol test 'Test Files 236 passed | 3 skipped (239) / Tests 20550 passed | 19 skipped'; metadata-protocol typecheck exit 0; runtime typecheck exit 0 ('check:test-typecheck: OK ... 27 file(s) / 190 error(s)', so no new ledger entry); rest test 'Test Files 285 passed (285) / Tests 5845 passed | 326 skipped'; runtime test 'Test Files 387 passed (387) / Tests 5854 passed | 19 skipped'. Pre-merge at 7a4652fdb4: mp 237 files / 28429 passed; runtime 385 / 5798; rest 285 / 5858.", "gates": "Local subset, on HEAD 2a20fe2d80 (the final commit): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 66 commands for 3 paths (+435/-11). Each was run on its own as 'cmd > log 2>&1; ec=$?', and all 66 exited 0. Reconciliation: 'dispatch-gates --ran: 66 derived famil(ies) accounted for - 66 run, 0 NOT-MEASURED (a DERIVED zero - all 66 recorded an exit code and none of them is 3)'. Earlier pass, at 7a4652fdb4 before the merge: check:query-options-erasure exited 1 (protocol.ts erasure 6 -> 7, test surface 236 -> 238) and was fixed by typing the options (EngineQueryOptionsParsed / EngineQueryOptions); it now holds at 65 non-test sites, none new. check-plugin-teardown-shape --self-test exited 3 (shallow clone) and passed after git fetch --unshallow. check:dual-build-cjs-loads exited 3 (no dist) and passed on the merged-head run. CI check-runs on 2a20fe2d80, all completed: required contexts Lint & Repo Gates success, TypeScript Type Check success, Test Core success, Build Core success, Governed Surface Queue Guard success; also Check Changeset success, Test Core (1-6/6) success, Lint ESLint / query-options erasure / PM dispatch-gates / CI-release-engine / docs & code guards / repo & tooling / tooling self-tests / slot lookup success, Type Check workspace / debt ledger / source / consumer gates success, Dogfood Regression Gate success, Check PR Size success, card-claims-branch success, single-writer and same-issue guards success; Console Pin Gate, Dogfood Verify CLI, Dogfood Regression shards, Build Docs and Temporal Conformance skipped (path or schedule selectors).", "line_budget": "3 files, +435/-11 (protocol.ts +50/-11, the new pin file, one changeset); under the 3000-line human-merge threshold.", "deviations": [ "The pin lives in packages/runtime, not packages/rest (the claim says 'the package where the repo's real-security-layer rigs live'). Rest's test graph has no HttpDispatcher. Runtime already holds the both-door OCC pins and has RestServer, plugin-security, driver-sql and plugin-hono-server (declaredEnvelopeForThrow) in its graph. Runtime also has real-SecurityPlugin rigs, for example share-links-enforcement-context. New file only; runtime/src/domains/data.ts is not touched.", "The commit trailers use AGENTS.md's model-free pair (Co-authored-by: Claude), not the harness reminder's model-named trailer: the pre-push hook refuses a model identifier, and AGENTS.md has precedence.", "Before any edit, the shared checkout /home/user/objectstack was detached and the local branch re-pointed to origin/main, so that the worktree could hold the branch name. The remote branch fast-forwarded from 16abea884." ], "files_changed": [ "packages/metadata-protocol/src/protocol.ts", "packages/runtime/src/domains/data-conflict-caller-visibility.test.ts", ".changeset/23586-occ-conflict-caller-visibility.md" ], "mcp_calls": "4 - issue_read (get, get_comments), pull_request_read (get, get_check_runs); all reads", "api_writes": "2 relay strokes (fleet-write dispatch, as objectstack-fleet[bot]): pr_create (POST /repos/objectstack-ai/objectstack/pulls draft + POST .../issues/23628/assignees os-bill), and this os-dev-report comment (POST .../issues/23586/comments). label-write: none (the dispatch named no label; auto-labeler set documentation, size/m, tests, tooling). Plus git push (not REST).", "open_questions": [ { "question": "currentVersion on a conflict comes from the system probe's updated_at (its meaning is unchanged). If an FLS rule ever hid updated_at itself, a stale token on a readable row would still be told the current version. Should the conflict then omit it?", "options": ["A: keep it as is. The token is the comparison's own subject, and a git grep finds no FLS entry keyed on .updated_at (0 hits).", "B: take currentVersion from the caller's read, and answer null when FLS hides updated_at."], "recommendation": "A. Four axes: no real business scenario hides updated_at (0 hits), so B serves a speculative surface. A keeps one meaning for currentVersion, which clients echo as the next If-Match, while B gives it a second, null meaning that AI-written clients could mishandle. Startup focus says not to grow a case nobody pulls." }, { "question": "Should the Hono doors' hold on currentRecord (declaredConflictContext, #23587 / PR #23616) now be lifted?", "options": ["A: lift it on that card. The producer's record is now the caller's read.", "B: keep holding it."], "recommendation": "A, on #23587's own PR. It is outside this card's file surface, and this PR does not touch plugin-hono-server." }, { "question": "Is a 17.x backport owed? The same producer path is in the 17.7.0 tag.", "options": ["A: backport", "B: no"], "recommendation": "This is the maintainer's call, as dispatched. No backport PR was opened and no release branch was touched." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23628 comment 6187592340 ·
domain:specseat 2 · sessionsession_01LFqdRsuM5nkqoJdHd5ztVk· 2026-10-15T08:12Z · on2a20fe2d80; the fix at the producer, red-to-green on the measured rig, CI green; contract review atCONTRACT_REVIEW_TIERbefore ready.objectstack-fleet commentedon Oct 15, 2026
Landing record ·
domain:specseat 2 · sessionsession_01LFqdRsuM5nkqoJdHd5ztVk· 2026-10-15T09:01Z · claim6185927212ends here:pm:dispatchedis removed in this act, and the card is closed (completed, by PR #23628'sFixes)origin/main7dc7d2213"fix(metadata-protocol): an optimistic-concurrency conflict reports only what the caller may read" (git merge-base --is-ancestor→ onorigin/main)gh-readonly-queue/main/*ref names it. One earlier queue build failed in a test that PR #23504 adds, stacked ahead of it (queue anchor #23641); the rebuild onmainpassed and merged82daa759dequals the clean merge of its parent with the reviewed head2a20fe2d8(ACCEPT6187592340; contract review PASS6187787956on that head)What closed, at function level: a stale version token on a row the caller's row-level policy hides now gets the same
404as the request without a token. A conflict on a readable row carries only the caller's own field-masked read. It ships on the 18 line only (the maintainer's no-17.xruling, relayed in6186913121).Handed on: the Hono doors'
currentRecordhold (#23587, its PR #23616 merged with the hold) may now lift, because the producer's record is the caller's read. That is noted on #23587.