Repository navigation
Commit 4e8bbbf
fix(cloud-connection): an installed package's sample rows are owned by the person who seeded them (#30739)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DAVwSYT2ApQTee3Tp1n1i8q)_
Fixes #30324
## 维护者速读
1. 按裁决 C(6670861426):安装应用时写入的示例行,`owner_id` 与 `created_by`
记为执行播种的人,之后按平台普通权限规则走,不为示例数据加任何读规则。
2.
四个播种入口各自的人选:安装与重新播种用调用者;启动自愈用台账记录的安装者(须仍是用户,否则不写并记日志);按组织回放用组织创始人(未给则不写并记日志)。
3. 只写本次加载新建的行(`insertedIds`),且只写仍为空的字段:已存在被匹配的行、种子自己写了负责人的行都不动。
4. 实测:私有共享下同事持有默认权限集仍读到 0 条(按普通规则,已钉为接受行为);安装者可读、可改、可删;清除示例数据的识别不变。
5. 只改 `packages/cloud-connection/src/**`(新模块一个、插件四处接线)与测试;不碰
spec、plugin-sharing、plugin-security、seed loader。
6. 已存在的旧示例行不回填(重新播种不碰已在的行);要补写需先清除再重新播种,changeset 写明。
## What changes
`packages/cloud-connection/src/marketplace-install-local-seed-owner.ts`
(new) holds `stampSeedOwner`: after a seed load, it writes `owner_id`
and `created_by` = a named person on the rows that load created
(`SeedLoadResult.insertedIds`), where each field is still empty.
Predicate writes over chunks of 50 ids, under `{ isSystem: true,
skipAutomations: true }`, yielding between chunks; a refused chunk is
retried row by row, and a row still refused keeps what the seed wrote
and is counted. `sys_*`, managed and federated objects are skipped;
under a wall every predicate also carries the load's `organization_id`.
`marketplace-install-local-plugin.ts`: `runInlineSeed`, the one loader
run behind all four seeding doors, takes the person its door names and
stamps right after the load (`stampLoadOwner`, which logs the stamp, a
door with no person, and any refused rows). The doors:
| Door | Person | With none |
|:--|:--|:--|
| install (`POST /install-local`) | the caller | n/a (the route requires
a principal) |
| reseed (`POST …/reseed-sample-data`) | the caller | n/a |
| boot heal (rehydrate over emptied tables, no wall) | the ledger
entry's `installedBy`, when a system `sys_user` read finds them | rows
keep no owner; info line names why (no installer recorded, or no longer
a user) |
| per-organization replay (`seed-replayer`) | `options.userId`, the
organization's founder the org-scoping middleware hands in | rows keep
no owner; info line says the replay names no founder |
When `AppPlugin`'s replayer is the registered `seed-replayer` instead of
this plugin's, that replay does not go through this code (outside this
surface).
### Why not through `claimSeedOwnership`
The boot seed's first-admin hand-over (`plugin-security`, and its org
twin in `organizations`) runs on `app:seeded`, which this seed does not
emit; reads its ids from the `seed-settlement` host, which this plugin
does not publish to; writes `owner_id` only; and targets the platform
admin, not the person who seeded. Routing through it would change all
four outside this package. Its selector discipline is kept (only ids the
loader reports), narrowed to `insertedIds`, and its write shape is
borrowed.
## Why
Measured on `main` b77c160 (real `os start`, inline install of one
`private` object, a `controlled_by_parent` detail and an `isDefault` set
at `readScope: 'own'`): the 4 seeded rows had `owner_id` and
`created_by` NULL, so a member granted the set read 0 rows and got 404
on a by-id read, and no `own` grant reached them, the installer's
included. The ruling (6670861426) replaces ruling A (6651650982): owner
and creator = the person who ran the install, then the ordinary rules.
## Security
No read is widened for anyone but the stamped person. What changes is
that each seeded row now has an owner:
- the stamped person reads, edits, deletes and transfers those rows as
their owner (pinned end to end: PATCH 200, DELETE 200);
- anyone whose access reaches that person's records reaches these rows
the same way: a role-hierarchy `readScope` above them, an owner-based
sharing rule naming them;
- a colleague holding only the package's default set at `readScope:
'own'` under `private` sharing reads none of them (pinned as accepted
behaviour);
- the stamp itself runs as the system with automations off, writes only
rows the load created and only empty fields, and never names
`usr_system`.
## Verification
Clause-②: no
Metadata-protocol: none
Measured on a real `os start` (tsx source entry, fresh home), inline
install of the fixture above:
| | `main` b77c160 | this branch |
|:--|:--|:--|
| sample rows' `owner_id` / `created_by` | NULL / NULL (4 of 4) | the
installer / the installer (4 of 4) |
| colleague granted the default set, `private` sharing | 0 rows, by-id
404 | 0 rows, by-id 404 (accepted: ordinary rule) |
| installer PATCH / DELETE a sample row | not measured (no owner) | 200
/ 200 as owner |
| purge | content-keyed | unchanged: deleted 3, the colleague's own deal
kept |
Tests, at head `58742b1d` (merged with `main` `badf24df`):
- `pnpm --filter @objectstack/cloud-connection test`: 71 files, 906
tests passed (includes the new
`marketplace-install-local-seed-owner.test.ts`, 16 tests: the stamp over
an in-memory engine, and each of the four doors' person).
- `pnpm --filter @objectstack/cloud-connection typecheck`: exit 0.
-
`packages/cli/test/package-install-local-sample-owner.integration.test.ts`:
6 passed, two `os start` boots on one home (install, colleague read,
owner edit and delete, purge, reseed, then a restart whose heal is owned
by the recorded installer); 38.4 s locally.
Ablations, each through `scripts/ablation-replace.mjs` (restore proven
against HEAD), dist-resolved ones rebuilt and proven with
`scripts/ablation-dist-preflight.mjs`:
| Mutation | Red |
|:--|:--|
| stamp `landedIds` instead of `insertedIds` | the matched-row pin (plus
the four door pins) |
| drop the "field still empty" term | the authored-owner pin |
| drop the organization term | the walled-install, replay and wall pins
|
| accept any string as a person (`''`, `usr_system`) | the no-person pin
|
| drop the platform/managed skip; drop the federated skip | the skips
pin (each) |
| heal names `installedBy` without the user lookup | the heal
no-installer pin |
| replay invents a person when no founder is named | the replay
no-founder pin |
| e2e: no stamp at all (dist proven) | install, reseed and heal owner
pins |
| e2e: fixture object `public_read` | the colleague-reads-none pin only
|
| e2e: purge content key broken (dist proven) | the purge-identification
pin only |
Controls after every restore: unit 16/16, e2e 6/6; dist rebuilt and
proven free of the markers.
Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 69
families; all 69 run, every one exit 0; `--ran` reconciliation: 69 run,
0 NOT-MEASURED, 0 UNRUN. `pnpm lint`: exit 0.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: n/a.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 section is
the dispatch's maintainer summary).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016WV4cwQrDVUWUzjAPq7jXa)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e8dc483 commit 4e8bbbf
6 files changed
Lines changed: 1193 additions & 2 deletions
File tree
- .changeset
- packages
- cli/test
- cloud-connection/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
0 commit comments