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
REST: clone and createMany report a readonly strip in the body's droppedFields but send no X-ObjectStack-Dropped-Fields header, unlike single create and update #28933
QA-source: #28927 · access-security.write-path-guards · observed beside the clauses
What happens
X-ObjectStack-Dropped-Fields (#3431) surfaces a legal write strip on "the REST write response". Single POST /data/:object and PATCH /data/:object/:id send it. Two other write doors that strip the same fields send only the body channel:
POST /data/:object/:id/clone → 201 with droppedFields:[{object, fields:[lead_score], reason:readonly}] in the body, no header;
a bare-array createMany POST /data/:object with […] → 201 with droppedFields in the body, no header.
A caller that reads the header (the documented "warning header" channel) misses these strips.
Measured at framework 7fa9873f (main, 2026-11-02) on a stock showcase boot, seen independently by two runners.
Reproduction
Boot the stock showcase.
As admin: POST /api/v1/data/showcase_contact{"name":"h1","lead_score":10} → 201 with header X-ObjectStack-Dropped-Fields: lead_score;reason=readonly (control).
As admin: POST /api/v1/data/showcase_contact[{"name":"h2","lead_score":10},{"name":"h3","lead_score":10}] → 201; body droppedFields names lead_score; no header.
As admin: POST /api/v1/data/showcase_contact/ID/clone{"overrides":{"lead_score":42}} → 201; body droppedFields names lead_score; no header.
Expected: the header on every 2xx write answer whose body carries droppedFields.
Where
applyDroppedFieldsHeader (packages/rest/src/error-response.ts) is called only from the single create and update handlers in packages/rest/src/crud-routes.ts (around L304 and L495); the clone and createMany handlers do not call it.
Read fresh. The clone and createMany handlers in packages/rest/src/crud-routes.ts never call applyDroppedFieldsHeader (packages/rest/src/error-response.ts), so these two doors report a readonly strip only in the body. That is inconsistent with the rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 header contract that single create and update keep. It is a user-visible behaviour defect, so it is p2 in domain:server, and with the location and repro named it goes to pm:queue.
Claim: PM loop round 4 · card thread · 2026-11-02T05:21Z
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5nE2GswUTcEX8HbTmt
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-gibefk (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#2
Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 split the queue and gave this card to seat 2. Seat's route: every 2xx REST write answer whose body carries droppedFields sends the header through the existing applyDroppedFieldsHeader; createMany gets the union of the rows' stripped fields.
File surface: packages/rest/src/crud-routes.ts write handlers (clone, createMany, and any other write door measured to lack the header); their tests; one dogfood pin; changeset
Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: medium
Clause-②: yes (widening) expected: a header added to answers; the dev measures and declares
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev routes the strip report through the one header seam, red first with ablation; the seat reviews and lands
Thread-read: none
Premise: measured by triage (6569529210) on main
Serial constraints cleared: none; overlap re-read of open PRs on crud-routes.ts before pr_create
Resume-from: none — fresh from origin/main
State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.
domain:server#2 · os-project-manager · session_01FEkQ5nE2GswUTcEX8HbTmt
os-dev-report
{
"issue": 28933,
"status": "done",
"branch": "claude/project-thread-gibefk",
"pr": "#29034",
"session": "session_01FEkQ5nE2GswUTcEX8HbTmt",
"premise_still_valid": true,
"summary": "Every 2xx REST write answer whose body carries droppedFields now sends X-ObjectStack-Dropped-Fields through the one seam applyDroppedFieldsHeader: clone, createMany (header = the body's union), per-object batch, updateMany and import commit (header = union of results[i].droppedFields), and the cross-object /batch (union of its per-op events). The seam takes an optional rows argument and emits each field;reason token once; single create/update values are unchanged. The stale roll-up 201 of clone and createMany now goes through earnedWriteDespiteStaleSummary so it carries the header too; an import dry run writes nothing and sends none. Spec describe text (clone, createMany, batch row), the client CloneDataResult docblock and data-api.mdx stopped saying no header; reference docs regenerated. Premise note: the card's bare-array createMany path POST /data/:object refuses arrays (400, #3899); the real door is POST /data/:object/createMany. File surface is wider than the claim's (crud-routes.ts is untouched): batch-routes.ts, data-action-routes.ts, error-response.ts, stale-summary-answer.ts, rest-server.ts (comment), spec batch.zod.ts and protocol.zod.ts (describe text only), client index.ts (docblock), content/docs/api/data-api.mdx, content/docs/references/api/batch.mdx and protocol.mdx (regenerated). The spec touch means a contract review is owed before landing.",
"tests": "At a6b0d83: new packages/rest/src/rest-dropped-fields-every-write-door.test.ts (15 cases, real ObjectQL + protocol on better-sqlite3); on main 135054e it went 11 failed / 4 passed (controls), on the fix 15/15. Header suites together (7 files): Tests 66 passed (66). pnpm --filter @objectstack/rest test: Test Files 367 passed (367), Tests 6951 passed, 332 skipped, VERDICT command-exit 0. rest + client typecheck (rest includes check:test-typecheck): VERDICT command-exit 0. Dogfood showcase-static-readonly (4 cases added): on main the clone header case red (null) and createMany cases red; on the fix Tests 8 passed (8). Ablation via scripts/ablation-replace.mjs (anchor hits 1, restore blob == HEAD each leg), each against the 15-case pin: clone success -> 1 red (clone overrides); clone stale answer -> 1 (clone earned); createMany success -> 3 (createMany strip cases); createMany stale answer -> 1; per-object batch -> 1; updateMany -> 1; import commit -> 1; import dry-run guard -> 1 (import case); cross-object -> 1; seam token dedupe -> 4 (updateMany, batch, import, cross-object); seam per-row reading -> 3 (updateMany, batch, import). Dist-resolved dogfood leg: clone + createMany calls replaced by markers, rest rebuilt, ablation-dist-preflight found both markers in dist, dogfood Tests 3 failed | 5 passed; restore leg rebuilt, preflight --absent exit 0 for both, tree clean, Tests 8 passed.",
"gates": "Local: dispatch-gates --commands derived 106; all 106 recorded :: exit 0 at a6b0d83; dispatch-gates --ran: 106 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). First pass had 8 x exit 3 (PREREQUISITE NOT MET: spec dist predated the describe edit; check-plugin-teardown-shape --self-test needed git fetch --depth=1 of 621a487) and check:generated exit 1 on the same stale spec dist; after pnpm --filter @objectstack/spec build and the fetch all 9 reran exit 0. Full ran list: /mnt/project-files/pm-orders/server/28933-gates-ran.txt. CI check-runs on #29034: in_progress at report time (not awaited, per os-dev).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one touched dogfood file was run: Tests 8 passed (8)",
"mcp_calls": "6 — issue_read x3 (28933 get, get_comments x2), list_pull_requests x1, pull_request_read get_files x1 (28731, overlap), plus this thread's hearthbot messaging; no GitHub write tool",
"api_writes": "2 — fleet-write pr_create (#29034, run 41399043192); this os-dev-report comment on #28933 via post-stamped. git push is not a REST write.",
"open_questions": [
{
"question": "Import dry run: send the header or not? It previews the strip in results[i].droppedFields but writes nothing.",
"options": [
"A: no header on a dry run (shipped): the header says a write's fields never landed",
"B: send it on the dry run too, mirroring the body"
],
"recommendation": "A, because the ruling names 2xx WRITE answers and a dry run is not one; flipping is one line (drop the dryRun guard), pinned by the import case."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/:id/clone measured on main 135054e in a real-stack rest test (a clone of a row whose readonly column ro2 is null answers droppedFields naming ro2) and on the stock showcase (a plain clone of a contact with null lead_score answers droppedFields naming lead_score); the console announces every non-empty droppedFields · evidence: cloneData copies the source row (including null readonly columns) and engine.insert's readonly strip reports a present key whatever its value, so a clone reports drops the caller never made, and with this PR the header repeats it · dedupe words: clone droppedFields null readonly, cloneData copies null readonly, clone phantom dropped field",
"carrier: none · noted, not filed: the runtime dispatcher's POST and PATCH /data arms (packages/runtime/src/domains/data.ts, the @objectstack/hono catch-all twin) set no X-ObjectStack-Dropped-Fields even for single create/update; reach not measured (whether that arm is reachable while RestServer mounts the data routes)"
]
}
Release: session_0186vfjHHMsRXC9EUT9hTFMo · cause: the maintainer's wrap-up (「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」, 「草稿的也交给下一任,在座位帖写清楚」); seat 2 signs off · next: PR #29034 is ACCEPTed (6570761103) with review PASS (6570720694) and is in the merge queue; Fixes #28933 closes this card on merge. Next: landing record, and file the clone null-readonly follow-up card (dev report 6570533136). ⛔ Do not re-dispatch while #29034 is queued.
Resume-from: claude/project-thread-gibefk@a6b0d839d45a4e08037beab8bd4efb60b2fcb369
State: pm:dispatched → pm:queue, assignee cleared, in this same relay stroke. Seat hand-off: mailbox #28284 comment 6571506250; seat post #20966.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: access-security.write-path-guards
Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:05Z
⛔ Not a claim.
applyDroppedFieldsHeader(packages/rest/src/error-response.ts), so these two doors report a readonly strip only in the body. That is inconsistent with the rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 header contract that single create and update keep. It is a user-visible behaviour defect, so it is p2 indomain:server, and with the location and repro named it goes topm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Claim: PM loop round 4 · card thread · 2026-11-02T05:21Z
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5nE2GswUTcEX8HbTmtAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-gibefk(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#2Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 split the queue and gave this card to seat 2. Seat's route: every 2xx REST write answer whose body carries
droppedFieldssends the header through the existingapplyDroppedFieldsHeader; createMany gets the union of the rows' stripped fields.File surface:
packages/rest/src/crud-routes.tswrite handlers (clone, createMany, and any other write door measured to lack the header); their tests; one dogfood pin; changesetContainer & model:
M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: mediumClause-②: yes (widening) expected: a header added to answers; the dev measures and declares
Metadata-protocol: none (expected; the dev measures and declares)
Responsibility: the dev routes the strip report through the one header seam, red first with ablation; the seat reviews and lands
Thread-read: none
Premise: measured by triage (
6569529210) on mainSerial constraints cleared: none; overlap re-read of open PRs on crud-routes.ts before pr_create
Resume-from: none — fresh from
origin/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_01FEkQ5nE2GswUTcEX8HbTmt
os-dev-report
{
"issue": 28933,
"status": "done",
"branch": "claude/project-thread-gibefk",
"pr": "#29034",
"session": "session_01FEkQ5nE2GswUTcEX8HbTmt",
"premise_still_valid": true,
"summary": "Every 2xx REST write answer whose body carries droppedFields now sends X-ObjectStack-Dropped-Fields through the one seam applyDroppedFieldsHeader: clone, createMany (header = the body's union), per-object batch, updateMany and import commit (header = union of results[i].droppedFields), and the cross-object /batch (union of its per-op events). The seam takes an optional rows argument and emits each field;reason token once; single create/update values are unchanged. The stale roll-up 201 of clone and createMany now goes through earnedWriteDespiteStaleSummary so it carries the header too; an import dry run writes nothing and sends none. Spec describe text (clone, createMany, batch row), the client CloneDataResult docblock and data-api.mdx stopped saying no header; reference docs regenerated. Premise note: the card's bare-array createMany path POST /data/:object refuses arrays (400, #3899); the real door is POST /data/:object/createMany. File surface is wider than the claim's (crud-routes.ts is untouched): batch-routes.ts, data-action-routes.ts, error-response.ts, stale-summary-answer.ts, rest-server.ts (comment), spec batch.zod.ts and protocol.zod.ts (describe text only), client index.ts (docblock), content/docs/api/data-api.mdx, content/docs/references/api/batch.mdx and protocol.mdx (regenerated). The spec touch means a contract review is owed before landing.",
"tests": "At a6b0d83: new packages/rest/src/rest-dropped-fields-every-write-door.test.ts (15 cases, real ObjectQL + protocol on better-sqlite3); on main 135054e it went 11 failed / 4 passed (controls), on the fix 15/15. Header suites together (7 files): Tests 66 passed (66). pnpm --filter @objectstack/rest test: Test Files 367 passed (367), Tests 6951 passed, 332 skipped, VERDICT command-exit 0. rest + client typecheck (rest includes check:test-typecheck): VERDICT command-exit 0. Dogfood showcase-static-readonly (4 cases added): on main the clone header case red (null) and createMany cases red; on the fix Tests 8 passed (8). Ablation via scripts/ablation-replace.mjs (anchor hits 1, restore blob == HEAD each leg), each against the 15-case pin: clone success -> 1 red (clone overrides); clone stale answer -> 1 (clone earned); createMany success -> 3 (createMany strip cases); createMany stale answer -> 1; per-object batch -> 1; updateMany -> 1; import commit -> 1; import dry-run guard -> 1 (import case); cross-object -> 1; seam token dedupe -> 4 (updateMany, batch, import, cross-object); seam per-row reading -> 3 (updateMany, batch, import). Dist-resolved dogfood leg: clone + createMany calls replaced by markers, rest rebuilt, ablation-dist-preflight found both markers in dist, dogfood Tests 3 failed | 5 passed; restore leg rebuilt, preflight --absent exit 0 for both, tree clean, Tests 8 passed.",
"gates": "Local: dispatch-gates --commands derived 106; all 106 recorded :: exit 0 at a6b0d83; dispatch-gates --ran: 106 run, 0 NOT-MEASURED, 0 UNRUN (exit 0). First pass had 8 x exit 3 (PREREQUISITE NOT MET: spec dist predated the describe edit; check-plugin-teardown-shape --self-test needed git fetch --depth=1 of 621a487) and check:generated exit 1 on the same stale spec dist; after pnpm --filter @objectstack/spec build and the fetch all 9 reran exit 0. Full ran list: /mnt/project-files/pm-orders/server/28933-gates-ran.txt. CI check-runs on #29034: in_progress at report time (not awaited, per os-dev).",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one touched dogfood file was run: Tests 8 passed (8)",
"mcp_calls": "6 — issue_read x3 (28933 get, get_comments x2), list_pull_requests x1, pull_request_read get_files x1 (28731, overlap), plus this thread's hearthbot messaging; no GitHub write tool",
"api_writes": "2 — fleet-write pr_create (#29034, run 41399043192); this os-dev-report comment on #28933 via post-stamped. git push is not a REST write.",
"open_questions": [
{
"question": "Import dry run: send the header or not? It previews the strip in results[i].droppedFields but writes nothing.",
"options": [
"A: no header on a dry run (shipped): the header says a write's fields never landed",
"B: send it on the dry run too, mirroring the body"
],
"recommendation": "A, because the ruling names 2xx WRITE answers and a dry run is not one; flipping is one line (drop the dryRun guard), pinned by the import case."
}
],
"out_of_scope_findings": [
"class: a · reach: POST /api/v1/data/:object/:id/clone measured on main 135054e in a real-stack rest test (a clone of a row whose readonly column ro2 is null answers droppedFields naming ro2) and on the stock showcase (a plain clone of a contact with null lead_score answers droppedFields naming lead_score); the console announces every non-empty droppedFields · evidence: cloneData copies the source row (including null readonly columns) and engine.insert's readonly strip reports a present key whatever its value, so a clone reports drops the caller never made, and with this PR the header repeats it · dedupe words: clone droppedFields null readonly, cloneData copies null readonly, clone phantom dropped field",
"carrier: none · noted, not filed: the runtime dispatcher's POST and PATCH /data arms (packages/runtime/src/domains/data.ts, the @objectstack/hono catch-all twin) set no X-ObjectStack-Dropped-Fields even for single create/update; reach not measured (whether that arm is reachable while RestServer mounts the data routes)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Release:
session_0186vfjHHMsRXC9EUT9hTFMo· cause: the maintainer's wrap-up (「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」, 「草稿的也交给下一任,在座位帖写清楚」); seat 2 signs off · next: PR #29034 is ACCEPTed (6570761103) with review PASS (6570720694) and is in the merge queue;Fixes #28933closes this card on merge. Next: landing record, and file the clone null-readonly follow-up card (dev report 6570533136). ⛔ Do not re-dispatch while #29034 is queued.Resume-from:
claude/project-thread-gibefk@a6b0d839d45a4e08037beab8bd4efb60b2fcb369State:
pm:dispatched→pm:queue, assignee cleared, in this same relay stroke. Seat hand-off: mailbox #28284 comment 6571506250; seat post #20966.Generated by Claude Code