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.
?recordId= carries no object, so a form action fired from the wrong object can edit a same-id record in the target object #4292
Found while implementing #4278 (which makes /forms/:name?recordId= load and PATCH that record). Filed unassigned for triage; deliberately not fixed there — the producer side is packages/core/src/actions/ActionRunner.ts, which #4278's ruling put out of scope and which #4046 currently holds.
What is missing
ActionRunner.executeForm forwards only the record id:
Nothing in that URL says which object the id belongs to. The consumer therefore has to pick one, and the only sane choice is the FormView's own target object — which is what #4278 implements: it reads GET /api/v1/data/:object/:id with the view's object, and writes PATCH /api/v1/data/:object/:id to the same.
That is correct whenever the action was fired from a record of the view's target object. Nothing enforces that it was. An action's locations bind it to a record surface, but the target FormView's object is authored independently, so target: 'showcase_task.edit' on an Account record header is publishable metadata today.
Why it can be silent
Two outcomes when the fired-from object and the view's target object differ:
ids DO collide (integer / per-table sequential primary keys, which several drivers produce): GET /api/v1/data/showcase_task/42 happily returns Task 42 while the user was looking at Account 42. The form prefills Task 42's values and, on submit, PATCHes Task 42. No error, no warning, and the record the user was actually on is untouched.
The consumer cannot detect this. The server echoes the path object back (getData returns object: request.object), so the response looks perfectly valid — #4278 pins an object-mismatch guard at the consumer and documents that a live packages/rest deployment cannot trigger it for exactly this reason.
Why it is worth grading now rather than before
Pre-#4278 the same ambiguity existed but the blast radius was smaller: the route ignored recordId entirely, so a mis-scoped action produced an empty form and an INSERT. Now that the route honours the param, the same mis-scoped action produces a targeted UPDATE of a different object's record. The fix made the surface correct; it also made this hole worth naming.
Options a fix would choose between
Not obvious enough to guess — hence filed rather than fixed:
Forward the object too (?recordObject= alongside recordId) and have the form route refuse when it disagrees with the view's target. Closes it at both ends, but adds a URL param to the console's reserved contract (app-shell/src/urlParams.ts).
Validate at the producer: executeForm resolves the target view's object and refuses to navigate when the context record's object differs. No new URL surface, but it makes ActionRunner a metadata reader it currently is not.
Validate at publish time: reject a type: 'form' action whose locations put it on an object other than its target view's. Structurally prevents the mis-authoring instead of catching it at runtime — the "make AI-written metadata hard to get wrong" direction — but only covers statically-placed actions.
Measured on
origin/main at 90e792e11 plus the #4278 branch, by reading ActionRunner.executeForm, packages/metadata-protocol/src/protocol.ts (getData echoes object: request.object), and the data plugin's route table in @objectstack/specapi/plugin-rest-api.zod.ts. Not browser-verified; the id-collision half depends on the deployment's primary-key strategy.
Activity
yinlianghui commentedon Aug 11, 2026
CLAIM + 代决 — session
session_017Qqyix2QcnpUC9XeYVDzx3(objectui whole-repo PM seat;否决窗口开放)。Branch:claude/issue-4292-recordid-object-identity。闸门:PR #4293 落地(同 FormPage 面;#4285 已落,core/actions 面已释放)。裁决(不变量表述):id 不得脱离其对象身份旅行;不匹配在两端都 fail-closed。
ActionRunner.executeForm,core 面已由 [P3] objectuiActionDef's[key: string]: anyabsorbs every retired and misspelled action key — removing a spec key produces no compile error #4046 释放):只有当发起上下文记录的对象与 target view 的对象(<object>.<view>前缀)匹配时才转发?recordId=;不匹配(如从 task 记录发起、目标是另一对象的 create 视图——"log time" 形状)则不转发,保持 create 语义——这正是 pre-type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278 该场景的正确行为。同时转发对象身份参数(如recordObject),作为纵深防御的第二半。上下文如何得知对象名由 dev 实测(context.record 的元字段 / objectName / target 前缀),不许发明第二个"对象身份"事实源。type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278 之上):recordObject参数存在且与 FormView 目标对象不符 ⇒ fail-closed 错误态(沿用type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278 已钉的 fail-closed 通道),不加载、不写。参数缺席(手写深链/旧链)⇒ 维持现行为(按视图目标对象解析)——守卫只在有身份信息时收紧,不破坏既有深链。type: 'form'action fired from a record opens an empty CREATE form —/forms/:nameignores the?recordId=ActionRunner forwards #4278 的主路径)不回归、showcase_log_time类跨对象 create 场景恢复 create。Generated by Claude Code
yinlianghui commentedon Aug 11, 2026
ACCEPT — PM review of record (session
session_017Qqyix2QcnpUC9XeYVDzx3)。PR #4311 按不变量("id 不得脱离对象身份旅行,不匹配两端 fail-closed")落地,验收要点:
context.objectName是仓库既有的唯一拼写(ActionContext 平铺三元组、useConsoleActionRuntime 播种、recordFormNavigation 已按同键解析)——零新事实源。action.objectName被有据排除(描述动作挂载处而非记录来源)且有 pin 防它把 runner 骗过边界,这层细致正是本卡要防的错位。/data流量、永不触写(已钉);断言而非覆盖;错误措辞与 payload-mismatch 区分,失败自报哪道检查开火。check:action-forward-parity零白名单改动确认"行为非词汇"。CI 20 项收敛。Flipping ready + auto-merge。#4278 → #4292 这条"实现中发现更深缺陷 → 立卡 → 闸门接力"的链条,两天内从重复插入修到身份守卫,是滚动模式的理想形状。
Generated by Claude Code
fix(core,console): an id never travels without its object on the form…