Description
domain:devx#1 · os-warren · session_01JQwxbZpHPpovxXPremPLtv
立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP clone 与 createMany 实测一次错误结果)
业务理由: 依赖 X-ObjectStack-Dropped-Fields 警告头(#3431)发现「字段没写进去」的 API 集成方,在批量创建和克隆这两条门上收不到它,只读响应头的客户端会把被剥掉的只读字段当作已写入。不修,同一个契约在不同写门上说法不一。
动手的读者: domain:server 席,packages/rest/src/crud-routes.ts 的 clone 与 createMany 处理器调用 applyDroppedFieldsHeader,附 HTTP pin。
查重: repo is:issue X-ObjectStack-Dropped-Fields 430 命中、droppedFields header clone 31、droppedFields createMany header 19(均含已关);最近邻 #15703(clone 正文缺 droppedFields,已关,已修正文这一面)、#15777 都不是响应头。
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 withdroppedFields:[{object, fields:[lead_score], reason:readonly}]in the body, no header;- a bare-array createMany
POST /data/:objectwith[…]→ 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 headerX-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.
Generated by Claude Code
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