Repository navigation
Commit e67ba80
fix(plugin-security): the seed ownership claim writes with skipAutomations — no app hooks, flows, approvals or notifications on the first sign-up (#22069)
Fixes #22067
Clause-②: no
## What changed
`claimSeedOwnership` hands every seeded row to the first platform admin.
Its one write, `reown` (a predicate write per unowned shape, paged only
when the engine refuses it for the per-row hook ceiling), now runs with
`{ isSystem: true, skipAutomations: true }` instead of a bare `{
isSystem: true }`. This is triage's ruling on the card (comment
6035454999), verbatim: "Run `reown` with `{ isSystem: true,
skipAutomations: true }`. This is the seed's own principle
(`engine.ts:5413`: seed loads end-state data, not user events), carried
to the attribution write that completes the seed." The page read
(`readPage`) is unchanged.
| file | change |
| --- | --- |
| `packages/plugins/plugin-security/src/claim-seed-ownership.ts` |
`CLAIM_WRITE_CTX`, used by `reown` only; the header says why, and which
hooks still run |
| `packages/plugins/plugin-security/src/claim-seed-ownership.test.ts` |
every `update` call (refused whole-set attempts, fallback pages, the
closing whole-set write) carries exactly `{ isSystem: true,
skipAutomations: true }` |
|
`packages/plugins/plugin-security/src/claim-seed-ownership-dispatch.pin.test.ts`
(new) | real `ObjectQL` over `SqlDriver`: what the flag reaches, and the
per-row hook ceiling |
|
`packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts`
(new) | booted app, first sign-up over REST: no automation in the claim
window; audit rows and sharing grants still land |
| `.changeset/22067-seed-claim-skip-automations.md` | `patch` for
`@objectstack/plugin-security`, with the line that app hooks no longer
fire for the seed ownership claim |
No `packages/objectql` change, the claim stays inside the request, and
no narrower flag was added.
## Latency A/B, built from source
Example: `examples/app-showcase`, the in-repo example that carries
metadata hooks and record-change flows on seeded objects. Each run:
`OS_SEED_ADMIN=0 objectstack dev --fresh --no-seed-admin --log-level
debug` on an empty database. The seed inserts 132 rows; 20 s after
`[Seeder] Seed loading complete`, the first user signs up with `POST
/api/v1/auth/sign-up/email` (curl `time_total`). The claim window runs
from `first user promoted to platform admin` to `handed 127 seeded
record(s) to platform admin` in the same request's log.
- **before**: this branch with `claim-seed-ownership.ts` restored to
`56bf27affb` (blob `78f3850d4a`), `@objectstack/plugin-security`
rebuilt; `dist/` verified to carry the base write (`multi: true,
context: SYSTEM_CTX9 }` x1, `CLAIM_WRITE_CTX` x0), then restored by blob
and rebuilt.
- **after**: this branch, rebuilt.
- One shared container for every run: absolute seconds are shared-box
readings, and the ratios are what carries.
| leg | sign-up wall time (3 runs) | claim window (3 runs) |
| --- | --- | --- |
| before | 1.540 / 1.670 / 1.675 s | 1.113 / 1.200 / 1.145 s |
| after | 0.947 / 0.954 / 1.016 s | 0.499 / 0.466 / 0.557 s |
Claim-window counts, identical in all three runs of each leg:
| in the claim window | before | after |
| --- | --- | --- |
| rows claimed | 127 (18 of 22 eligible objects) | 127 (18 of 22) |
| predicate writes (`Update operation starting`) | 44 | 44 |
| app hook bodies run (`[BodyRunner] hook fired`) | 10 | 0 |
| record-change flow dispatches (start condition evaluated) | 117 | 0 |
| flows run / approvals opened (`approval node suspended run`) | 2 / 2 |
0 / 0 |
| emails handed to the transport (`[LogTransport] would send email`) | 0
| 0 |
| metadata-bound dispatches skipped (`Skipping metadata-bound hook
(skipAutomations)`) | 0 | 416 |
- The showcase's flows gate on transitions (`status == "done" &&
previous.status != "done"` and the like), so an owner-only change
evaluates 117 start conditions and runs none of them, with one
exception. The draft `showcase_approver_bindings` flow has no condition,
so the claim ran it on both seeded `showcase_field_zoo` rows and opened
2 approval requests, each "resolved to no concrete approver". The
showcase wires no email transport, so 0 emails before and after.
- The 416 skipped dispatches: 35 app-hook dispatches
(`showcase_normalize_task_title` 10, `showcase_guard_task_reopen` 10,
`showcase_audit_task_completion` 10, `showcase_warn_over_budget` 5), 127
`sys_stamp_audit_update`, and 254 `sys_stamp_audit_insert` on
plugin-audit's 127 `sys_audit_log` and 127 `sys_activity` inserts.
- The card's hotcrm reading (17.7.0, a 354-row seed, about 45 s, 1,254
app hooks, 8 flow runs, 2 approvals, 8 emails) is far larger than
anything in this repo's examples. The dogfood fixture below carries the
hotcrm shape: a condition-free flow that notifies and opens an approval,
on seeded rows.
## The pins
- **The claim dispatches no automation**, measured on a booted app
(`seed-ownership-claim-dispatch.dogfood.test.ts`). Four seeded deals
carry a condition-free metadata hook and a condition-free record-change
flow (notify the owner, then an approval). The real chain is mounted:
automation, `RecordChangeTriggerPlugin`, approvals, messaging,
`AuditPlugin` and sharing. The first user signs up over `POST
/api/v1/auth/sign-up/email` (dev admin seed off). The claim window shows
0 metadata-hook dispatches, 0 flow runs, 0 approvals and 0
notifications, and every deal's `owner_id` is the admin. A positive
control (the admin's own `PATCH` of one deal) fires the hook twice and
runs the flow once, with its approval.
- **Audit and sharing still run (#2922).** The same window has one
`sys_audit_log` update row per claimed deal, each recording the new
owner and carrying `created_at`. It also has one `sys_record_share` rule
grant per deal, from a criteria rule (`record.owner_id != null`, shared
with the `owner_id` field) whose grants only the claim's owner change
can earn. Before the sign-up there are 0 grants.
- **What the flag reaches**, measured on a real engine
(`claim-seed-ownership-dispatch.pin.test.ts`). A hook bound through
`bindHooksToEngine` (so it carries `meta`, like an app hook) gets 0
dispatches. A code-registered hook gets one per-row dispatch per claimed
row and phase, each with `isSystem`, `skipAutomations` and
`skipTriggers` true. `skipTriggers` is the one field
`RecordChangeTrigger`'s handler reads before dispatching a flow.
Control: a plain system predicate write over the same rows fires the
metadata hook 2 x N.
- **The per-row hook ceiling (mechanism hypothesis 2), measured.** With
`MAX_BULK_PER_ROW_HOOK_ROWS + 500` unowned rows, a `{ isSystem: true,
skipAutomations: true }` predicate write is still refused whole (`code`
and `limit` asserted, nothing written, no hook run). The engine counts
matched rows against the ceiling whenever any hook covers the object and
does not consult the flag. The claim then pages to every row (10 500 of
10 500), with 0 metadata-hook dispatches. `CLAIM_PAGE_ROWS` is
unchanged, because its derivation is still right.
- **Every caller (mechanism hypothesis 3).** Both callers write through
`claimSeedOwnership`: the promotion pass (`bootstrap-platform-admin.ts`,
inside the first sign-up) and the `app:seeded` settle pass
(`security-plugin.ts` `claimSeedOwnershipOnSettle`, on every boot
including warm boots). The changeset names both.
**Ablation** (`node scripts/ablation-replace.mjs`, anchor
`CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true };` replaced
with `CLAIM_WRITE_CTX = { isSystem: true };`, landed x1 to x0, blob
`fa63cffb37` to `7b581f3b26`):
- `ablation-dist-preflight` found the marker in `dist/index.js` and
`dist/index.mjs`.
- The claim unit test went red: `expected { isSystem: true } to deeply
equal { isSystem: true, …(1) }`.
- The real-engine pin went red twice: `a metadata-bound hook fired for
the claim: expected [ …(80) ] to deeply equal []`, and on the fallback
`expected [ …(21000) ] to deeply equal []`.
- The dogfood pin went red: `appHooks: 8, flowRuns: 4, approvals: 4,
notifications: 4` against all-zero.
- Restore: blob == HEAD and `git diff HEAD` empty,
`@objectstack/plugin-security` rebuilt, and `--absent` preflight shows
the marker gone from all 6 built files with a clean tree.
## Owner-change readers (first-party, in tree)
I enumerated every metadata-bound hook and record-triggered flow in
`examples/**` and `packages/**`. Outside test fixtures, `packages/**`
ships none. **None reads an `owner_id` change**, so this needs no
decision:
- **app-crm.** Hook `opportunity_stage_probability` (`crm_opportunity`,
before insert and update) reads `stage`. `convert-lead` is a screen
flow, not record-triggered.
- **app-showcase hooks** (`src/data/hooks/index.ts`).
`showcase_normalize_task_title` reads `title`,
`showcase_audit_task_completion` reads `done`,
`showcase_warn_over_budget` reads `spent`/`budget`,
`showcase_stamp_inquiry_defaults` reads `status`/`source` on insert, and
`showcase_guard_task_reopen` reads `done`.
- **app-showcase record-triggered flows**
(`src/automation/flows/index.ts` unless noted). Their start conditions
read `status` (`showcase_task_completed`, `_task_completed_slack`,
`_task_completed_rest_ping`, `_task_done_notify_owner`,
`_fan_out_notify`, `_resilient_sync`, `_project_closure`,
`_invoice_signoff`, `_expense_signoff`, `_committee_quorum`), `assignee`
(`showcase_task_assigned_notify`), `budget`
(`showcase_budget_approval`), `health` (`showcase_project_escalation`),
`priority` (`showcase_urgent_task_alert`) and `title`
(`showcase_dynamic_approval`, `dynamic-approval.flow.ts`). Three are
insert-only (`showcase_declarative_connector_ping`,
`_mcp_connector_echo`, `_task_follow_up`). The draft
`showcase_approver_bindings` (`approver-bindings.flow.ts`) has no
condition. Several notify `{record.owner}`, a business field, not
`owner_id`.
- **app-todo.** Hook `task_logic` and flow `task_completion` read
`status`.
- **Fixture only.** `packages/verify/src/handle.fixture.ts` hooks on
`hnd_deal` read no owner field.
## Gates and tests (head `aa21b6c3f9`)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, re-derived on this change with no paths,
gives 69 families: the 57 at dispatch plus 12 for the changeset and the
new files. All 69 were run on `aa21b6c3f9`, exit code captured before
any pipe, every one `exit 0`. `--ran` with the exit codes: "69 derived
famil(ies) accounted for — 69 run, 0 NOT-MEASURED (a DERIVED zero …)".
- `check-tenant-audit-census`: "OK -- 233 write call sites certified".
The write context is a plain literal on purpose. Spelled `{ … } as const
satisfies ExecutionContext`, it moved this write from "decidably
elevated" to "elevation undecidable" in the census (122/103 against the
page's 123/102), because the census does not unwrap `satisfies`.
- `check:engine-double-contract`: "OK — 980 pinned". `check:nul-bytes`:
"OK (scanned 10064 text file(s) …)". `check:dual-build-cjs-loads`: "106
published require entry point(s) across 66 package(s) load", after
building the 8 packages its prerequisite named.
- `@objectstack/plugin-security`, after `pnpm --filter
'@objectstack/plugin-security^...' build`:
- `typecheck` passes: `tsc` plus `check:test-typecheck: OK … 0
error(s)`;
- `vitest run` gives 169 files, 3640 passed, 45 skipped.
- Dogfood tests that drive the seed claim. Every `bootStack` boot runs
it through the dev admin seed; the new one runs it through REST.
- `--project isolated`: `seed-ownership-claim-dispatch`,
`owner-anchor-and-bulk-writes`, `authored-row-write-scope`,
`predicate-write-unreadable-not-matched`, `bulk-widener-probe` and
`admin-platform-admin-standing`, 6 files, 46 passed.
- `--project shared-showcase`: `showcase-private-owd` and
`showcase-public-read-owd`, 2 files, 8 passed.
- `@objectstack/dogfood` `typecheck` passes.
- The rest of the dogfood suite is left to CI's Dogfood Regression Gate.
- `eslint --no-inline-config --format json` on the 4 changed `.ts` files
gives 4 files, 0 errors, 0 warnings. Each file matches the config
(`--print-config` exits 0). This repo's config enables no type-aware
linting (`eslint.config.mjs` near line 327: no `parserOptions.project`),
so this diff cannot move any untouched file's verdict. The repo-wide
`pnpm lint` is CI's.
## Acceptance notes
- **ObjectQL's `sys_stamp_audit_insert` / `sys_stamp_audit_update` are
skipped under `skipAutomations`.** They are bound through `ql.bindHooks`
(`packages/objectql/src/plugin.ts`, packageId `sys:audit`), so they
carry `meta`. The A/B log shows them among the 416 skipped dispatches.
- For this write that changes nothing. The claim has no user to stamp
into `updated_by`, and every driver stamps `created_at`/`updated_at`
itself. Measured on the dogfood fixture: the claimed rows' `updated_at`
advances (`…:55.456Z` to `…:57.042Z`), `updated_by` stays null as
before, and the claim's audit rows carry `created_at`.
- It does contradict the `ExecutionContext.skipAutomations` description,
which lists audit among the hooks that still run. For a data import with
"run automations" unchecked, `created_by`/`updated_by` stamping would be
skipped. That is NOT MEASURED. Noted, not filed. Carrier: none.
- **The multi-tenant twin, `claimOrgSeedOwnership`**
(`packages/plugins/organizations/src/claim-org-seed-ownership.ts`),
re-owns seed rows with single-id writes under a bare `{ isSystem: true
}`, so it has the same dispatch shape. It is not touched here. Its reach
is NOT MEASURED (it needs a multi-tenant boot). Carrier: none.
- **The draft `showcase_approver_bindings`** says "Draft on purpose",
but draft flows fire their triggers (service-automation logs that at
boot). At `56bf27affb` the first sign-up's claim ran it and opened 2
approval requests with no concrete approver. On this branch the claim no
longer reaches it. A user edit of a `showcase_field_zoo` row still
would; that is NOT MEASURED. Carrier: none.
References only: #14530, #14719, #21486, objectstack-ai/hotcrm#2008.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 879bd38 commit e67ba80
5 files changed
Lines changed: 702 additions & 5 deletions
File tree
- .changeset
- packages
- plugins/plugin-security/src
- qa/dogfood/test
| 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 | + | |
| 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 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
| 23 | + | |
22 | 24 | | |
23 | 25 | | |
24 | 26 | | |
| |||
27 | 29 | | |
28 | 30 | | |
29 | 31 | | |
| 32 | + | |
| 33 | + | |
30 | 34 | | |
31 | 35 | | |
32 | 36 | | |
| |||
90 | 94 | | |
91 | 95 | | |
92 | 96 | | |
93 | | - | |
| 97 | + | |
94 | 98 | | |
95 | 99 | | |
96 | 100 | | |
97 | | - | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
98 | 108 | | |
99 | 109 | | |
100 | 110 | | |
| |||
127 | 137 | | |
128 | 138 | | |
129 | 139 | | |
| 140 | + | |
130 | 141 | | |
131 | 142 | | |
132 | 143 | | |
| |||
512 | 523 | | |
513 | 524 | | |
514 | 525 | | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
515 | 563 | | |
0 commit comments