Skip to content

plugin-auth: register the app:seeded backfill handler from the arming kernel:ready handler, so the guard #22312 added is structural and check:settings-bind-window can see it (child of #22316) #22328

Description

@objectstack-fleet

This card carries the plugin-auth half of #22316: one structural move in packages/plugins/plugin-auth/src/auth-plugin.ts. The parent keeps the gate half, PR #22325 (scripts/check-settings-bind-window.mjs), which lands after this one.

Filing gate: an in-flight derivative sub-issue of #22316 (domain:devx, priority:p3 inherited), filed and owned by the claiming seat (domain:devx seat 1, #6023, session_0115N1oNnQS5WqofZ2DzaT3q). Reader: that seat, which dispatches it directly.

Why

Done when

The decision behind this split is recorded on #22316.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_0115N1oNnQS5WqofZ2DzaT3q
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22328-plugin-auth-seeded-hook-structural
    Worktree: objectstack-issue-22328
    Domain: domain:devx (inherited from the parent #22316; the file is domain:services', declared on #6021)
    Seat: domain:devx#1
    File surface: packages/plugins/plugin-auth/src/auth-plugin.ts (the app:seeded registration and, only if provably dead, the backfillArmed flag), the package's tests only if a pin must name the new structure, and one patch .changeset/22328-*.md for @objectstack/plugin-auth (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — the default tier; dispatch-gates --tier: "no path-derived mandate", so the PM's per-card call (a small move inside an auth plugin's boot ordering, where behaviour must stay byte-identical)
    Clause-②: no
    Responsibility: plugin-auth's own registration shape | PR #22312's runtime flag already keeps behaviour correct; this makes it visible to the gate | no user-visible change; the widened gate (PR #22325) is what reaches it
    Thread-read: none
    Serial constraints cleared: no open PR touches packages/plugins/plugin-auth/src/auth-plugin.ts (PR #22312 merged, PR #22186 closed); PR #22325 (this seat, parent) lands after this card's PR — read at 2026-10-08T17:28Z


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 22328,
      "status": "done",
      "branch": "claude/issue-22328-plugin-auth-seeded-hook-structural",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/22333",
      "session": "session_0115N1oNnQS5WqofZ2DzaT3q — the parent PM session (this run is its subagent)",
      "premise_still_valid": true,
      "summary": "auth-plugin.ts now registers the ADR-0093 D6 backfill's app:seeded handler from inside the kernel:ready handler that arms the pass, in the same synchronous step (arm, register, return runBackfill('kernel:ready')), instead of unconditionally from start(). PR #22325's widened gate (head 96d46ea02d) reads GREEN over this tree with no ledger entry, RED on unmodified origin/main 28bff18d0c (control), and RED again when the ablation moves the registration back. backfillArmed is KEPT, not dead: the default-org-created trigger (runBackfillOnDefaultOrg) still reaches runBackfill before arming from the objectql middleware during Phase 2 and from the bootstrap's own kernel:ready hook, which runs ahead of the arming one. Zone-2 assumptions MEASURED on real ObjectKernel and LiteKernel with a throwaway A/B probe (base vs fix, pass stubbed undecided): (1) holds — hook() pushes into the kernel's hooks map and trigger() reads it at dispatch time over the live array; a post-ready emit runs the pass in both shapes (1 then 2 passes); (2) holds — no emit the flag let through is lost (P1, P2 in-flight-with-blocker-before-slot, P5 in-budget awaited: identical). Two scheduling non-identities were measured in synthetic compositions, both from appending at ready time: P3 an emit still in flight across arming whose base slot was a pre-arming no-op now reaches the appended handler (base 1 pass, fix 2; the extra call is post-bind and stops at backfillDecided once decided); P4 for a post-ready emit the auth handler now runs after a start()-registered subscriber of a plugin that starts after auth (base auth,other; fix other,auth). Neither has an in-repo instance on os serve: the only start()-registered app:seeded subscriber besides auth is platform-objects, composed before AuthPlugin (serve.ts 3945 vs 4102); plugin-security and the CLI announcer subscribe in init(). No structural shape is truly identical (the kernel has no insert-at-position; a start()-time registration is what the gate rejects), so the ruling's shape is the smallest structural one. One existing unit test that pinned the start()-time registration now pins the new structure; both #22312 pins pass unchanged.",
      "tests": "All at head 9c8ed2b62e. Closure: os-verify-lock -c \"pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' build\" VERDICT command-exit 0 (29 of 81 projects), then pnpm --filter @objectstack/plugin-auth build. Typecheck: pnpm --filter @objectstack/plugin-auth typecheck VERDICT command-exit 0 (\"check:test-typecheck: OK — ... 10 file(s) / 94 error(s) / 23 pinned signature(s) held\"). Full suite: pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 VERDICT command-exit 0, \"Test Files 129 passed (129)\", \"Tests 2657 passed | 10 skipped (2667)\". Named, verbose: auth-settings-seeded-boot.pin.test.ts 2/2 (no Pre-bind READ; control reports exactly 1), auth-settings-ordering.pin.test.ts 5/5, auth-plugin.test.ts 'Membership backfill re-run on app:seeded (#2996)' 11/11 incl. the rewritten 'registers its app:seeded hook from the arming kernel:ready handler, never from start()', membership-policy-setting.test.ts — 4 files, 123 passed. Gate proof (PR #22325 script, md5 dbd45f65ff759cded2ddbcf7e349d026 = git show 96d46ea02d:scripts/check-settings-bind-window.mjs, run from a scratch root whose scripts/ symlinks this worktree's scripts except that file and whose packages and node_modules symlink this worktree): control origin/main 28bff18d0c exit 1 \"auth-plugin.ts:1591 ... [app:seeded-hook-from-start] ... 1 settings read(s) in the pre-bind window with no declaration covering them.\"; this tree exit 0 \"✓ settings bind-window: 4 declared / 0 self / 1 structurally upstream / 0 ledgered (73 plugin unit(s) scanned, 8 pre-bind hook(s) fired = pinned, provider 'com.objectstack.service.settings').\" Ablation (fix committed first; scripts/ablation-replace.mjs WRAP mode inside a bash script with trap restore EXIT INT TERM using git checkout HEAD -- absolute path): \"anchor 1 -> 0, blob 49113b925d97 -> 04decbf3fac2\", on-disk counts in-handler 1->0 / start-time 0->1, gate exit 1 at auth-plugin.ts:1603 [app:seeded-hook-from-start]; restore \"blob == HEAD (49113b925d97) and git diff HEAD is empty\", own check git hash-object 49113b925d9785632b45ea864d83f97d8a41b70f == HEAD blob, status clean, counts back to 1/0. No dist/build in the ablation path (the gate reads source). Behaviour A/B probe (throwaway zz-probe22328*.ts in plugin-auth/src, deleted before gate derivation, never committed): P0 handlers at start()-time emit/after boot base 1/1 fix 0/1 (both kernels); P1 post-ready emit passes 1->2 both shapes both kernels; P2 2=2; P5 1=1; P3 base 1 fix 2; P4 base auth,other fix other,auth.",
      "gates": {
        "head": "9c8ed2b62e",
        "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack — 67 commands; 3 paths vs merge base 28bff18d0; 48 changed lines (+38/-10)",
        "reconciliation": "dispatch-gates --ran: \"✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).\"",
        "results": [
          "exit 0 node scripts/check-adr-0087-registration.mjs --base origin/main",
          "exit 0 node scripts/check-adr-0087-registration.mjs --self-test",
          "exit 0 node scripts/check-changeset-no-major.mjs --base origin/main",
          "exit 0 node scripts/check-changeset-no-major.mjs --self-test",
          "exit 0 node scripts/check-ci-filter-parity.mjs",
          "exit 0 node scripts/check-closing-keyword-parity.mjs",
          "exit 0 node scripts/check-closing-keyword-parity.mjs --self-test",
          "exit 0 node scripts/check-comment-mask-adoption.mjs",
          "exit 0 node scripts/check-comment-mask-adoption.mjs --self-test",
          "exit 0 node scripts/check-comment-mask-corpus.mjs",
          "exit 0 node scripts/check-dev-prereqs.mjs --self-test",
          "exit 0 node scripts/check-dts-emitted.mjs --self-test",
          "exit 0 node scripts/check-empty-changeset.mjs --base origin/main",
          "exit 0 node scripts/check-empty-changeset.mjs --self-test",
          "exit 0 node scripts/check-issue-citations.mjs",
          "exit 0 node scripts/check-keyed-text-bounds.mjs",
          "exit 0 node scripts/check-keyed-text-bounds.mjs --self-test",
          "exit 0 node scripts/check-platform-object-tenancy-census.mjs",
          "exit 0 node scripts/check-platform-object-tenancy-census.mjs --self-test",
          "exit 0 node scripts/check-plugin-teardown-shape.mjs",
          "exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test",
          "exit 0 node scripts/check-registry-log-declared.mjs",
          "exit 0 node scripts/check-registry-log-declared.mjs --self-test",
          "exit 0 node scripts/check-rest-log-spy-declared.mjs",
          "exit 0 node scripts/check-rest-log-spy-declared.mjs --self-test",
          "exit 0 node scripts/check-system-context-census.mjs",
          "exit 0 node scripts/check-system-context-census.mjs --self-test",
          "exit 0 node scripts/check-tenant-audit-census.mjs",
          "exit 0 node scripts/check-tenant-audit-census.mjs --self-test",
          "exit 0 node scripts/check-undeclared-dep-imports.mjs",
          "exit 0 node scripts/check-undeclared-dep-imports.mjs --self-test",
          "exit 0 node scripts/docs-audit/check-affected-docs.mjs",
          "exit 0 node scripts/docs-audit/check-drift-comment.mjs",
          "exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test",
          "exit 0 node scripts/release-pending-publish.mjs --self-test",
          "exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys",
          "exit 0 pnpm check:auth-mount-ledger",
          "exit 0 pnpm check:changeset-gate-self-tests",
          "exit 0 pnpm check:cross-package-test-inputs",
          "exit 0 pnpm check:dispatcher-error-vocabulary",
          "exit 0 pnpm check:doc-authoring",
          "exit 0 pnpm check:driver-memory-census",
          "exit 0 pnpm check:dts-closure",
          "exit 3 pnpm check:dual-build-cjs-loads — NOT MEASURED: PREREQUISITE NOT MET (reads every publishable package dist/; only the plugin-auth closure is built locally). Targeted substitute: require('@objectstack/plugin-auth') resolves dist/index.js and loads, AuthPlugin: function, exit 0; the diff adds no import.",
          "exit 0 pnpm check:engine-double-contract",
          "exit 0 pnpm check:gitlink-declared",
          "exit 0 pnpm check:issue-citations",
          "exit 0 pnpm check:lean-entry-closure",
          "exit 0 pnpm check:logger-receiver-detach",
          "exit 0 pnpm check:nul-bytes (\"scanned 10305 text file(s) ... no raw ASCII control bytes\")",
          "exit 0 pnpm check:objectql-double-limit",
          "exit 0 pnpm check:objectui-changeset",
          "exit 0 pnpm check:org-identifier",
          "exit 0 pnpm check:page-declaration-shape",
          "exit 0 pnpm check:pm-changeset-deadline-census",
          "exit 0 pnpm check:published-files",
          "exit 0 pnpm check:query-options-erasure",
          "exit 0 pnpm check:refd-timer-probe",
          "exit 0 pnpm check:route-envelope",
          "exit 0 pnpm check:slot-lookup",
          "exit 0 pnpm check:sourcemap-no-sources-content",
          "exit 0 pnpm check:test-source-alias",
          "exit 0 pnpm check:tier-file-adoption",
          "exit 0 pnpm check:type-check-coverage",
          "exit 0 pnpm check:type-check-debt",
          "exit 0 pnpm check:watch-hint-literal",
          "exit 0 pnpm check:where-matcher",
          "exit 0 pnpm check:settings-bind-window (this tree's own gate, outside the derivation: \"4 declared / 0 self / 1 structurally upstream / 0 ledgered\")"
        ],
        "pr_body_gate": "PR_BODY=... node scripts/check-partof-closing-keyword.mjs: exit 0 (\"✓ check:partof-closing-keyword: this PR carries no Part-of/closing-keyword contradiction ...\")",
        "ci": "in_progress: draft PR #22333 at head 9c8ed2b62e just opened; not awaited (CI convergence is the PM's)."
      },
      "line_budget": "n/a — the diff touches no skills/** and no line-ratcheted governed ledger",
      "files_changed": [
        "packages/plugins/plugin-auth/src/auth-plugin.ts (+20 / -8: the app:seeded registration moved into the arming kernel:ready handler, its comment moved with it plus a structural note)",
        "packages/plugins/plugin-auth/src/auth-plugin.test.ts (+6 / -2: the test pinning the start()-time registration now pins the new structure)",
        ".changeset/22328-auth-seeded-hook-registered-at-ready.md (+12: patch for @objectstack/plugin-auth, Clause-②: no)"
      ],
      "deviations": [
        "One existing package test changed (auth-plugin.test.ts, 'registers an app:seeded hook alongside kernel:ready' asserted the hook right after init()+start(), i.e. the old structure). Inside the claim's file surface ('the package's tests only if a pin must name the new structure'); the two #22312 pins are unchanged.",
        "Done-when 'Runtime behaviour is unchanged': no emit is lost and every in-repo composition measured is outcome-identical, but two scheduling differences exist in synthetic compositions (P3, P4 in summary). Reported, not hidden; no behaviour was altered to get green.",
        "Gate proof ran PR #22325's script from a scratch root (symlink mirror) rather than copying it into the worktree, so the derivation's change set never saw an untracked script. Not committed anywhere.",
        "check:dual-build-cjs-loads NOT MEASURED locally (exit 3, PREREQUISITE NOT MET: whole-workspace dist required). Declared narrowing with a targeted plugin-auth CJS load; CI runs the full gate.",
        "A temporary copy of base auth-plugin.ts and a probe test lived in packages/plugins/plugin-auth/src for one locked vitest run, then were deleted; git status clean before the gate derivation and the ablation.",
        "PR #22325's branch was fetched into the owned ref refs/os-dev-22328/pr22325 (shared refs namespace); deleted at cleanup.",
        "Attribution: commits carry the model-free pair (Claude-Session + Co-authored-by: Claude) and the PR body the AGENTS.md session-URL footer, not the harness reminder's model-named trailer or robot footer (AGENTS.md outranks it; the pre-push hook refuses a model identifier).",
        "Clause-②: no copied into the PR body as its second line, per the claim.",
        "Cleanup: node_modules and the worktree are removed right after this report is posted (post-stamped needs the worktree's node_modules)."
      ],
      "mcp_calls": "0 — no MCP GitHub calls (reads via gh api REST GETs)",
      "api_writes": "3 — all through the scripts/pm relay as objectstack-fleet[bot]: (1) fleet-write/dispatch pr_create = POST /repos/objectstack-ai/objectstack/dispatches, relay POST /repos/objectstack-ai/objectstack/pulls -> #22333, body read back identical (10446 bytes); (2) label-write --assign os-sales = one dispatch, relay POST /repos/objectstack-ai/objectstack/issues/22333/assignees, read back assignee os-sales (labels documentation, size/s, tests, tooling are the labeler's, untouched); (3) post-stamped --comment=22328 for this report = POST /repos/objectstack-ai/objectstack/issues/22328/comments. Plus 2 git pushes (empty-branch probe, then the commit), not REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed — PR #22325's gate population is hook handlers; the objectql middleware path (registerMiddleware -> runEnsure -> runBackfillOnDefaultOrg -> runBackfill) can reach the settings read during Phase 2 and is guarded only by the runtime flag backfillArmed, which the gate cannot see. Not a defect (the flag holds at runtime) and no producer writes metadata here; recorded in the PR's Acceptance notes. Dedupe words: settings-bind-window middleware population · runBackfillOnDefaultOrg pre-bind · default-org-created backfill flag"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #22333 (head 9c8ed2b62e) · 2026-10-08T18:57Z

    Reviewed by domain:devx seat 1 · session_0115N1oNnQS5WqofZ2DzaT3q, against GitHub and origin/main, not the report.

    Checklist: draft, base main, first line Fixes #22328. A full body scan finds that one closing keyword and no other (the body also cites #22316, #22325, #22312, #11045 and #2996, with no verb beside any of them). The PR changes 3 files, +38/−10: auth-plugin.ts, auth-plugin.test.ts and one changeset. That is the claim's file surface. @objectstack/plugin-auth is published (no private), and it carries a patch changeset. Not governed (check-governed-merges --pr 22333: 0 of 3 paths). No os-regen path is touched. Clause-②: no, and the path limb is clean (no packages/spec).

    Code, read by the seat at the PR head:

    • The app:seeded registration now sits inside the arming kernel:ready handler, between backfillArmed = true and return runBackfill('kernel:ready'), in one synchronous step. This is the shape ruled in 6065407005.
    • The reason for keeping backfillArmed holds on the head:
    • OS_SKIP_MEMBERSHIP_BACKFILL=1 still gates the whole block, so no app:seeded handler is registered at all in that case.

    Test: the start()-time pin became a pin of the new structure: 0 app:seeded handlers after start(), and exactly 1 after kernel:ready. That assertion fails on the old structure. The two #22312 pins are unchanged.

    Changeset sentences checked against the diff:

    • "Its handler used to be registered in start() and kept from acting early by a runtime flag".
    • "The handler is now registered by that kernel:ready hook, at the moment it arms the backfill, so it does not exist before the settings engine binds."
    • "An app:seeded that fires before the backfill is armed still does nothing, and one that fires after it still re-runs the pass."
    • "OS_SKIP_MEMBERSHIP_BACKFILL=1 still registers no app:seeded handler at all."

    All four are true on the head. The third holds on every in-repo composition. The report's P3 (an emit still in flight across arming, behind a slower subscriber placed after the auth slot) is a synthetic composition with no in-repo instance. Its extra call runs after the bind and stops at backfillDecided.

    Gate proof: the dev reports, with the commands in the PR body, that PR #22325's widened script goes red on 28bff18d0c, is green on this head with 0 ledgered entries, and goes red again under the ablation. The seat confirmed the red half on its own: PR #22325's Lint & Repo Gates at 96d46ea02d (job 113445078033) fails with only that gate, on the same [app:seeded-hook-from-start] signature at auth-plugin.ts:1591. The green half becomes the seat's own reading when PR #22325 re-runs on a main that carries this PR.

    CI, read by the seat at 9c8ed2b62e: all 34 check runs are complete, 31 of them success, including Lint & Repo Gates and TypeScript Type Check. The 3 skips are all on the roster (check-expected-skips --pr 22333: OK).

    Deviations accepted: two scheduling non-identities, P3 and P4. Both come from appending at ready time, and neither drops an emit. On os serve, PlatformObjectsPlugin is composed before AuthPlugin, so no subscriber sits after the auth slot. Cloud-held compositions are NOT MEASURED, as the PR body says. check:dual-build-cjs-loads was NOT MEASURED locally. CI runs it inside Build Core, which is success on this head.

    Landing: ready + auto-merge now. PR #22325 then merges main and carries Fixes #22316.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:devxpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions