Skip to content

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

@objectstack-fleet

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 the repo:hotcrm seat (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 56d98f7e built to an artifact, then objectstack start --artifact … -d file:… --log-level debug on an EMPTY database. The default seed is 354 rows ([Seeder] Seed loading complete {"inserted":354,…}). The first user signed up through REST.

run seed settled wait before sign-up first POST /auth/sign-up/email
17.7.0, debug log 32 s after the health check went green 120 s more 45.8 s
17.7.0, browser (console Create Account) — ~45 s 44.3 s
17.7.0, REST, same procedure as the 17.6.0 row — 20 s 34.1 s
17.6.0, REST — ~20 s ~31 s
any later sign-up — — 0.02 s (403: self-registration closes after the first user, as designed)

So 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:

  • 09:21:45.5Z — [security] first user promoted to platform admin, then the default-organization bind.
  • 09:21:45.9Z → 09:22:30.6Z, 44.7 s, ending in:
    [security] handed 350 seeded record(s) to platform admin … (17 of 18 eligible object(s) had unowned rows) followed by platform bootstrap complete {"adminPromoted":true,"ownershipClaimed":350,…}.
  • The response is sent immediately after. The claim runs inside the sign-up request.

What those 44.7 s did, counted from the debug log of the window (13,261 lines):

what count
Update operation starting on business objects (crm_campaign 64, crm_opportunity 38, crm_quote 16, …) ~200
[BodyRunner] hook fired — app hooks bound from metadata, run in the QuickJS sandbox (opportunity_amount_rollup, campaign_metrics_refresh, opportunity_lifecycle, lead_automation, …) 1,254
record-change flow start conditions evaluated (Flow '…' skipped: start condition not met) 335
record-change flows that ran: case_escalation ×4, task_urgent_alert ×4 8 runs
approval requests opened (approval node suspended run, flow:opportunity_approval on two seeded deals) 2
notification emails handed to the transport ([LogTransport] would send email) 8
sys_audit_log / sys_activity inserts 392 / 391

Why it matters

  1. 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).

  2. A bookkeeping write fires business automation. Re-owning seed rows to the first admin is attribution, not a user event. Yet it:

    • escalates cases;
    • raises urgent-task alerts;
    • opens approval requests on demo deals;
    • emails the new admin.

    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 at engine.ts:5413: "seed loads end-state data, not user events". The claim that follows it does not keep that principle.

Code reading (origin/main 8caa131e52)

  • 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 no skipAutomations and no skipTriggers, 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.skipAutomations suppresses hooks bound FROM METADATA (entry.meta). Code-registered hooks run regardless: "audit, capability gates, sharing projection — have no meta and always run: the opt-out must never bypass security or audit (数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922)".
  • engine.ts:5413–5419: skipAutomations implies skipTriggers, so the record-change flow dispatch is suppressed too.

A direction, for triage to rule on (⛔ not a ruling)

  • Run the claim's reown writes with { isSystem: true, skipAutomations: true }.
    • The engine already keeps audit and the sharing projection running under that flag. The claim's own comments (the per-row hook budget, the plugin-sharing recompute) depend on exactly those code-registered hooks, not on app hooks.
    • The flag would remove the app hooks, the flows and the approvals/emails, and most of the 44.7 s.
    • Open question for the ruling: does any app-hook contract need to see an owner change made by the claim? An owner-propagation hook would be one. If so, that is the case that decides between skipAutomations and a narrower flag.
  • Separately: should the claim run inside the first sign-up request at all, or after the response (it already settles on app:seeded for later seeds)?
  • Not measured: the effect of either change. An A/B in the hotcrm session that patched the installed bundle was stopped by the session's safety policy (it would have edited installed dependencies). The measurement belongs on an objectstack branch built from source.

Related, not duplicates

Duplicate check

Semantic issue search on objectstack, three queries:

  • claimSeedOwnership seed ownership claim slow first sign-up fires hooks flows automation: 8 hits, all closed. The three above are the related ones; the rest concern dev-admin seeding, seed budgets and owner seeds.
  • first user sign up takes 30 seconds platform admin bootstrap: 0 hits.
  • ownership claim owner_id update triggers record-change flows approvals notifications emails skipAutomations seed: 6 hits, all closed and on other surfaces.

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

