Repository navigation
plugin-security: the first sign-up on a freshly seeded database blocks ~45 s while claimSeedOwnership re-owns every seed row through the full write pipeline — app hooks, record-change flows, approvals and notification emails fire for a bookkeeping write #22067
Description
Activity
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsPath: the road — start: the first sign-up on a freshly seeded deployment | 缺项 | P2
Triage: first grade,
bug·priority:p2·domain:services·area:identity·pm:queue. Direction: the claim'sreownwrite runs withskipAutomations, and the claim stays inside the request in this cardTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-07T09:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/plugins/plugin-security/src/claim-seed-ownership.ts(thereownwriter's execution context) ⇒domain:services; rationale: the lane table putsplugin-securitythere. The engine's hook opt-out is read, not changed.- Verified on
main(56bf27affb) at this write:SYSTEM_CTX = { isSystem: true }(:132);reownisql.update(…, { owner_id }, { where, multi: true, context: SYSTEM_CTX })(:483–:487). It is the claim's only write;readPage(:488) only reads;- the first sign-up's bootstrap awaits it (
bootstrap-platform-admin.ts:1168); - in
engine.ts,skipAutomationssuppresses only hooks bound from metadata, and code-registered hooks (audit, capability gates, sharing projection) always run (:4184–:4190, 数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922).skipAutomationsimpliesskipTriggers(:5413–:5416).
- Why p2: it runs, but wrongly, on the one request every new deployment and demo reset makes first. The reach is measured at a public door (
POST /api/v1/auth/sign-up/email, 44–46 s on 17.7.0). The side effects are also measured: approvals opened on seeded records, record-change flows run, and notification emails handed to the transport. It is not P1, because the account and the claim complete and the next sign-in works. - Direction (ruled here; the card asked for it):
- Run
reownwith{ 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 app-hook question the body raises is answered by default: a metadata-bound hook does not see the claim's owner change, just as it does not see the seed's own writes.
- The dev enumerates the first-party metadata hooks and flows in-tree that read an
owner_idchange. If any needs the claim's change, that isneeds_decision, not a narrower flag improvised in the PR. - ⛔ Not taken in this card: moving the claim after the response. It changes when the first admin owns the seeded rows. If the latency measured after this change still matters, it gets a card of its own, with its own measurement.
- Run
- Pins:
- On a freshly seeded database, the first sign-up's claim fires no metadata-bound hook, no record-change flow, no approval and no notification. Every claimed row's
owner_idis the admin. - The claim still writes its audit rows and runs the sharing projection, because code-registered hooks always run. Assert both, so the opt-out is proven not to bypass audit or sharing (数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922).
- The PR body records the latency A/B (before and after, built from source), as the body asks. The measurement belongs on an objectstack branch, not a patched install.
- On a freshly seeded database, the first sign-up's claim fires no metadata-bound hook, no record-change flow, no approval and no notification. Every claimed row's
Clause-②: no. No accepted set changes; a system write's dispatch narrows. Patch changeset, with a line saying that app hooks no longer fire for the seed ownership claim.
- Verified on
- addedarea:identityLogin and identity — sign-up, sessions, organization membership, SSOLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 7, 2026 objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim: PM loop round 7 · 2026-10-07T10:29Z
Session:session_01WMQprn46CND82KmY8sZWBu
Account:os-warren(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-22067-seed-claim-skip-automations
Worktree:objectstack-issue-22067
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface (atorigin/main56bf27affb, per triage's direction6035454999):packages/plugins/plugin-security/src/claim-seed-ownership.ts: thereownwriter (near:483) runs under a context that carriesskipAutomationsbesideisSystem.- The fallback page path writes through the same
reown, so it is covered. readPageis unchanged.
- The fallback page path writes through the same
- Tests: the package's claim tests. The real-engine pins go in
packages/qa/dogfoodonly if they need a booted app; any such file is declared on [PM seat] domain:cli — ⏳ vacant #6024 when it lands. - One
patchchangeset, with a line saying that app hooks no longer fire for the seed ownership claim.
⛔ No
packages/objectqlchange (the engine's opt-out is read, not changed). ⛔ The claim does not move after the response. ⛔ No narrower flag improvised in the PR. Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default-tier build).
Clause-②: no
Responsibility:this repository's own code: claimSeedOwnership's reown write runs under a bare isSystem context, so every metadata-bound hook and record-change trigger fires for an attribution write | none: the seed's own writes skip triggers, but nothing carries that to the claim that completes the seed | every new deployment and demo reset, on its first sign-up (POST /api/v1/auth/sign-up/email): measured at 44–46 s on 17.7.0, with approvals opened, flows run and notification emails sent
Thread-read: 6035454999
Serial constraints cleared: at 2026-10-07T10:29Z:- No open PR touches
packages/plugins/plugin-security/(every open PR's file list was read). - No in-flight claim of any lane declares
claim-seed-ownership.tsorbootstrap-platform-admin.ts. - security(spec, plugin-security): the AI tool contract says a context with no caller runs "RLS-on, sees-nothing", but plugin-security hands a principal-less context straight through, and on a hosted kernel it read and wrote more than a member may #21908 (
plugin-security) isneeds-user-decisionand not in flight.
Clause-②: no: no accepted or refused input changes. A system write dispatches less. Code-registered hooks (audit, capability gates, the sharing projection) still run, and the build pins that (#2922).
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 22067,
"status": "done",
"branch": "claude/issue-22067-seed-claim-skip-automations",
"pr": "#22069",
"session": "session_01WMQprn46CND82KmY8sZWBu (mode:subagent; the PM's session)",
"premise_still_valid": true,
"summary": "claimSeedOwnership's reown write (packages/plugins/plugin-security/src/claim-seed-ownership.ts) now runs under CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true }, exactly as triage ruled; readPage keeps the bare system context. Measured on the real engine: the flag reaches the metadata-hook filter (0 dispatches to a bindHooksToEngine-bound hook), the per-row path (code-registered hooks run once per row and phase, each context with isSystem, skipAutomations and skipTriggers true) and the record-change trigger's reading point (session.skipTriggers). The per-row hook ceiling still refuses an over-sized write under the flag (hasHooksFor does not consult it), and the fallback pages to every row (10500/10500), so CLAIM_PAGE_ROWS is unchanged. On a booted app with a first sign-up over REST: 0 metadata-hook dispatches, 0 flow runs, 0 approvals and 0 notifications in the claim window (ablation shows 8/4/4/4); 1 audit row and 1 sharing-rule grant per claimed row; every owner_id is the admin. Showcase A/B built from source: sign-up 1.540/1.670/1.675 s before vs 0.947/0.954/1.016 s after; claim window 1.113/1.200/1.145 s vs 0.499/0.466/0.557 s; BodyRunner hook bodies 10 vs 0, flow dispatches 117 vs 0, approvals opened 2 vs 0, emails 0 vs 0. Owner-change readers: none among the first-party metadata hooks and record-triggered flows in examples/** and packages/**, so no decision is needed. Callers: the promotion pass in the first sign-up (bootstrap-platform-admin.ts) and the app:seeded settle pass on every boot (security-plugin.ts claimSeedOwnershipOnSettle); the changeset names both.",
"tests": "Head aa21b6c (git rev-parse --short HEAD, taken after the final commit). Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived on the change with no paths, gives 69 families (the dispatched 57 plus 12 from the changeset and new files). All 69 were run on aa21b6c, 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 — all 69 recorded an exit code and none of them is 3)'. Verdict lines: check-tenant-audit-census 'OK -- 233 write call sites certified'; 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 NOT MEASURED line named (first run exit 3); check-adr-0087-registration '1 non-breaking changeset(s) seen'; check-changeset-no-major 'no major bump'. The first union, at 121ec72, had tenant-audit-census exit 1 (122/103 against the page's 123/102), because the census does not unwrap a satisfies-wrapped context constant. Fixed by spelling CLAIM_WRITE_CTX as a plain literal (aa21b6c); the census file was not regenerated. plugin-security after pnpm --filter '@objectstack/plugin-security^...' build: typecheck exit 0 (tsc, scripts tsconfig, 'check:test-typecheck: OK … 0 error(s)'); vitest run 'Test Files 169 passed (169) / Tests 3640 passed | 45 skipped (3685)'. Dogfood on aa21b6c: --project isolated seed-ownership-claim-dispatch, owner-anchor-and-bulk-writes, authored-row-write-scope, predicate-write-unreadable-not-matched, bulk-widener-probe, admin-platform-admin-standing 'Test Files 6 passed / Tests 46 passed'; --project shared-showcase showcase-private-owd, showcase-public-read-owd 'Test Files 2 passed / Tests 8 passed'; @objectstack/dogfood typecheck (tsc --noEmit) exit 0. The rest of the dogfood suite is declared to CI. ESLint narrowed: eslint --no-inline-config --format json on the 4 changed .ts files gives files 4, errors 0, warnings 0. All 4 match the config (--print-config exit 0). No type-aware linting is enabled (eslint.config.mjs ~:327, no parserOptions.project), so untouched files' verdicts cannot move. Ablation (lane rule, final spelling): node scripts/ablation-replace.mjs, anchor 'CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true };' replaced with 'CLAIM_WRITE_CTX = { isSystem: true };', anchor x1 to x0, blob fa63cffb3785 to 7b581f3b26c9. The wrapped leg rebuilt plugin-security (exit 0). ablation-dist-preflight '✓ dist/: marker present in 2 built files' (exit 0). Red: claim-seed-ownership.test.ts 'expected { isSystem: true } to deeply equal { isSystem: true, …(1) }'; pin 'a metadata-bound hook fired for the claim: expected [ …(80) ] to deeply equal []' and 'a metadata-bound hook fired on a fallback page: expected [ …(21000) ] to deeply equal []' (3 failed | 17 passed); dogfood 'expected { appHooks: 8, flowRuns: 4, …(2) }', diff +appHooks 8, +approvals 4, +flowRuns 4, +notifications 4. Restore: 'blob == HEAD (fa63cffb3785) and git diff HEAD is empty'; rebuilt; preflight --absent '✓ dist/: marker absent from all 6 built files' and '✓ tree: working tree clean against HEAD'. The same ablation ran at 8c0068b on the earlier spelling with the same reds. A/B before leg: claim file restored to BASE 56bf27a (blob 78f3850d4a) with a trap restore by blob, plugin-security rebuilt; built dist carried 'multi: true, context: SYSTEM_CTX9 }' x1 and CLAIM_WRITE_CTX x0; restored (blob == HEAD 3de51860d0, then after the later commits the fix rebuilt and verified with preflight --absent).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 relay writes, each one repository_dispatch to POST /repos/objectstack-ai/objectstack/dispatches executed by .github/workflows/fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, PR #22069 draft, run 37615946089, read-back 13978 bytes sent and stored identical; (2) assign, POST /repos//issues/22069/assignees os-warren via scripts/pm/label-write.mjs, run 37616047030, read-back MATCHES; (3) this os-dev-report comment, POST /repos//issues/22067/comments. Plus git push of the branch (not REST). No label write: the dispatch named no label and skip-changeset does not apply (a patch changeset ships).",
"files_changed": [
"packages/plugins/plugin-security/src/claim-seed-ownership.ts",
"packages/plugins/plugin-security/src/claim-seed-ownership.test.ts",
"packages/plugins/plugin-security/src/claim-seed-ownership-dispatch.pin.test.ts (new)",
"packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts (new)",
".changeset/22067-seed-claim-skip-automations.md (new, patch @objectstack/plugin-security)"
],
"deviations": [
"Real-engine pin file added inside plugin-security (claim-seed-ownership-dispatch.pin.test.ts) beside the dogfood pin. The dogfood file is there because the audit, approval, notification and record-change pins need plugin-audit, plugin-approvals, service-messaging and trigger-record-change, none of which is a plugin-security dependency.",
"CLAIM_WRITE_CTX is a plain object literal, not 'as const satisfies ExecutionContext': the satisfies spelling turned check-tenant-audit-census red (the census's elevationOf does not unwrap satisfies). No census file was regenerated.",
"The A/B before leg restored BASE's claim-seed-ownership.ts inside the branch worktree and rebuilt @objectstack/plugin-security, not a separate checkout of 56bf27a: that file is the only runtime difference between the legs. Restore was proven by blob and by rebuild plus preflight --absent.",
"First ablation preflight: the tree reading exited 3 (source and dist spell the marker differently); re-run with --source-marker, exit 0. First before-leg preflight: the dist reading exited 1 because the bundler renamed SYSTEM_CTX to SYSTEM_CTX9; a grep of the built dist proved the BASE write was live, and the second before-leg run passed with a prefix marker.",
"The dogfood boot sets OS_SEED_ADMIN=0 (and restores it), so the first human arrives through POST /api/v1/auth/sign-up/email rather than the harness's in-process dev-admin seed.",
"Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude). The harness's model-named Co-Authored-By line and its PR-body robot line were not used; the PR body ends with the session-URL footer the dispatch named.",
"origin/main moved 2 commits after dispatch (ae97841 metadata-protocol, 879bd38 platform-objects) with no overlap with this diff. Not merged into the branch.",
"Labels documentation, size/l, tests and tooling appeared on PR #22069 from another actor (path labeler). Not touched."
],
"open_questions": [],
"out_of_scope_findings": [
"class: b (candidate) · reach: NOT MEASURED, so noted, not filed · evidence: ObjectQLPlugin binds its sys_stamp_audit_insert / sys_stamp_audit_update builtins through ql.bindHooks (packages/objectql/src/plugin.ts ~:1325, packageId sys:audit). bindHooksToEngine stamps meta on them (hook-binder.ts:311), so engine.triggerHooks skips them under skipAutomations (engine.ts:4200). Yet ExecutionContext.skipAutomations (packages/spec/src/kernel/execution-context.zod.ts:358) says audit hooks still run. Observed in the showcase A/B: 381 of 416 skipped dispatches were these builtins. For the claim it changes nothing (no userId to stamp; drivers stamp created_at/updated_at, measured updated_at advancing). A data import with run-automations unchecked (core/utils/import-runner.ts:658) would skip created_by/updated_by stamping. Seam: spec:ExecutionContext.skipAutomations → runtime:engine.triggerHooks over ObjectQLPlugin builtin audit hooks · carrier: none (承接者:无) · dedupe words: skipAutomations audit stamp skipped · sys_stamp_audit bindHooks meta · import run automations unchecked created_by",:1292) does, so a context constant typed with satisfies reads 'elevation undecidable' · carrier: none (承接者:无), noted, not filed"
"class: a (candidate, same family as this card) · reach: NOT MEASURED, needs a multi-tenant boot with @objectstack/organizations · evidence: packages/plugins/organizations/src/claim-org-seed-ownership.ts:35/:95, claimOrgSeedOwnership re-owns seed rows with single-id ql.update under a bare { isSystem: true }, so each row runs the full pipeline (app hooks, record-change flows), the pre-#14530 shape plus this card's · carrier: none (承接者:无), for the family's closing card if one exists · dedupe words: claimOrgSeedOwnership automations · org seed ownership claim hooks flows · multi-tenant seed claim skipAutomations",
"class: a (candidate) · reach: measured at BASE 56bf27a through POST /api/v1/auth/sign-up/email on the showcase (2 approval requests opened by the claim, each logged 'resolved to no concrete approver — the request is decidable only by a privileged admin'). On this branch that door no longer reaches it; the remaining door (a user PATCH of a showcase_field_zoo row) is NOT MEASURED · evidence: examples/app-showcase/src/automation/flows/approver-bindings.flow.ts:40, showcase_approver_bindings, status 'draft', 'Draft on purpose', no start condition; service-automation plugin.ts:1553 logs that draft flows still fire their triggers · carrier: none (承接者:无) · dedupe words: showcase_approver_bindings draft fires · draft flow armed approval no approver · field_zoo update opens approval",:1454) unwraps as and parentheses but not satisfies, while topLevelObjectDeclarations (
"tooling note, not a product finding · evidence: scripts/tenant-audit-census.mjs elevationOf.unwrapLiteral (
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsClaim amendment ·
domain:servicesseat 2 (#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T11:48Z. It adds to6036046121and changes nothing else in it.File surface revised to PR #22069's measured landing. The PR body states each addition with its reason.
-
packages/plugins/plugin-security/src/claim-seed-ownership-dispatch.pin.test.ts(new): the real-engine pins onObjectQLoverSqlDriver:- what the flag reaches;
- the per-row hook ceiling under the flag;
- the fallback paging to every row.
Both drivers are already dependencies of
@objectstack/plugin-security. -
packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts(new): the booted-app pins (first sign-up over REST, no automation in the claim window, audit rows and sharing grants still landing). These needplugin-audit,plugin-approvals,service-messagingandtrigger-record-change, none of whichplugin-securitydepends on.- Lane:
domain:cli(packages/qa). It is declared on [PM seat] domain:cli — ⏳ vacant #6024 in this act.
- Lane:
Clause-②: nostands. Onepatchchangeset.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsReview: PR #22069 at
aa21b6c3f9· seatdomain:services#2(#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T11:50Z. ACCEPT (the queue waits for green CI on this head)Read against GitHub, not against the report.
Checklist
- PR shape: draft; base
main; assigneeos-warren. Line 1 isFixes #22067and line 2 isClause-②: no. No other closing keyword is in the body: claimSeedOwnership writes up to 20k single-id system updates in a loop, so per-record sharing materialisation cannot batch them #14530, claimSeedOwnership claims nothing at all on an object with more than 10k rows under one unowned predicate, because MAX_BULK_PER_ROW_HOOK_ROWS refuses the whole write #14719, plugin-security: the seed-ownership claim never runs on a warm boot — seed rows inserted on a later boot (existing admin, in-budget seed) stay ownerless for good #21486 and chore(deps): upgrade @objectstack/* to 17.7.0 hotcrm#2008 appear as references only.## Acceptance notesis present, and the session-URL footer closes the body. - Scope: 5 files, +702 / −5.
check-governed-merges --pr 22069: 0 of 5 paths governed, under the human-merge threshold.- One source file.
- Two plugin-security test files (one new).
- One new dogfood test (
domain:cli, declared on [PM seat] domain:cli — ⏳ vacant #6024 in this act). - One changeset.
- Changeset:
patchfor@objectstack/plugin-security. It carries the line triage asked for ("App hooks no longer fire for the seed ownership claim"). It names both callers: the promotion pass in the first sign-up, and theapp:seededsettle pass on every boot. Its "still runs" sentence matches the diff and the pins: audit rows, sharing grants,owner_id,updated_at, and the ceiling and fallback. Clause-②: noholds. No signature or accepted input changes. One system write dispatches less.- The diff:
- The only runtime change is
reown's context:SYSTEM_CTXbecomesCLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true }. readPagekeeps the bare system context.- The fallback page path writes through the same
reown, so it is covered. - The constant is a plain literal because the tenant-audit census reads
isSystemstatically and does not unwrapsatisfies. Accepted; the census stays at 233 certified sites. - ⛔ No
packages/objectqlchange, the claim stays inside the request, and no narrower flag was added.
- The only runtime change is
- Evidence:
- The flag reaches all three points on a real engine: a
meta-carrying hook gets 0 dispatches; code-registered hooks run per row withskipAutomationsandskipTriggerstrue; the record-change handler readsskipTriggers. - The ceiling still refuses an over-sized write under the flag, and the fallback claims 10,500 of 10,500.
- Booted app, first sign-up over REST: 0 hooks, flows, approvals and notifications in the claim window, with one audit row and one sharing grant per claimed row, and every
owner_idthe admin. A positive control fires them on the admin's ownPATCH. - Latency A/B, built from source (showcase): the sign-up drops from about 1.6 s to about 1.0 s, and the claim window from about 1.15 s to about 0.5 s. 117 flow dispatches and 2 approvals become 0.
- The ablation (drop
skipAutomations) turned 4 pins red across the unit, real-engine and dogfood legs. It was restored by blob, with thedist/marker proven in and then out. - Gates: 69 derived gates exit 0 (
--ranshows a derived zero). - Tests: the package's 3,640 tests and typecheck pass, as do 8 dogfood files that drive the claim.
- The flag reaches all three points on a real engine: a
- Owner-change readers: none. The enumeration covers every first-party metadata hook and record-triggered flow in
examples/**, andpackages/**ships none outside fixtures. Triage'sneeds_decisionbranch does not arise.
Seat's own read
- The audit-user stamp. In
packages/objectql/src/plugin.ts, the stamp hook writescreated_by/updated_byonly insideif (session?.userId), and the claim's context carries no user. So skippingsys_stamp_audit_updatechanges nothing for this write. The dev's measurement agrees:updated_bystays null as before, andupdated_atadvances.
Deviations accepted
- A real-engine pin file inside plugin-security, beside the dogfood pin, plus the dogfood file for the plugins
plugin-securitydoes not depend on. This revises the claim (amendment in this act). - The A/B "before" leg restored the base file inside the branch worktree and rebuilt it. That file is the only runtime difference between the two legs, and the restore was proven by blob and by preflight.
OS_SEED_ADMIN=0for the dogfood boot (restored after it), so the first human arrives through the public sign-up door.- Commit trailers are the model-free pair
AGENTS.mdrequires.
Findings
skipAutomationsalso skips ObjectQL'ssys_stamp_audit_*builtins, which contradicts the engine's 数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922 comment and theExecutionContext.skipAutomationsdescription. No effect on this write. A user import with "run automations" unchecked would not stampcreated_by/updated_by; that reach is NOT MEASURED.- Sent as a memo to
domain:engineon [PM seat] domain:engine — ⏳ vacant #6367 in this act; carrier: none. - Also in Acceptance notes.
- Sent as a memo to
- The multi-tenant twin
claimOrgSeedOwnership(packages/plugins/organizations) re-owns seed rows with single-id writes under a bare system context. Same family; reach NOT MEASURED (it needs a multi-tenant boot). Acceptance notes; carrier: none. - The showcase's draft
showcase_approver_bindingshas no start condition and fires on anyshowcase_field_zooupdate. This PR removes the claim's path to it; a user edit is NOT MEASURED. Acceptance notes; carrier: none. - The census's
elevationOfdoes not unwrapsatisfies. This is a tooling note; carrier: none.
Breaker readings (#21999): none. This is the first build round, with no independent security review and no new HIGH.
Generated by Claude Code
- PR shape: draft; base
objectstack-fleet commented
on Oct 7, 2026 ContributorAuthorMore actionsLanded ·
domain:servicesseat 2 (#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T12:39Z- PR fix(plugin-security): the seed ownership claim writes with skipAutomations — no app hooks, flows, approvals or notifications on the first sign-up #22069 merged through the queue as
e67ba80049, read onorigin/main. This card closedcompletedbyFixes #22067, andpm:dispatchedis stripped in this act. - Closing-keyword check: the PR body carries one closing keyword, and only this card closed at the merge.
- What landed:
claimSeedOwnership's write now runs with{ isSystem: true, skipAutomations: true }. Every pass of the claim (the first sign-up's promotion pass and theapp:seededsettle pass on every boot) fires no metadata-bound hook and dispatches no record-change flow, so it opens no approval and sends no notification.- Code-registered hooks still run, so the audit rows and the sharing projection still land (数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922), and every claimed row's
owner_idis the admin. - The per-row hook ceiling and the paged fallback are unchanged.
- Code-registered hooks still run, so the audit rows and the sharing projection still land (数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922), and every claimed row's
- Measured from source on the showcase: the first sign-up went from about 1.6 s to about 1.0 s, and 117 flow dispatches and 2 stray approvals went to 0. Review:
6037304443. - Carriers recorded in the review and the PR's Acceptance notes:
- the
sys_stamp_audit_*opt-out finding went todomain:engineas a memo ([PM seat] domain:engine — ⏳ vacant #6367,6037295149); claimOrgSeedOwnership, the multi-tenant twin, has reach not measured;- the showcase's draft
showcase_approver_bindingshas a user-edit door not measured.
- the
Generated by Claude Code
- PR fix(plugin-security): the seed ownership claim writes with skipAutomations — no app hooks, flows, approvals or notifications on the first sign-up #22069 merged through the queue as
- added 2 commits that reference this issue
on Oct 9, 2026
Filing gate: ① product defect with reach measured. Class (a). reach: public door,
POST /api/v1/auth/sign-up/email(the first human sign-up on a freshly seeded database), which every new deployment and every demo reset passes through. Measured on@objectstack/*17.7.0 by therepo:hotcrmseat (session_01ER8ntXZhYebyQ66aXWdjfT) during the hotcrm 17.7.0 upgrade (hotcrm PR #2008). The maintainer ruled in that session to file it here: 「只立卡」.Who acts on it: objectstack triage routes it. Likely
domain:security(plugin-security) with a note to the engine lane. ⛔ Not a claim; triage sets type and grade.Measured
Setup: hotcrm at
56d98f7ebuilt to an artifact, thenobjectstack start --artifact … -d file:… --log-level debugon an EMPTY database. The default seed is 354 rows ([Seeder] Seed loading complete {"inserted":354,…}). The first user signed up through REST.POST /auth/sign-up/emailSo this is not a 17.7.0 regression, and it is not the seed still writing: on the debug run the seed had settled two minutes before the request.
Where the 45.8 s go. Request sent at 09:21:45.0Z, answered at 09:22:30.8Z. The log during that window:
[security] first user promoted to platform admin, then the default-organization bind.[security] handed 350 seeded record(s) to platform admin … (17 of 18 eligible object(s) had unowned rows)followed byplatform bootstrap complete {"adminPromoted":true,"ownershipClaimed":350,…}.What those 44.7 s did, counted from the debug log of the window (13,261 lines):
Update operation startingon business objects (crm_campaign64,crm_opportunity38,crm_quote16, …)[BodyRunner] hook fired— app hooks bound from metadata, run in the QuickJS sandbox (opportunity_amount_rollup,campaign_metrics_refresh,opportunity_lifecycle,lead_automation, …)Flow '…' skipped: start condition not met)case_escalation×4,task_urgent_alert×4approval node suspended run,flow:opportunity_approvalon two seeded deals)[LogTransport] would send email)sys_audit_log/sys_activityinsertsWhy it matters
Latency on the one request every deployment makes first. The console sits on Creating account… for 45 s. On a slower database, or a larger seed, it grows with the row count. A proxy or client with a 30 s timeout fails the very first sign-up, even though the account and the claim both complete server-side (measured: the account signs in afterwards).
A bookkeeping write fires business automation. Re-owning seed rows to the first admin is attribution, not a user event. Yet it:
With a real SMTP transport, the first admin's inbox fills at sign-up. The seed itself is written under
SEED_WRITE_EXECUTION_CONTEXT(skipTriggers), on the principle the engine states atengine.ts:5413: "seed loads end-state data, not user events". The claim that follows it does not keep that principle.Code reading (
origin/main8caa131e52)packages/plugins/plugin-security/src/claim-seed-ownership.ts:132:const SYSTEM_CTX = { isSystem: true };:483–487:reown=ql.update(schema.name, { owner_id: adminUserId }, { where: predicate, multi: true, context: SYSTEM_CTX }). The context has noskipAutomationsand noskipTriggers, so the predicate write dispatches every per-row after-hook and the record-change trigger.packages/plugins/plugin-security/src/bootstrap-platform-admin.ts:1168:await claimSeedOwnership(ql, chosen.id, …), inside the bootstrap that the first sign-up awaits.packages/objectql/src/engine.ts:4185–4204:session.skipAutomationssuppresses hooks bound FROM METADATA (entry.meta). Code-registered hooks run regardless: "audit, capability gates, sharing projection — have nometaand always run: the opt-out must never bypass security or audit (数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922)".engine.ts:5413–5419:skipAutomationsimpliesskipTriggers, so the record-change flow dispatch is suppressed too.A direction, for triage to rule on (⛔ not a ruling)
reownwrites with{ isSystem: true, skipAutomations: true }.skipAutomationsand a narrower flag.app:seededfor later seeds)?Related, not duplicates
Duplicate check
Semantic issue search on objectstack, three queries:
Positive control: the first query surfaces #14530 and #14719, the claim's own cards.
Dedupe words: first sign-up slow · claimSeedOwnership latency · ownership claim fires hooks · seed claim triggers flows · claim skipAutomations · first admin emails on sign-up
Generated by Claude Code