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 }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
[repo:objectui] Master-detail form's batch save sends owner_id on every parent and child operation, so a permitted line edit 403s with "'owner_id' is system-managed" — the list view's inline grid sends only the dirty cell and succeeds #10108
The master-detail form (inlineEdit: 'grid' on a child's master_detail field, or a form view subforms entry) saves parent + children through POST /api/v1/batch, and the payload it builds includes owner_id in every operation — parent and each child row — whether or not ownership was touched. The server correctly refuses that field for any caller without the transfer grant, so the whole atomic batch fails with 403 and the user cannot save an edit they were fully permitted to make.
Note this is a different payload defect from objectstack-ai/objectstack#15259 (which is about field-level-security-denied fields): here the master-detail grid actually picks the right business columns — it sends only the editable cells and does not send the FLS-denied ones — and is stopped by a system-managed field the form adds on its own.
Minimal reproduction
Platform 17.2.0, objectstack dev, SQLite, single tenancy, requires: ['sharing'].
Position reporter permission set: both objects allowEdit: true, writeScope: 'own', allowTransfer absent (false), modifyAllRecords false. Rows are visible/editable to this user through a sharing rule (sys_record_share, access_level: 'edit'), not through ownership.
Steps as the reporter user:
Open the parent's edit form (any surface that renders it — a related list row menu Edit).
Change one cell in the inline child grid (actual_value) — touch nothing else.
Click Update.
Observed request:
POST /api/v1/batch
{"operations":[
{"object":"kpi_entry_sheet","action":"update","id":"QYH…","data":{"owner_id":"ptc…","plan":"_LH…","subject":"bu_sw_sales","subject_type":"department","status":"draft","pending_action":null,"action_reason":null,"remark":"…"}},
{"object":"kpi_entry_line","action":"update","id":"AVy…","data":{"owner_id":null,"sheet":"QYH…","plan_indicator":"fAV…","actual_value":1000,"last_adjustment":null,"remark":null}},
{"object":"kpi_entry_line","action":"update","id":"rgQ…","data":{"owner_id":null,…,"actual_value":1100,…}},
{"object":"kpi_entry_line","action":"update","id":"tfr…","data":{"owner_id":null,…,"actual_value":1200,…}}],
"atomic":true}
Observed response:
403 {"code":"PERMISSION_DENIED",
"error":"[Security] Access denied: 'owner_id' on 'kpi_entry_line' is system-managed — changing record ownership on update requires the transfer grant (allowTransfer or modifyAllRecords)"}
Controls with the same session on the same rows, isolating owner_id as the sole cause (the child rows' owner_id is null before and after — the form is resending the value it read):
POST /batch operations[].data = {actual_value: 900} → 200, all three rows saved, scoring hooks ran
POST /batch operations[].data = {actual_value: 950, owner_id: null} → 403 (same message)
PATCH /api/v1/data/kpi_entry_line/<id> {"actual_value": 95} → 200
The same user editing the same field through a list view's inline grid succeeds, because that path sends one PATCH per row carrying only the dirty cell.
Expected
The master-detail batch should send only dirty fields, and in no case emit server-managed / injected fields (owner_id, owning_business_unit_id, organization_id, created_*, updated_*). Resending an unchanged system-managed value is indistinguishable from an ownership transfer attempt at the security layer, so a read-only round-trip of the record turns into a permission error.
At minimum, owner_id should be stripped from update operations when its value is unchanged — a no-op write should never require the transfer grant.
Secondary: the console surfaces this as a generic save failure; the message naming the refused field only appears in the network response.
Because of this, a master-detail form is currently unusable for every role except one holding allowTransfer / modifyAllRecords — i.e. exactly the line-entry roles the feature exists for.
Environment
@objectstack/* 17.2.0 (runtime, console, spec) · Node 22 · better-sqlite3 · single tenancy · SharingServicePlugin active · Chromium 1440×900 · zh-CN. App: objectstack-ai/kpi (kpi_entry_sheet / kpi_entry_line, position kpi_dept_reporter).
a master-detail form is currently unusable for every role except one holding allowTransfer / modifyAllRecords — i.e. exactly the line-entry roles the feature exists for.
Declared file surface (for the in-flight serialization check)
The master-detail / subform batch-save payload builder and its call sites:
packages/app-shell/src/** — the form save path that assembles POST /api/v1/batch
packages/plugin-form/src/** — the subforms / inlineEdit: 'grid' child-row writer
packages/data-objectstack/src/** — the outbound field filter, if the injection is there
plus the tests and a changeset under whatever package the fix lands in
⛔ Not claimed here: packages/plugin-detail/src/renderers/recordHistory* (in flight on PR #10093), packages/plugin-calendar/** (PRs #10101 / #10152), packages/react/src/hooks/useSettledSchema* (PR #10149), the three publish-drafts call sites in app-shell that PR #10147 already moves. The dev order names those as hands-off so the two diffs cannot collide.
What this seat has NOT measured
Everything in the card and in the two triage comments above is relayed, not re-derived by this seat: the 403 body, the three isolating controls, the claim that owner_id is injected outside the declared form sections, and the claim that the list-view inline grid takes a different, working path. The dev's step 1 is to reproduce on today's main before touching anything — the card itself asks for that, and this shift has already had two cards whose stated control did not reproduce.
Note for the dev on today's tree
main currently carries a RED Bundle Analysis (the check-eager-closure-budget.mjs gate — incident #10148). PR #10151 is the fix and is in the merge queue as of 2026-09-20T15:35Z. ⛔ Do not touch that script or any ceiling in it; merge origin/main in before the final push and the fix comes with it.
{
"issue": 10108,
"status": "done",
"branch": "claude/issue-10108-batch-payload-system-managed-fields",
"pr": "https://github.com/objectstack-ai/objectui/pull/10154",
"session": "session_018HrVaotisyhgmot9o2MLRq",
"premise_still_valid": true,
"summary": "REPRODUCED on today's main (87af769e9) before any edit, with a lit control. Both paths the card names run through ONE builder, sanitizeFormData (packages/plugin-form/src/sanitize.ts): the master-detail POST /api/v1/batch (MasterDetailForm submitViaBatch -> buildMasterDetailEditBatch/buildMasterDetailBatch in masterDetailTx.ts, child rows via its toWritable; LineItemsPanel save is the second caller) and the plain record edit PATCH (ObjectForm submit -> sanitizeFormData -> saveWithOcc). The lit control: in the SAME call on the SAME fixture the filter emitted owner_id, owning_business_unit_id, created_by and updated_by while refusing created_at, updated_at, organization_id and id - so the refusal is a hand-written NAME roster and the emitted four were simply never in it, not an absence the probe invented. Card's plain-form claims both reproduced verbatim: a form declaring three business fields and no ownership column rendered two inputs and still sent owner_id, and every child row was rewritten in full including the two the user never touched. BOTH TIERS SHIPPED. (1) The write-side roster in sanitize.ts and the render-side roster in autoLayout.ts were two hand-written copies of one list that had drifted (render had learned owner_id, write never did); they are now the same object, SERVER_OWNED_FIELD_NAMES, with the rationale in its header - a form never writes a field it refuses to render. (2) sanitizeFormData also refuses any field the schema marks system:true, so the next injected column needs no edit here - this is the class fix triage asked for. (3) The edit batch sends only cells that differ from the loaded snapshot and emits NO operation for a row where nothing moved; the comparison resolves everything it cannot settle towards SENDING so a real edit is never dropped. (4) Both batch builders now apply the roster to the PARENT payload too, so the builder no longer trusts its caller. Secondary error-message item NOT done, deliberately - see open_questions.",
"tests": "REVERSE VERIFICATION (fix committed first so the restore leg had a real restore point; the three implementation files reverted to the branch point, each mutation proven on disk by git hash-object vs the HEAD blob, restored under a trap, restore proven by an empty git diff HEAD): MUTATED sanitize.ts HEAD=9bbbffaca432 -> disk=e6d07c267cfa; autoLayout.ts 634f120185aa -> 9373fc11e754; masterDetailTx.ts faa37e406e6c -> d7ab7883d82a; grep -c owner_id on the PRE-FIX sanitize.ts prints 0 (the defect itself). MUTATED RUN: 'Tests 11 failed | 11 passed (22)', exit 1. RESTORED: all three match their HEAD blob, git diff HEAD empty. RESTORED RUN: 'Tests 22 passed (22)', exit 0. All 11 reds are the new and retriaged pins and they cover BOTH paths - the '/batch save' rows AND the 'plain record edit form PATCH' row both went red - plus two system-managed fields beyond owner_id (owning_business_unit_id, created_by/updated_by) and one no roster in the repo names, refused by its system flag alone. No rebuild needed for the ablation: all three files are consumed as sources through relative imports inside the same package, nothing reads a dist/. CHECKS ON HEAD 7fd26bbe8 (origin/main merged in, so PR 10151's bundle-analysis repair rode along; that script and its ceilings untouched): pnpm exec vitest run packages/plugin-form/ -> 'Test Files 105 passed (105) Tests 1021 passed | 1 skipped (1022)' exit 0 (under os-verify-lock.sh, VERDICT command-exit 0). pnpm --filter @object-ui/plugin-form run type-check (tsc --noEmit AND tsc -p tsconfig.test.json) -> exit 0, after building the dependency closure with pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build. pnpm --filter @object-ui/plugin-form lint -> exit 0; eslint --format json reports 136 files, 0 errors, 899 warnings (the package's standing no-explicit-any baseline). check-changeset-presence exit 0 ('5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-no-major exit 0; check:changeset-claims exit 0 (re-run AFTER the commit - the pre-commit run reported it had measured nothing); check:pending-changeset-literals, check:new-line-citations (0 new citations), check:control-bytes, check:test-path-roots, check:unreferenced-sources, check:metadata-write-doors, check:handler-key-reads all exit 0. Every exit code captured before any pipe. DECLARED NARROWING on the repo-wide lint: pnpm lint is turbo run lint, i.e. each package's own eslint .; the run above IS that command for the one package this diff touches, over its whole tree. Universe read from eslint.config.js, file count read from --format json, and the invariance leg: eslint.config.js configures no parserOptions.project and no projectService, so type-aware linting is not enabled and no rule can read across a file boundary - a change inside this package cannot move any untouched file's verdict. PUBLIC FACE MEASURED, not asserted: after building plugin-form on this head, grep -c SERVER_OWNED_FIELD_NAMES packages/plugin-form/dist/index.d.ts prints 0 and filterSystemFields keeps its signature, which is the evidence behind Clause-2: no. NOT MEASURED: remote CI, which had not run on this head when this report was written.",
"mcp_calls": "0 - no MCP GitHub tool was called at any point; every GitHub read and write went through the REST curl helper",
"api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (the draft PR) and POST /repos/objectstack-ai/objectui/issues/10108/comments (this report). No label write: this repo's AGENTS.md forbids applying skip-changeset here (the label exists but nothing reads it) and this change carries a real changeset, so no label was called for. needs:contract-review is not on the PR and was not touched - Clause-2 is no. git push of the branch is not a REST write.",
"open_questions": [
{
"question": "Secondary item (surface the server's message to the user) was NOT done. The console's substitution of a localized form.noPermissionToSave for every permission denial is a RULED behaviour with its own regression history, not a gap: rendering the server text verbatim leaked the object's machine name and the record id, untranslated, and was repaired at objectstack#3821 and restated for a 403 at objectui#5210. The producer-side seam already exists - declaredUserMessage reads userMessage, which a hook sets at throw time (objectstack#9934). Who carries the platform-side change?",
"options": [
"A - file an objectstack card: have the ownership-transfer refusal set userMessage, after which this console surfaces it with zero code change here",
"B - relax the renderer to print the raw 403 body for this error class only",
"C - leave it; the renderer already logs the full error under '[form] write denied:' before substituting, so the diagnosis is not invisible"
],
"recommendation": "A. It is the only option that does not reverse objectstack#3821 for every permission denial in the product, and it lands on the side that actually knows which text is safe for an end user. B reintroduces the exact leak that ruling exists to stop. I did not file the card myself because it is a cross-repo change this dispatch explicitly asked me to report rather than widen into."
},
{
"question": "Scope boundary: the master-detail PARENT operation and the plain record edit PATCH still send the full sanitized form values rather than a dirty diff. The card scopes 'send only dirty fields' to the batch and its child rows are where the snapshot lives; the parent's persisted record is fetched inside ObjectForm and never handed back out, so a parent dirty diff needs a new seam through the form's submitHandler contract. Should that be a follow-up?",
"options": [
"A - leave it: the 403 was caused by the roster, not by payload volume, and it is fully shut on both paths now",
"B - follow-up card to make ObjectForm's edit mode send only fields that differ from the record it loaded, which covers the parent op and the plain PATCH at once"
],
"recommendation": "A for this card, B as a separate one if the PM wants it. B touches the most-used write path in the product, where a false-clean silently drops a user's edit with a 200 back - that risk deserves its own card and its own reverse verification, not a rider on this one."
}
],
"out_of_scope_findings": [
"noted, not filed: packages/types/src/system-fields.ts holds a third roster of this family (display-oriented) and packages/plugin-detail a fourth (inline-edit 'never editable'). Each has a written reason to be separate and none is wrong today. Successor: none - nothing in flight touches them, and folding four purpose-specific rosters together is a decision, not a cleanup.",
"noted, not filed: the card says the refused-field message 'only appears in the network response'. It also reaches the browser console - the form renderer logs '[form] write denied:' with the whole error before substituting the user-facing text. A correction to the card's wording, not a defect. Successor: the PM, who asked for a verdict on the secondary item.",
"noted, not filed: the fixture 'emits only the parent update when children are unchanged' in masterDetailTx.test.ts was FALSE under its own name before this branch (the batch also carried a full-row rewrite of the child), and its snapshot omitted the back-reference FK the real loader always returns. Both repaired in this PR since the diff was in that file anyway. Successor: none needed."
]
}
Claim: domain:ui execution seat, re-hung in the protocol shape
Branch: claude/issue-10108-batch-payload-system-managed-fields
Seat: domain:ui#2
Session: session_018HrVaotisyhgmot9o2MLRq
Clause-②: no
Read at: 2026-09-20T16:10Z
⚠️This is the FIRST protocol-shaped claim on this thread, ⛔ not a second one. The seat's earlier comment 5750777481 opened ## Claim — … as a heading and carried no line beginning Claim: and no Branch: line, so the declaration limb had no card-side carrier at all — the same defect this seat hit and repaired on objectui#10058 an hour earlier. ⛔ Nothing about the dispatch changes; this hangs the carrier the earlier comment failed to hang.
Clause-②: no is declared from the diff of PR #10154, which the seat read, and is checked three ways rather than asserted:
The diff adds one export — SERVER_OWNED_FIELD_NAMES in packages/plugin-form/src/sanitize.ts.
It does not reach the public face. The package's entry is src/index.tsx (vite.config.tslib.entry, ⛔ not src/index.ts, which does not exist — this seat's first probe for it was a dead instrument and is reported as such). That barrel takes named imports from ./autoLayout — seven of them, SERVER_OWNED_FIELD_NAMES not among them — and names ./sanitize nowhere. autoLayout.ts imports the roster and aliases it to a non-exported const SYSTEM_FIELD_NAMES.
The dev's own artefact reading agrees: after building the package, grep -c SERVER_OWNED_FIELD_NAMES packages/plugin-form/dist/index.d.ts prints 0, and filterSystemFields keeps its signature.
⇒ a runtime behaviour change behind existing surface, with no published key or export moved: the no arm.
Activity
baozhoutao commentedon Sep 6, 2026
补一条:同一个
owner_id注入在普通记录编辑表单上也发生,不限于主从子表的/batch。同一应用同一账号(kpi_dept_reporter,writeScope: 'own',记录通过共享规则可写),从相关列表行菜单打开kpi_entry_line的编辑表单、只改「实际值」一格点「更新」:值得注意的是
owner_id不在表单视图声明的任何分区里 —— 表单只声明了 所属填报单 / 来源下达 / 指标名称 / 计量单位 / 目标值 / 权重 / 实际值 / 备注 八项。所以这不是「表单提交了它显示的字段」(#15259 那一类),而是提交体在表单字段之外另行注入了系统托管字段;把表单里的字段删干净也躲不开。顺带一个可复核的对照:该表单原先在「计分」分区里显示
score,于是先撞 objectstack-ai/objectstack#15259 的Field write denied [score];把只读的计分字段从表单移出后score不再出现在提交体里(#15259 那一半按预期消失),剩下的就是本单这条owner_id。两个缺陷是独立的两层。环境同上:
@objectstack/*17.2.0 · Node 22 · better-sqlite3 · 单租户 · Chromium 1440×900 · zh-CN。os-zhuang commentedon Sep 6, 2026
分诊 ·
repo:objectui/bug/priority:p1/pm:queue分诊席位。⛔ 不认领、不派发、不写代码、不合并。2026-09-06T03:14Z。
路由
repo:objectuipayload 是表单构造的:master-detail 内联网格(
inlineEdit: 'grid')与 form viewsubforms的保存路径在@object-ui/app-shell/ console 侧。服务端的行为在本卡里是正确的——它按契约拒绝了一个无权限的owner_id写入。⇒ 落点在 objectui,本仓零改动。⛔ 不加domain:*(跨仓卡以repo:*路由,对照 objectstack-ai/objectstack#15260 / objectstack-ai/objectstack#15009 / objectstack-ai/objectstack#16084)。⭐ 定级 p1 —— 这一档我给得比本轮任何一张都高,理由列明以便被推翻
一个已发布功能对它存在的那个角色完全不可用。 卡自己那句是准的:
录入行的角色按定义不该有转移授权。⇒ 不是「某些配置下坏」,是目标人群 100% 坏。
唯一的绕过办法是授予一个危险权限。 让
reporter拿到allowTransfer/modifyAllRecords才能保存一个自己有权改的字段——这是用扩大权限来换取功能,比缺陷本身更坏。⇒ 无安全的 workaround。证据质量是本轮最高的。 MEMBER 报告,含最小复现、真实请求/响应、以及三条把
owner_id隔离为唯一成因的对照:⭐ 第二条与第一条只差
owner_id: null一个键,且那个值与库里的现值相同(卡明写 child rows 的owner_id前后都是null)。⇒ 一次无操作的写入触发了转移授权检查。这不是推断,是按运行测出来的。同一能力的另一条路径是好的,这排除了「服务端过严」这个读法:列表视图的内联网格发一个只带脏单元格的 PATCH,成功。⇒ 缺陷严格地在 master-detail 表单的 payload 构造里。
不给 p0:⛔ 不是安全漏洞——服务端拒绝是正确的,没有越权发生,没有数据泄露或损坏。是可用性的完全丧失。
⛔ 与 objectstack-ai/objectstack#15259 不是重复
卡自己先划清了,我复核认同并复述,免得后人合卡:
⇒ 同一个门上的两个不同 payload 缺陷。⛔ 不要合并;但接手时值得一起读,因为修法可能共用同一个「出站字段过滤」的位置。
给接卡人的边界
owner_id值未变时剥掉——「一次无操作写入不该需要转移授权」。owner_id、owning_business_unit_id、organization_id、created_*、updated_*)。⭐ 只做最小档,下一个系统管理字段会以同样方式再咬一次——卡列的那一串就是候选名单。
次要项也请一起报:console 把这个显示为通用保存失败,指名被拒字段的那句话只在网络响应里。⇒ 用户看到的是「保存失败」,排障的人才看得到原因。这条可以是同一 PR 的第二个 commit,⛔ 不必单开卡。
SharingServicePlugin;接手前请在当前 objectui head 上复现一次再动手——本轮已多次遇到「卡填写时为真、今天已漂移」的情况。Generated by Claude Code
os-tesla commentedon Sep 20, 2026
Claim —
domain:uiexecution seatSeat: domain:ui#2
Session: session_018HrVaotisyhgmot9o2MLRq
Claimed at: 2026-09-20T15:35Z
Declared file surface (for the in-flight serialization check)
The master-detail / subform batch-save payload builder and its call sites:
packages/app-shell/src/**— the form save path that assemblesPOST /api/v1/batchpackages/plugin-form/src/**— thesubforms/inlineEdit: 'grid'child-row writerpackages/data-objectstack/src/**— the outbound field filter, if the injection is there⛔ Not claimed here:
packages/plugin-detail/src/renderers/recordHistory*(in flight on PR #10093),packages/plugin-calendar/**(PRs #10101 / #10152),packages/react/src/hooks/useSettledSchema*(PR #10149), the three publish-drafts call sites inapp-shellthat PR #10147 already moves. The dev order names those as hands-off so the two diffs cannot collide.What this seat has NOT measured
Everything in the card and in the two triage comments above is relayed, not re-derived by this seat: the 403 body, the three isolating controls, the claim that
owner_idis injected outside the declared form sections, and the claim that the list-view inline grid takes a different, working path. The dev's step 1 is to reproduce on today'smainbefore touching anything — the card itself asks for that, and this shift has already had two cards whose stated control did not reproduce.Note for the dev on today's tree
maincurrently carries a REDBundle Analysis(thecheck-eager-closure-budget.mjsgate — incident #10148). PR #10151 is the fix and is in the merge queue as of 2026-09-20T15:35Z. ⛔ Do not touch that script or any ceiling in it; mergeorigin/mainin before the final push and the fix comes with it.Generated by Claude Code
os-tesla commentedon Sep 20, 2026
os-dev-report
{ "issue": 10108, "status": "done", "branch": "claude/issue-10108-batch-payload-system-managed-fields", "pr": "https://github.com/objectstack-ai/objectui/pull/10154", "session": "session_018HrVaotisyhgmot9o2MLRq", "premise_still_valid": true, "summary": "REPRODUCED on today's main (87af769e9) before any edit, with a lit control. Both paths the card names run through ONE builder, sanitizeFormData (packages/plugin-form/src/sanitize.ts): the master-detail POST /api/v1/batch (MasterDetailForm submitViaBatch -> buildMasterDetailEditBatch/buildMasterDetailBatch in masterDetailTx.ts, child rows via its toWritable; LineItemsPanel save is the second caller) and the plain record edit PATCH (ObjectForm submit -> sanitizeFormData -> saveWithOcc). The lit control: in the SAME call on the SAME fixture the filter emitted owner_id, owning_business_unit_id, created_by and updated_by while refusing created_at, updated_at, organization_id and id - so the refusal is a hand-written NAME roster and the emitted four were simply never in it, not an absence the probe invented. Card's plain-form claims both reproduced verbatim: a form declaring three business fields and no ownership column rendered two inputs and still sent owner_id, and every child row was rewritten in full including the two the user never touched. BOTH TIERS SHIPPED. (1) The write-side roster in sanitize.ts and the render-side roster in autoLayout.ts were two hand-written copies of one list that had drifted (render had learned owner_id, write never did); they are now the same object, SERVER_OWNED_FIELD_NAMES, with the rationale in its header - a form never writes a field it refuses to render. (2) sanitizeFormData also refuses any field the schema marks system:true, so the next injected column needs no edit here - this is the class fix triage asked for. (3) The edit batch sends only cells that differ from the loaded snapshot and emits NO operation for a row where nothing moved; the comparison resolves everything it cannot settle towards SENDING so a real edit is never dropped. (4) Both batch builders now apply the roster to the PARENT payload too, so the builder no longer trusts its caller. Secondary error-message item NOT done, deliberately - see open_questions.", "tests": "REVERSE VERIFICATION (fix committed first so the restore leg had a real restore point; the three implementation files reverted to the branch point, each mutation proven on disk by git hash-object vs the HEAD blob, restored under a trap, restore proven by an empty git diff HEAD): MUTATED sanitize.ts HEAD=9bbbffaca432 -> disk=e6d07c267cfa; autoLayout.ts 634f120185aa -> 9373fc11e754; masterDetailTx.ts faa37e406e6c -> d7ab7883d82a; grep -c owner_id on the PRE-FIX sanitize.ts prints 0 (the defect itself). MUTATED RUN: 'Tests 11 failed | 11 passed (22)', exit 1. RESTORED: all three match their HEAD blob, git diff HEAD empty. RESTORED RUN: 'Tests 22 passed (22)', exit 0. All 11 reds are the new and retriaged pins and they cover BOTH paths - the '/batch save' rows AND the 'plain record edit form PATCH' row both went red - plus two system-managed fields beyond owner_id (owning_business_unit_id, created_by/updated_by) and one no roster in the repo names, refused by its system flag alone. No rebuild needed for the ablation: all three files are consumed as sources through relative imports inside the same package, nothing reads a dist/. CHECKS ON HEAD 7fd26bbe8 (origin/main merged in, so PR 10151's bundle-analysis repair rode along; that script and its ceilings untouched): pnpm exec vitest run packages/plugin-form/ -> 'Test Files 105 passed (105) Tests 1021 passed | 1 skipped (1022)' exit 0 (under os-verify-lock.sh, VERDICT command-exit 0). pnpm --filter @object-ui/plugin-form run type-check (tsc --noEmit AND tsc -p tsconfig.test.json) -> exit 0, after building the dependency closure with pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-form^...' build. pnpm --filter @object-ui/plugin-form lint -> exit 0; eslint --format json reports 136 files, 0 errors, 899 warnings (the package's standing no-explicit-any baseline). check-changeset-presence exit 0 ('5 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'); check-changeset-no-major exit 0; check:changeset-claims exit 0 (re-run AFTER the commit - the pre-commit run reported it had measured nothing); check:pending-changeset-literals, check:new-line-citations (0 new citations), check:control-bytes, check:test-path-roots, check:unreferenced-sources, check:metadata-write-doors, check:handler-key-reads all exit 0. Every exit code captured before any pipe. DECLARED NARROWING on the repo-wide lint: pnpm lint is turbo run lint, i.e. each package's own eslint .; the run above IS that command for the one package this diff touches, over its whole tree. Universe read from eslint.config.js, file count read from --format json, and the invariance leg: eslint.config.js configures no parserOptions.project and no projectService, so type-aware linting is not enabled and no rule can read across a file boundary - a change inside this package cannot move any untouched file's verdict. PUBLIC FACE MEASURED, not asserted: after building plugin-form on this head, grep -c SERVER_OWNED_FIELD_NAMES packages/plugin-form/dist/index.d.ts prints 0 and filterSystemFields keeps its signature, which is the evidence behind Clause-2: no. NOT MEASURED: remote CI, which had not run on this head when this report was written.", "mcp_calls": "0 - no MCP GitHub tool was called at any point; every GitHub read and write went through the REST curl helper", "api_writes": "2 - POST /repos/objectstack-ai/objectui/pulls (the draft PR) and POST /repos/objectstack-ai/objectui/issues/10108/comments (this report). No label write: this repo's AGENTS.md forbids applying skip-changeset here (the label exists but nothing reads it) and this change carries a real changeset, so no label was called for. needs:contract-review is not on the PR and was not touched - Clause-2 is no. git push of the branch is not a REST write.", "open_questions": [ { "question": "Secondary item (surface the server's message to the user) was NOT done. The console's substitution of a localized form.noPermissionToSave for every permission denial is a RULED behaviour with its own regression history, not a gap: rendering the server text verbatim leaked the object's machine name and the record id, untranslated, and was repaired at objectstack#3821 and restated for a 403 at objectui#5210. The producer-side seam already exists - declaredUserMessage reads userMessage, which a hook sets at throw time (objectstack#9934). Who carries the platform-side change?", "options": [ "A - file an objectstack card: have the ownership-transfer refusal set userMessage, after which this console surfaces it with zero code change here", "B - relax the renderer to print the raw 403 body for this error class only", "C - leave it; the renderer already logs the full error under '[form] write denied:' before substituting, so the diagnosis is not invisible" ], "recommendation": "A. It is the only option that does not reverse objectstack#3821 for every permission denial in the product, and it lands on the side that actually knows which text is safe for an end user. B reintroduces the exact leak that ruling exists to stop. I did not file the card myself because it is a cross-repo change this dispatch explicitly asked me to report rather than widen into." }, { "question": "Scope boundary: the master-detail PARENT operation and the plain record edit PATCH still send the full sanitized form values rather than a dirty diff. The card scopes 'send only dirty fields' to the batch and its child rows are where the snapshot lives; the parent's persisted record is fetched inside ObjectForm and never handed back out, so a parent dirty diff needs a new seam through the form's submitHandler contract. Should that be a follow-up?", "options": [ "A - leave it: the 403 was caused by the roster, not by payload volume, and it is fully shut on both paths now", "B - follow-up card to make ObjectForm's edit mode send only fields that differ from the record it loaded, which covers the parent op and the plain PATCH at once" ], "recommendation": "A for this card, B as a separate one if the PM wants it. B touches the most-used write path in the product, where a false-clean silently drops a user's edit with a 200 back - that risk deserves its own card and its own reverse verification, not a rider on this one." } ], "out_of_scope_findings": [ "noted, not filed: packages/types/src/system-fields.ts holds a third roster of this family (display-oriented) and packages/plugin-detail a fourth (inline-edit 'never editable'). Each has a written reason to be separate and none is wrong today. Successor: none - nothing in flight touches them, and folding four purpose-specific rosters together is a decision, not a cleanup.", "noted, not filed: the card says the refused-field message 'only appears in the network response'. It also reaches the browser console - the form renderer logs '[form] write denied:' with the whole error before substituting the user-facing text. A correction to the card's wording, not a defect. Successor: the PM, who asked for a verdict on the secondary item.", "noted, not filed: the fixture 'emits only the parent update when children are unchanged' in masterDetailTx.test.ts was FALSE under its own name before this branch (the batch also carried a full-row rewrite of the child), and its snapshot omitted the back-reference FK the real loader always returns. Both repaired in this PR since the diff was in that file anyway. Successor: none needed." ] }Generated by Claude Code
os-tesla commentedon Sep 20, 2026
Claim:
domain:uiexecution seat, re-hung in the protocol shapeBranch:
claude/issue-10108-batch-payload-system-managed-fieldsSeat: domain:ui#2
Session:
session_018HrVaotisyhgmot9o2MLRqClause-②: no
Read at: 2026-09-20T16:10Z
5750777481opened## Claim — …as a heading and carried no line beginningClaim:and noBranch:line, so the declaration limb had no card-side carrier at all — the same defect this seat hit and repaired on objectui#10058 an hour earlier. ⛔ Nothing about the dispatch changes; this hangs the carrier the earlier comment failed to hang.Clause-②: nois declared from the diff of PR #10154, which the seat read, and is checked three ways rather than asserted:SERVER_OWNED_FIELD_NAMESinpackages/plugin-form/src/sanitize.ts.src/index.tsx(vite.config.tslib.entry, ⛔ notsrc/index.ts, which does not exist — this seat's first probe for it was a dead instrument and is reported as such). That barrel takes named imports from./autoLayout— seven of them,SERVER_OWNED_FIELD_NAMESnot among them — and names./sanitizenowhere.autoLayout.tsimports the roster and aliases it to a non-exportedconst SYSTEM_FIELD_NAMES.grep -c SERVER_OWNED_FIELD_NAMES packages/plugin-form/dist/index.d.tsprints0, andfilterSystemFieldskeeps its signature.⇒ a runtime behaviour change behind existing surface, with no published key or export moved: the
noarm.Generated by Claude Code
fix(plugin-form): never write the columns the server owns, and send o…