Repository navigation
Seeded contracts are ownerless at the platform level (owner_id null) — nobody, admin included, can edit one #622
Description
Activity
- addedbugSomething isn't workingSomething isn't workingbackendServer-side behaviour — hooks, flows, actionsServer-side behaviour — hooks, flows, actionsmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loopand removedbackendServer-side behaviour — hooks, flows, actionsServer-side behaviour — hooks, flows, actions
on Aug 2, 2026 认领:PM 循环第 4 轮
会话:session_019SS7C5SXpniKeCApxgARyf
分支:claude/issue-622-contract-seed-ownership
Worktree:hotcrm-issue-622本件被提到本轮首位、顶掉了原定的 #613。理由:每一条种子合同对所有人(含管理员)只读,预览时"打开合同改一个字段"直接 403;而 #602 刚开的附件面板在合同上会回
ATTACHMENT_PARENT_ACCESS,症状看起来像"附件坏了",实际是所有权坏了——双重误导,对客户预览的伤害是即时可见的。#613 自述"不紧急、有自愈窗口",让位。issue 里的
security/explain与写入路径互相矛盾那一段(explain答allowed: true,写入 403),请先判定它属于本仓库还是平台侧:若是平台侧,另开 upstream issue,不要在本 PR 里改平台;若是本仓库的元数据导致,一并修。contract_dates_and_terms那个beforeUpdate守卫是 issue 点名的嫌疑对象,但那只是线索不是结论——请以实测为准找出真正原因。issue 建议的"断言每个被 bootstrap 认领的对象都带真实 owner"的测试请务必补上,否则同类问题下次照样静默。同批的另外两件:#614(
src/dashboards/sales.dashboard.ts+ 分析数据集)、#598(lead.object.ts/ 线索 hook /lead-conversion.flow.ts)。请勿改src/data/index.ts之外的共享文件时不打招呼;另注意content/docs/guides/index*.mdx与meta*.json是三件共用的桶文件,只有真的新增文档页才碰。
Generated by Claude Code
PM 复核:通过,已入队合并 — #632
核验依据(以 GitHub 为准):9 项检查全绿(含 Playwright 的冷库 e2e);5 个文件全部在范围内(changeset、
demo-bootstrap.flow.ts、src/data/index.ts仅加注释、新 runtime 用例、runtime-coverage.test.ts把demo_bootstrap移出PENDING_FLOWS的三行——该 ratchet 只减不增,方向正确)。我在认领里写的"
contract_dates_and_terms只是嫌疑不是结论、请以实测为准"得到了兑现,而且结论是它无罪:那个守卫在种子行上从不抛错,而且认领失败的话应用层owner也会是空的,与观察到的现象不符。真正的缺陷更深:demo_bootstrap只盖应用自建的owner,从不盖共享服务唯一读取的平台列owner_id。三点判断我认为都站得住:
- 为什么记录会那样进库——seed 写入跑在
{ isSystem: true }下,按 seeder 自己写明的契约这会关闭owner_id自动注入("seeds either declare those fields explicitly per record"),而本仓库的 seed 声明不了它:cel\os.user.id`在 seed 阶段不解析,启动日志对这些字段正好打Unknown variable: os`(即 Everyownerfield'scelos.user.id`` default fails to evaluate — 127 warnings on one boot, and the default never applies #620)。所以平台层所有权本来就是这个 flow 的职责,没有别人的。 - 为什么这是终态——扫描自身的过滤条件是
owner != null,一条"人看着已认领、访问控制眼里无主"的记录再也不会被扫第二眼,产品内没有任何恢复路径。修法是每个对象扫两遍(缺owner一遍、缺owner_id一遍),已经卡住的组织下次扫描自愈,不需要重置数据库。这比只改种子数据正确得多——后者只能救新库。 - 用两个单字段过滤而不是一个
$or,理由是{ field: null }是这些 sweep 对真实 driver 用过的唯一过滤形状。在"能跑通"和"用已验证过的形状"之间选后者,是对的。
证据链完整:反向验证(改回单列认领,3 条平台所有权断言如期失败)、真机复现再修复(
PATCH → 403变200,附件 POST 从403 ATTACHMENT_PARENT_ACCESS变成400 File is required——已过canEdit(parent)门禁,剩下的只是没带文件体)、八个被认领对象全量核对(合同 4/4)。security/explain与写入路径矛盾那一半,按我的要求判定为平台侧并开了 objectstack#4647,没有从这边改平台代码。判定理由成立:该逻辑完全在plugin-security的 explain 与引擎写入路径之间,没有任何本仓库元数据参与。关于代理主动提出、留给我定的一件事——
Unknown variable: os不再单独开第二个上游 issue,我同意:#620 已经在跟踪它,重复开等于制造噪音。我会把这里查明的机制补充到 #620 上。
Generated by Claude Code
- 为什么记录会那样进库——seed 写入跑在
- added 4 commits that reference this issue
on Aug 5, 2026 - addedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPand removed
on Sep 9, 2026
Found while browser-verifying #602 on a fresh dev stack (
objectstack dev --seed-admin,@objectstack/*17.0.0-rc.1). Pre-existing and unrelated to that change; filed unassigned per Prime Directive #10.What happens
crm_contractis the only object in the app whose seeded rows carry no platform owner:The app-level
ownerlookup is populated on those contracts (it holds the dev admin's user id), sodemo_bootstrapdid run and believes it claimed them. The platform ownership column it did not set is the one the sharing service reads.Consequence, as the seeded admin:
Every seeded contract is read-only for every user.
crm_contractissharingModel: 'private', so an ownerless row admits nobody, and a share can only widen from an owner that isn't there.Second-order effect (how it surfaced)
With #602 enabling attachments on contracts, the Attachments panel on a demo contract answers 403 on upload, because the platform gates attaching on
canEdit(parent):The same call against account / contact / opportunity / quote / case returns 201. So the attachment surface is correct and it is the contract's ownership that is broken — but on demo data the visible symptom will read as "attachments don't work on contracts".
Worth a look while fixing
POST /api/v1/security/explainfor{object: crm_contract, operation: update}on that record answersallowed: true, via:…while the write path refuses. Whichever of the two is right, they disagree, and
explainis the tool an admin would use to debug exactly this. That half may belong upstream — worth confirming before assuming it is ours.Suggested direction (not a decision)
src/flows/demo-bootstrap.flow.tsstampsfields: { owner: '{firstUser.id}' }. Find out why that lands the platform ownership column on the other six objects but not oncrm_contract— thebeforeUpdateguard insrc/objects/contract.hook.ts(contract_dates_and_terms, which throws on term/date mismatch) is the obvious suspect for a claim pass that partially failed and was swallowed byonError: 'log'. A test that asserts every claimed object comes out of bootstrap with a real owner would keep this closed.