Activity

  1. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: 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's reown write runs with skipAutomations, and the claim stays inside the request in this card

    Triage 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 (the reown writer's execution context) ⇒ domain:services; rationale: the lane table puts plugin-security there. The engine's hook opt-out is read, not changed.

    • Verified on main (56bf27affb) at this write:
    • 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 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 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_id change. If any needs the claim's change, that is needs_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.
    • Pins:
    • 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.
  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 7 · 2026-10-07T10:29Z
    Session: session_01WMQprn46CND82KmY8sZWBu
    Account: os-warren (the seat's linked user as GET /user answers 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 (at origin/main 56bf27affb, per triage's direction 6035454999):

    • packages/plugins/plugin-security/src/claim-seed-ownership.ts: the reown writer (near :483) runs under a context that carries skipAutomations beside isSystem.
      • The fallback page path writes through the same reown, so it is covered.
      • readPage is unchanged.
    • Tests: the package's claim tests. The real-engine pins go in packages/qa/dogfood only if they need a booted app; any such file is declared on [PM seat] domain:cli — ⏳ vacant #6024 when it lands.
    • One patch changeset, with a line saying that app hooks no longer fire for the seed ownership claim.

    ⛔ No packages/objectql change (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:

    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

  3. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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",
    "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",
    "tooling note, not a product finding · evidence: scripts/tenant-audit-census.mjs elevationOf.unwrapLiteral (
    :1454) unwraps as and parentheses but not satisfies, while topLevelObjectDeclarations (
    :1292) does, so a context constant typed with satisfies reads 'elevation undecidable' · carrier: none (承接者:无), noted, not filed"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:services seat 2 (#21118) · session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T11:48Z. It adds to 6036046121 and 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 on ObjectQL over SqlDriver:

      • 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 need plugin-audit, plugin-approvals, service-messaging and trigger-record-change, none of which plugin-security depends on.

    Clause-②: no stands. One patch changeset.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: PR #22069 at aa21b6c3f9 · seat domain: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; assignee os-warren. Line 1 is Fixes #22067 and line 2 is Clause-②: 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 notes is 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.
    • Changeset: patch for @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 the app:seeded settle 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-②: no holds. No signature or accepted input changes. One system write dispatches less.
    • The diff:
      • The only runtime change is reown's context: SYSTEM_CTX becomes CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true }.
      • readPage keeps 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 isSystem statically and does not unwrap satisfies. Accepted; the census stays at 233 certified sites.
      • ⛔ No packages/objectql change, the claim stays inside the request, and no narrower flag was added.
    • Evidence:
      • The flag reaches all three points on a real engine: a meta-carrying hook gets 0 dispatches; code-registered hooks run per row with skipAutomations and skipTriggers true; the record-change handler reads skipTriggers.
      • 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_id the admin. A positive control fires them on the admin's own PATCH.
      • 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 the dist/ marker proven in and then out.
      • Gates: 69 derived gates exit 0 (--ran shows a derived zero).
      • Tests: the package's 3,640 tests and typecheck pass, as do 8 dogfood files that drive the claim.
    • Owner-change readers: none. The enumeration covers every first-party metadata hook and record-triggered flow in examples/**, and packages/** ships none outside fixtures. Triage's needs_decision branch does not arise.

    Seat's own read

    • The audit-user stamp. In packages/objectql/src/plugin.ts, the stamp hook writes created_by / updated_by only inside if (session?.userId), and the claim's context carries no user. So skipping sys_stamp_audit_update changes nothing for this write. The dev's measurement agrees: updated_by stays null as before, and updated_at advances.

    Deviations accepted

    • A real-engine pin file inside plugin-security, beside the dogfood pin, plus the dogfood file for the plugins plugin-security does 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=0 for 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.md requires.

    Findings

    • skipAutomations also skips ObjectQL's sys_stamp_audit_* builtins, which contradicts the engine's 数据导入:批量 insert 给 Hook 的输入形状与单条不一致(installFlatInput 失效);「运行自动化与触发器」开关是摆设且默认值应为选中 #2922 comment and the ExecutionContext.skipAutomations description. No effect on this write. A user import with "run automations" unchecked would not stamp created_by / updated_by; that reach is NOT MEASURED.
    • 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_bindings has no start condition and fires on any showcase_field_zoo update. This PR removes the claim's path to it; a user edit is NOT MEASURED. Acceptance notes; carrier: none.
    • The census's elevationOf does not unwrap satisfies. 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

  6. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 (#21118) · session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T12:39Z


    Generated by Claude Code

  7. added 2 commits that reference this issue on Oct 9, 2026
    e67ba80
    c8d06a9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions