Repository navigation
Seeded contracts are ownerless at the platform level (owner_id null) — nobody, admin included, can edit one #622
Description
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_contract is the only object in the app whose seeded rows carry no platform owner:
crm_account: 9/9 rows have owner_id
crm_contact: 9/9
crm_opportunity: 22/22
crm_quote: 5/5
crm_case: 38/38
crm_task: 7/7
crm_lead: 21/21
crm_contract: 0/4 ← none
The app-level owner lookup is populated on those contracts (it holds the dev admin's user id), so demo_bootstrap did 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:
PATCH /api/v1/data/crm_account/3H58wnu6PbAnlWkK → 200
PATCH /api/v1/data/crm_quote/8Nq3pHYJSkbMWWOG → 200
PATCH /api/v1/data/crm_contract/58hCASGfz7fCn_Zi → 403
{"error":"FORBIDDEN: insufficient privileges to update crm_contract 58hCASGfz7fCn_Zi","code":"FORBIDDEN"}
Every seeded contract is read-only for every user. crm_contract is sharingModel: '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):
POST /api/v1/data/sys_attachment {parent_object: "crm_contract", parent_id: "<seeded id>"}
→ 403 ATTACHMENT_PARENT_ACCESS
"Cannot attach to crm_contract/…: the parent record does not exist or you cannot edit it"
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/explain for {object: crm_contract, operation: update} on that record answers allowed: true, via:
owd_baseline narrows record: excluded — "Private baseline admits only the owner"
sharing widens record: excluded — "No ownership and no edit/full share grants write on this record"
vama_bypass widens "View/Modify All Data bypass held via [admin_full_access] — ownership and sharing checks are skipped"
…while the write path refuses. Whichever of the two is right, they disagree, and explain is 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.ts stamps fields: { owner: '{firstUser.id}' }. Find out why that lands the platform ownership column on the other six objects but not on crm_contract — the beforeUpdate guard in src/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 by onError: 'log'. A test that asserts every claimed object comes out of bootstrap with a real owner would keep this closed.
Activity
os-zhuang commentedon Aug 2, 2026
认领:PM 循环第 4 轮
会话:
session_019SS7C5SXpniKeCApxgARyf分支:
claude/issue-622-contract-seed-ownershipWorktree:
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
os-zhuang commentedon Aug 2, 2026
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。三点判断我认为都站得住:
{ 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
feat(ownership)!: one Owner column — align on the platform's `owner_i…
fix(demo): claim the platform owner_id on seeded records, not just th…
fix: bug 批次修复(PM 派发循环)— objectstack-ai#606 objectstack-ai#673 objects…
fix(demo-bootstrap): claim crm_campaign and crm_knowledge_article see…