Skip to content

feat(auth)!: a managed environment adds pending, SSO-only people, and ban works without the admin plugin - #33227

Open
objectstack-fleet[bot] wants to merge 9 commits into
mainfrom
claude/project-thread-cl505i
Open

objectstack-fleet[bot] wants to merge 9 commits into
mainfrom
claude/project-thread-cl505i

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

Requested by Tesla · project thread

Fixes #33181

Clause-②: yes (narrowing) — narrowed: ban works without the admin plugin — admin/ban-user now works without the admin plugin, and a banned row is refused a session, with the admin plugin's own 403 BANNED_USER answer; both ban doors answered 501 NOT_IMPLEMENTED there. Widened: two doors, POST /admin/add-sso-user and POST /admin/import-sso-users (cloud-managed environments only; 409 RESOURCE_CONFLICT elsewhere); the idp_pending value of sys_user.source, which every password door and every session refuses; the auth plugin's optional environmentUserHost option; AuthManager#isAdminPluginMounted(); the exports of pending-sso-marker, environment-user-host and pending-sso-users; and, re-exported from the package root, the helpers bindUserToSoleOrganization, writeAdminAudit, joinCurrentOrganization and multiOrgCreationJoinsCurrentOrganization, the class CreationRefused, the type AdminAuditDecisions (with its user.admin_added_pending event), SetInitialPasswordOptions (the optional third argument of runSetInitialPassword) and the optional getBanWriter, host, getDataEngine and logger members of AdminBanEndpointDeps. The cloud-managed refusals of create-user, import-users and the invitation doors point to Setup → Users.
Metadata-protocol: none
Skills-docs: none — no skill under skills/** teaches the admin user doors, the ban doors or the cloud-managed refusals; content/docs/permissions/authentication.mdx gains the two doors, the pending state, the host option and the ban change in this change.

Owes a contract review. This draft owes a contract review at CONTRACT_REVIEW_TIER (narrowing arm), run in an isolated sub-agent and adopted by the seat, and is not landed before it.

Wording is final. The four cloud-managed 409 sentences (invitation, invitation link, create-user, import-users), kept in one block in cloud-managed.ts, are final per the maintainer's decision 6878953650 on the card.

维护者速读

  • 改了什么:托管环境的管理员可以在「设置 → 用户」通过两个新接口直接加人或导入 CSV,同时带上岗位、权限集和上级;这些人以「待激活」(idp_pending)状态存在,没有密码,在通过云端登录绑定之前,任何设密码、重置密码和其他登录方式都会被拒绝。
  • 为什么改:卡片 plugin-auth: pending, SSO-only users on a managed environment — add and CSV import with position, permission sets and manager, refused by every password door, with a host seam on create, deactivate and reactivate #33181(cloud#4465 子项 1a):种子数据和审批流需要在员工第一次登录前就能指向他们;Free/Team 托管环境没有 admin 插件,原来既不能加人也不能停用人。
  • 风险与回滚:patch 级;对未装 admin 插件的部署是破坏性收紧(BREAKING):停用/恢复(ban/unban)在那里开始生效,被停用的账号拿不到会话;装了 admin 插件的部署行为不变。其余都是新增;回滚就是还原本 PR。
  • 先看:packages/plugins/plugin-auth/src/pending-sso-users.ts(两个入口)、user-session-admission.ts(会话拒绝)、admin-ban-endpoints.ts 与 user-ban-write.ts(无插件时的停用)。
  • 两分钟验证:pnpm --filter @objectstack/plugin-auth exec vitest run src/pending-sso-users.test.ts src/pending-sso-seams.test.ts,预期全部通过。
  • 需要你做的:四句托管 409 文案已按你的决定(6878953650)定稿;合约评审 6879138774 为 PASS,修复后的 delta 复审进行中;改动超过 3000 行人工合并线,复审通过后需你批准。

What changes

Adding a pending person (managed only). Two platform-admin-gated doors, both mounted by the plugin itself and needing no admin plugin:

Door Takes Answers
POST /admin/add-sso-user email, name, optional positions, permissionSets, manager (the manager's email); a closed key set 201; 200 with alreadyPending for a person still pending (the host is told again); 409 for an address already held by someone not pending
POST /admin/import-sso-users { csv } (columns email, name, positions, permission_sets, manager_id, lists ;-separated) or { rows }, at most 500 200 with summary and one result per row; a bad row fails alone, managers resolve in a second pass

Off a managed environment both answer 409 RESOURCE_CONFLICT. The row is written by one engine insert as the system, attributed to the administrator (the AI service-account door's pattern), so it is born idp_pending: no sys_account, email_verified: false, and no moment where it reads env_native. Positions and permission sets are inserted under the caller's own execution context (create-user's grant seam, one unit of work), membership comes from create-user's organization bind, and the manager link is admin/set-user-manager's. An audit row user.admin_added_pending is written after the commit.

What a pending row is refused. admin/set-user-password, set-initial-password (judged after the body is read, through an optional beforeWrite on runSetInitialPassword), /reset-password and /phone-number/reset-password answer 409 RESOURCE_CONFLICT with their own sentence (pending-sso-marker.ts), in every mode. Session creation answers 403 PERMISSION_DENIED (user-session-admission.ts, composed into session.create.before after the AI service-account refusal, and reading the user row in that refusal's one read). The first sign-in through the platform identity provider binds to the row (implicit-linking rule 2) and stampIdentitySource moves it to idp_provisioned in account.create.after, before that sign-in's session; an enterprise provider does not bind (rule 1), and its link does not clear the marker either, since the stamp moves a pending row only on the platform provider's link.

Ban works without the admin plugin. admin/ban-user and admin/unban-user no longer answer 501 there: the ban columns are written through the data engine (engineUserBanWriter, the vendor-shaped write mapped to banned / ban_reason / ban_expires / updated_at; the last-administrator guard judges engine writes in every context), sessions are revoked as before, and session.create.before refuses a banned row with the vendor's own 403 BANNED_USER code and sentence, lifting an expired ban at sign-in as the vendor does. It runs only when the admin plugin is not mounted (AuthManager#isAdminPluginMounted()), so a kernel with the plugin answers exactly as before.

The host seam. environmentUserHost (auth plugin option, read per call) is told userAdded after the person's unit of work commits, and asked userDeactivated / userReactivated before the ban or unban write. It is never called inside a transaction (execution terms, comment 6870710213). A member that throws EnvironmentUserHostRefusal (409, an organization owner or admin; 403, for example DEVELOPER_ORGANIZATION_MEMBER_LIMIT_REACHED) refuses the operation with its own status, code and sentence. A refused deactivation or reactivation writes nothing. A refused add takes that person back out (grants, membership, row) and fails alone on its import row. A refused re-add of a still-pending person leaves that row as it was. Any other throw from a wired host refuses a deactivation (503 SERVICE_UNAVAILABLE, nothing written), since only the host can say whether the person is an organization owner or admin (ruling 5). It never undoes or blocks an add or a reactivation: the answer carries host: { notified: false, reason: 'host_failed' | 'no_host' } and the failure is logged at warn. On the ban doors host appears only when a host is wired. Every event carries actorCloudUserId, the account_id of the administrator's linked objectstack-cloud account, read from sys_account and never from the request (ruling 3), absent where there is no such link; on an impersonation session it also carries impersonatedBy, the administrator at the keyboard, read from the session. A session minted by the host's own sign-in handoff carries no record of its by in the environment; attributing those stays with ruling 9's work.

Re-adding a pending person re-tells the host and changes nothing else: positions, permission sets or a manager sent with the re-add are named on the answer (and on the import row) as notApplied. Only the platform provider's link clears the pending marker; a pending row that already holds its platform account but missed its stamp is stamped at its next session. An import row that faults answers the generic sentence, as the single-add door does.

Elsewhere. sys_user.source gains the option idp_pending ("Pending SSO Sign-In") with its description; the eleven translation bundles are regenerated with check-i18n-bundles.mjs --write. The cloud-managed 409 sentences point to Setup → Users (final, above). Route ledger rows for the two doors; the tenant audit census regenerated for the new write sites; authentication.mdx documents it all.

Unchanged: every env_native and idp_provisioned row at every door; create-user and import-users off a managed environment; the vendor's ban enforcement where the admin plugin is mounted.

Breaking

BREAKING for a deployment without the better-auth admin plugin (auth.plugins.admin off and SCIM off): admin/ban-user now works without the admin plugin, and a banned row is refused a session, with the admin plugin's own 403 BANNED_USER code and sentence; admin/unban-user lifts the ban there too. Level stays patch. ADR-0087 disposition: not-required (no-migration-prescription), in the changeset.

Why

Card #33181, child 1a of objectstack-ai/cloud#4465: a managed environment needs people to exist, with their positions, permission sets and manager, before their first sign-in, so seeds and approval flows can name them; on Free and Team hosted kernels (no admin plugin) there was no add door and no working deactivation. Seat rulings on the card: claim amendments 6869729952 and 6870557312.

Verification

Delta at 032dcc50 (the contract review record 6879138774 on f5a084c7, its should-fixes 1 to 7 and nit 10): pins added in pending-sso-users.test.ts (fail-closed deactivation and its CONTROLs, notApplied on both doors, the generic import fault, impersonatedBy on ban and add) and implicit-account-linking.test.ts (only the platform link clears the marker, with its CONTROL; a missed stamp healed at the next session, with its CONTROL). Ablations, each red: C1 the stamp's provider scope (1 failed), C2 the heal (1), C3 the heal's provider filter (2), C4 fail-closed deactivation (2), C5 notApplied on add (1), C6 on import (1), C7 the generic import sentence (1), C8 impersonatedBy (2). The tenant audit census was regenerated for the new write site (320 sites) and check-tenant-audit-census passes. Typecheck exits 0, pnpm lint exits 0, and check-issue-citations, check-comment-mask-adoption, check-adr-0087-registration, check-empty-changeset, check-i18n-bundles, check:cross-package-test-inputs and check:test-source-alias all exit 0. The plugin-auth full suite passes after a seam-test fake was taught to answer per object (271 files). The check-runs on 032dcc50 are the gate verdicts.

Readings below were taken at be43f4e1 (one commit on origin/main 7257f57a). Head f5a084c7 merges origin/main e540baf8. Only the generated tenant audit census conflicted: it was regenerated with tenant-audit-census.mjs --write, its prose figures were updated to the new count (319 sites), and check-tenant-audit-census passes. After the merge and a rebuild of the packages: plugin-auth typecheck exits 0, check-i18n-bundles exits 0, and the plugin-auth full suite passes (271 files, 5177 passed, 10 skipped). The check-runs on f5a084c7 are the gate verdicts for the merged tree.

Measured at main first

At origin/main 88355328, managed (OS_CLOUD_MANAGED=1) with the admin plugin off, as on a Free or Team hosted kernel: admin/create-user and admin/import-users answer the managed 409, and admin/ban-user / admin/unban-user answer 501 NOT_IMPLEMENTED, so no door adds or deactivates a person there. admin/set-user-password writes a credential on a credential-less env_native row, which is why the pending marker is its own value. The platform provider (objectstack-cloud) binds to an unverified credential-less row and stampIdentitySource restamps it idp_provisioned, while an enterprise provider is refused (rule 1). The new test files fail against main's wiring.

Ablations (each leg mutated through `scripts/ablation-replace.mjs`, red recorded, restored to HEAD)
  • A1 set-user-password pending arm: 2 failed. A2 set-initial-password arm: 2 failed. A3 reset-door arm: 2 failed.
  • A4 session refusal of a pending row: 3 failed. A5 session refusal of a banned row: 7 failed. A6 enforceBan wiring (admin plugin mounted or not): 4 failed. A8 a user-row read fault answers 503: 1 failed. A16 the one session read carries the admission columns: 6 failed.
  • A7 ban writer selection: 2 failed. A13 the engine ban writer refuses an unknown field: 1 failed.
  • A9 off a managed environment: 1 failed. A10 an address held by a non-pending row: 2 failed. A11 an unknown add key: 1 failed. A12 an unknown import column: 1 failed. A15 the marker on the insert itself: 7 failed.
  • A14 the host told on ban: 3 failed.
  • B1 a host refusal stops the ban: 1 failed. B2 stops the unban: 1 failed. B3 only 403 / 409 refusals are honoured: 1 failed. B4 a typed refusal is recognised at all: 5 failed. B5 a refused add takes the person back out: 2 failed. B6 a refused re-add fails the request: 1 failed. B7 the cloud id is read only from the objectstack-cloud account: 3 failed. B8 the ban event carries the cloud id: 2 failed. B9 the take-back removes grants and membership: 1 failed.
  • The dogfood sweeps' classification of the two new doors: both sweeps failed by name before the entries were added (POST /api/v1/auth/admin/add-sso-user, POST /api/v1/auth/admin/import-sso-users), and pass after.
Gates and suites
  • dispatch-gates --commands derived 108 families. All 108 ran with </dev/null, with exit codes recorded first, and --ran reconciles: ✓ 108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED (a DERIVED zero — all 108 recorded an exit code and none of them is 3). After the dogfood-table change, the gates that read test sources were re-run: check-issue-citations, check-comment-mask-adoption, check:cross-package-test-inputs and check:test-source-alias, all exit 0.
  • pnpm lint: exit 0. @objectstack/plugin-auth and @objectstack/platform-objects built with declarations: exit 0.
  • @objectstack/plugin-auth full suite: 270 files, 5169 passed, 10 skipped. @objectstack/platform-objects: 122 files, 1958 passed.
  • @objectstack/dogfood: the full run had two failures, the admin-route sweeps naming the two new doors. Both are now classified (ADR-0068 gate first; the admin gets 409 RESOURCE_CONFLICT off a managed environment), and the two sweep files re-run green (15 passed). The rest of the run was green: 461 files passed.

Acceptance notes

  • The cloud-managed 409 sentences are final per the maintainer's decision 6878953650 on the card; a later rewording is its own card.
  • An unreadable source at the three password-door pending arms falls through to the existing arms (the pending arm does not narrow main on a read fault). Session admission refuses 503 on a read fault, as the AI service-account refusal composed before it does.
  • Managed, a pending caller without platform-admin standing meets the cloud-managed set-initial-password refusal first; a pending caller with that standing meets the pending refusal.
  • Follow-up plugin-auth: the environment-user host seam cannot cancel a pending person's invitation, and has no durable retry or per-row invitation status #33228: a pending-only remove door that cancels the invitation, and where the durable outbox with retry and per-row invitation status lives (host-owned or a framework object). Both are execution terms in comment 6870710213 that this PR does not cover.
  • Follow-ups for Managed environments: re-show the add-people actions in Setup → Users (reverse #25295 / #25309's visible predicates for adding) #33186, not in this PR: Setup → Users has no console entry for the two doors yet, and the sys_user ban_user / unban_user actions keep requiresFeature: 'admin', so the console hides them on Free and Team kernels although the doors now answer there.
  • The non-English bundles carry the English option label the generator writes until translated.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched; the translation bundles were regenerated by their generator.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 section is the frame's Chinese summary.)
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh

… ban works without the admin plugin

On a cloud-managed environment a platform administrator adds a person
ahead of their first sign-in (admin/add-sso-user) or many from CSV
(admin/import-sso-users), with positions, permission sets and a manager.
The row is born idp_pending: no password, refused by every password door
and by every session until the first sign-in through the platform
identity provider binds to it.

admin/ban-user and admin/unban-user now work without the admin plugin:
the ban columns are written through the data engine and a banned row is
refused a session with the admin plugin's own BANNED_USER answer.

An optional environment-user host is told when a person is added, and
asked before one is deactivated or reactivated; it may refuse with a
typed 409 or 403. Events carry the administrator's cloud user id from
their linked platform account.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh
…-cl505i

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

Contract-review-claim: session_01L9qibvhGW6DCBPjrcAPjvh · head f5a084c · 2026-11-14T14:40Z

The review runs at CONTRACT_REVIEW_TIER (narrowing face) in an isolated sub-agent of this card thread; the record and its brief follow on this PR, for the seat to adopt.

@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, touching 200 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

89 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d683e6f3af5633a5c4a13ead523e06fbeb36db75.

⛔ 18 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-auth/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: userId (5 routes)
  • 3 anchor(s) matched too much of the corpus to be a work list: sys_user (symbol, 54 pages), PERMISSION_DENIED (literal, 53 pages), sys_user (literal, 54 pages)
  • 22 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d683e6f3af5633a5c4a13ead523e06fbeb36db75 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eba4238f03a7e2f79bae14c552c5306dfc1ea517 — the merge of head b8af0027e6bc974331c5e295d2c180ad4b5f7034 into base d683e6f3af5633a5c4a13ead523e06fbeb36db75, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin eba4238f03a7e2f79bae14c552c5306dfc1ea517 && git checkout eba4238f03a7e2f79bae14c552c5306dfc1ea517
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d683e6f3af5633a5c4a13ead523e06fbeb36db75 b8af0027e6bc974331c5e295d2c180ad4b5f7034 && git checkout -B drift-repro d683e6f3af5633a5c4a13ead523e06fbeb36db75 && git merge --no-ff b8af0027e6bc974331c5e295d2c180ad4b5f7034

node scripts/docs-audit/affected-docs.mjs --json d683e6f3af5633a5c4a13ead523e06fbeb36db75

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d683e6f3af5633a5c4a13ead523e06fbeb36db75 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

The brief the isolated reviewer was given for the record on head f5a084c7, from record-recognisers.mjs --brief-template, verbatim:

Contract review brief — PR #33227 @ f5a084c771de8823e3fd0a872640441e0518a57f (card #33181)

You review at CONTRACT_REVIEW_TIER. Render ONE ## Contract review record on this head — its shape is
node scripts/pm/record-recognisers.mjs --template — with a PASS or FAIL verdict, and post it as ONE comment
on the PR or its card.

Inputs — these and nothing else:
· card #33181: its body and EVERY comment (rulings and earlier records included)
· PR #33227: its body, its file list, and the net diff against main at f5a084c771de8823e3fd0a872640441e0518a57f
· the check-runs on f5a084c771de8823e3fd0a872640441e0518a57f: their conclusions ARE the gate verdicts
⛔ Not the dispatch order, ⛔ not the dispatching seat's own conclusions — this brief is adversarial by design.

Scope — the paths reviewFace returns as hits on this PR's file list: packages/platform-objects/src/identity/sys-user-identity-source-label.test.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/plugins/plugin-auth/src/admin-ban-endpoints.ts, packages/plugins/plugin-auth/src/admin-user-endpoints.ts, packages/plugins/plugin-auth/src/auth-manager-before-hook.ts, packages/plugins/plugin-auth/src/auth-manager-database-hooks.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts, packages/plugins/plugin-auth/src/auth-route-ledger.ts, packages/plugins/plugin-auth/src/cloud-managed.ts, packages/plugins/plugin-auth/src/environment-user-host.ts, packages/plugins/plugin-auth/src/implicit-account-linking.test.ts, packages/plugins/plugin-auth/src/implicit-account-linking.ts, packages/plugins/plugin-auth/src/index.ts, packages/plugins/plugin-auth/src/password-reset-ends-sessions.test.ts, packages/plugins/plugin-auth/src/pending-sso-marker.ts, packages/plugins/plugin-auth/src/pending-sso-seams.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.ts, packages/plugins/plugin-auth/src/service-account-identity.ts, packages/plugins/plugin-auth/src/set-initial-password.ts, packages/plugins/plugin-auth/src/user-ban-write.ts, packages/plugins/plugin-auth/src/user-session-admission.ts, packages/qa/dogfood/test/admin-platform-admin-standing.dogfood.test.ts, packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts;
plus every file IN THIS DIFF that directly implements or enforces them — the gate that enforces a rule, the code
a jsdoc or schema line describes, the validator a skill teaches — reviewed for agreement with the contract.
Widen it freely, ⛔ never below hits; the record's Scope: line names the implementing files apart from hits.
The rest of the diff (tests, unrelated modules, lockfiles, generated output, changesets) is the lane seat's
ACCEPT at TIER_DEFAULT, exactly as a standalone PR of those files.
④ judges the hits files and those implementing files ONLY:
lockfiles, generated output, changesets and prose elsewhere in the PR are not reviewed;
code outside the diff is READ to judge them, never reviewed.

⛔ READ-ONLY. No worktree, no checkout, no build, no test run, no gate re-run, no ablation: the check-runs
on the head already answered every derived gate family, and a local run costs more than it returns.
If ONE probe is unavoidable, the record's Local-runs: line says probe — REASON; otherwise none.
Scratch space is /tmp/claude-0/-home-user/07cb9531-6242-5ae5-ac54-8e9b6b690f1d/scratchpad/pr-33227/ alone; nothing there you did not write this round is read.

Judge, in the record's own sections:
① Derived judgments — every accept-set and public-surface change the diff implies, each named right or wrong.
② Semver level — the changeset (or skip-changeset) matches what the diff publishes, and the Clause-②: line.
③ Boundary flags — every dev flag and every open_questions entry answered, or escalated.
④ Design judgment — for EVERY hits file: does the change hold against the code it cites and the
scenario it is accepted against? A skill file against the card's eval prompt and the spec it teaches
on main; a schema file against the ruling or ADR it implements and its first consumer; an ADR
against its first consumer and its execution plan. For EVERY implementing file in the diff: does the
code agree with the contract, sentence by sentence? Read the cited code in the live tree, and try to
break the design.
Grade each finding must-fix, should-fix or nit — a nit has no effect on correctness or the contract.
A must-fix is a FAIL; a must-fix or should-fix earns a delta review of its fix; nits alone end the round.
A nit is fixed only on the prose faces, or left as an accepted deviation or a follow-up card: a fix
anywhere else moves the head, and this record no longer covers it.
⛔ Judge the tree, never the PR body's account of it.
Sign Implemented-by: with the dev's branch and Reviewed-by: with your session id — the value is the FIRST
thing after each colon.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f5a084c771de8823e3fd0a872640441e0518a57f
Local-runs: none
Scope: packages/platform-objects/src/identity/sys-user-identity-source-label.test.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/plugins/plugin-auth/src/admin-ban-endpoints.ts, packages/plugins/plugin-auth/src/admin-user-endpoints.ts, packages/plugins/plugin-auth/src/auth-manager-before-hook.ts, packages/plugins/plugin-auth/src/auth-manager-database-hooks.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts, packages/plugins/plugin-auth/src/auth-route-ledger.ts, packages/plugins/plugin-auth/src/cloud-managed.ts, packages/plugins/plugin-auth/src/environment-user-host.ts, packages/plugins/plugin-auth/src/implicit-account-linking.test.ts, packages/plugins/plugin-auth/src/implicit-account-linking.ts, packages/plugins/plugin-auth/src/index.ts, packages/plugins/plugin-auth/src/password-reset-ends-sessions.test.ts, packages/plugins/plugin-auth/src/pending-sso-marker.ts, packages/plugins/plugin-auth/src/pending-sso-seams.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.ts, packages/plugins/plugin-auth/src/service-account-identity.ts, packages/plugins/plugin-auth/src/set-initial-password.ts, packages/plugins/plugin-auth/src/user-ban-write.ts, packages/plugins/plugin-auth/src/user-session-admission.ts, packages/qa/dogfood/test/admin-platform-admin-standing.dogfood.test.ts, packages/qa/dogfood/test/admin-route-nonadmin-refusal.dogfood.test.ts + .changeset/33181-pending-sso-users.md, content/docs/permissions/authentication.mdx

Inputs: card #33181 body and all 8 comments, PR #33227 body and its 51-file list, the net diff against main at this head, and the head's check-runs. Cited code outside the diff was read, not reviewed: sso-only-local-password.ts, last-admin-guard.ts, stampIdentitySource in auth-manager-database-hooks.ts, packages/types/src/thrown-http-error.ts, and better-auth 1.7.12's email-verification, magic-link and email-otp routes.

Check-runs when read (54 total):

  • success: Check Changeset (both runs), PR-body guards (both runs), Governed Surface Queue Guard, Spec property liveness, Flag docs affected by code changes, Check Documentation Links, Check PR Size, Auto Label, filter, Plan Test Core shards, Dogfood Regression Gate, Test UI, Build Docs, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Lint · scoped · query-options erasure, Type Check · source gates, Type Check · debt ledger.
  • in progress: Test Core 1/8 to 8/8, Build Core, Live E2E UI, Type Check · workspace, Type Check · consumer gates, Lint · scoped · slot lookup & corpus.
  • failed: none. The rest are skipped.
  • This verdict is a design verdict. The in-progress runs remain the gate verdicts, and the PR does not land before they conclude.

① Derived judgments

  • Narrowing, declared, right: admin/ban-user now works without the admin plugin, and a banned row is refused a session. admin/unban-user lifts it there too. The admission runs only when isAdminPluginMounted() is false, so a kernel with the plugin is untouched.
  • Widening, declared, right:
    • POST /admin/add-sso-user and POST /admin/import-sso-users, platform-admin gated first, 409 RESOURCE_CONFLICT off a managed environment.
    • The idp_pending option of sys_user.source.
    • The environmentUserHost option and AuthManager#isAdminPluginMounted().
    • The exports of pending-sso-marker, environment-user-host and pending-sso-users.
    • The audit event user.admin_added_pending, two route-ledger rows and two dogfood sweep entries.
  • Widening, not declared, wrong (should-fix 1): index.ts re-exports admin-user-endpoints.js, set-initial-password.js and admin-ban-endpoints.js with export *. The diff therefore publishes bindUserToSoleOrganization, writeAdminAudit, CreationRefused, joinCurrentOrganization, multiOrgCreationJoinsCurrentOrganization, AdminAuditDecisions, SetInitialPasswordOptions with the third parameter of runSetInitialPassword, and three new optional members of AdminBanEndpointDeps. The Clause-②: line names none of them.
  • New 409 for a pending row at set-user-password, set-initial-password, /reset-password and /phone-number/reset-password: right, and it narrows nothing, since no row carries the marker on main. The four doors are the same set sso-only-local-password.ts names as every door that writes a hash without proving a current one.
  • resolveResetDoorUserId now runs on both reset doors in every mode, where it ran only in SSO-only mode: right. The read spends nothing, swallows its own faults, and a { ok: false } answer is ignored outside SSO-only mode.
  • readSessionUserRow replacing refuseAgentAccountSession's body: right. Same guards, same read fault, same refusal; the old entry point is kept.
  • The four cloud-managed 409 sentences: right, final per decision 6878953650.

② Semver level

Clause-②: yes (narrowing) — matches the tree. One arm from the closed pair, with the BREAKING banner in both the PR body and the changeset.

  • patch on @objectstack/plugin-auth and @objectstack/platform-objects is the level AGENTS.md gives a non-protocol change, breaking or not.
  • Metadata-protocol: none is right: sys_user.source is a system-managed column of a platform object, not something a metadata author writes.
  • The ADR-0087 disposition not-required (no-migration-prescription) is present, and nothing stored needs rewriting.
  • Both Check Changeset runs concluded success.
  • The declaration line is incomplete on the widened side (should-fix 1).

③ Boundary flags

  • Open question, the provisional 409 wording: answered by decision 6878953650.
  • Cancel-on-delete door, durable outbox with retry, per-row invitation status (execution terms, 6870710213): not in this PR, deferred to plugin-auth: the environment-user host seam cannot cancel a pending person's invitation, and has no durable retry or per-row invitation status #33228 as the release comment 6872883193 states. Accepted as a named follow-up. Until it lands, a host failure has no retry other than the administrator repeating the call.
  • Console entry and requiresFeature: 'admin' on the sys_user ban actions: deferred to Managed environments: re-show the add-people actions in Setup → Users (reverse #25295 / #25309's visible predicates for adding) #33186. Accepted.
  • "Adding an email that already has an environment row: decide in child 1a": decided. A pending row answers 200 alreadyPending and re-tells the host; any other row answers 409. See should-fix 4 for what the re-add drops.
  • Escalated to the maintainer (should-fix 2): the term "An owner or admin 409 refuses the whole deactivation" holds only while the host answers. A host that throws anything but a typed refusal lets the ban go ahead. That is the seat's choice in 6869729952, made before the rulings were quoted on the card.
  • Escalated (should-fix 5): the term "attributed through the handoff's by" has no carrier in the event. actorCloudUserId is always read from the session user's own linked account.
  • "Console-side removal: the roster shows that state" is not this child's surface; no finding.

④ Design judgment

A row of the new files per contract sentence, then the findings.

  • pending-sso-marker.ts, sys-user.object.ts and its label test: hold. The marker is its own value, never env_native, as the execution terms require.
  • pending-sso-users.ts: holds for the insert, the grants, the host call and the take-back.
    • The row is born pending in one engine insert, so there is no env_native moment.
    • Grants run under the caller's own context (ruling 2).
    • The host is called outside the unit of work (no transaction across the invite call).
    • A typed refusal takes the person back out, children first.
    • Findings 3, 4, 6 and 7 are here.
  • user-session-admission.ts, auth-manager-database-hooks.ts, service-account-identity.ts: hold. One read per session, 503 on a read fault. Findings 3 and 8.
  • admin-ban-endpoints.ts, user-ban-write.ts, auth-manager.ts, auth-plugin.ts: hold for writer selection, the closed field map and the self-ban refusal. The host is asked before the write. Findings 2, 5, 9 and 10.
  • auth-manager-before-hook.ts, admin-user-endpoints.ts, set-initial-password.ts: the pending arms hold. A source that cannot be read falls through to the existing arms. The PR declares this, and it departs from the fail-closed convention of sso-only-local-password.ts. Accepted as a declared deviation; its one consequence is in finding 3.
  • implicit-account-linking.ts (header only), cloud-managed.ts, environment-user-host.ts, index.ts, the ledger and its conformance list, the two dogfood tables, the two appended test files: agree with the code. environment-user-host.ts carries findings 5 and 10.

Findings:

  1. should-fix — undeclared public exports (see ①), index.ts:86-88 and admin-user-endpoints.ts:478, 525, 607, 953, 980, 1023. Name them on the Clause-②: line in the PR body and the changeset, or stop publishing the helpers.
  2. should-fix, escalated — environment-user-host.ts:175, 192 with admin-ban-endpoints.ts:265. A host that is unreachable cannot say whether the person is an organization owner or admin, and the deactivation proceeds. Ruling 5 says such a person "cannot be removed from the environment side". Either fail closed on deactivated when a host is wired and does not answer, or record the maintainer's word that fail-open stands.
  3. should-fix — the pending marker clears only through stampIdentitySource, which is fail-soft and runs once.
    • It runs only in account.create.after, catches every error, and logs that "the sign-in is fine".
    • If that one write fails, the account link exists, the row still reads idp_pending, and user-session-admission.ts:105 refuses the session.
    • Later sign-ins create no account, so the stamp never re-runs.
    • The stamp also skips a row that holds a credential account, which the declared read-fault fall-through can produce.
    • No door in this PR clears the marker, so the person is locked out.
    • Fix: let admission admit and stamp a pending row that already holds an objectstack-cloud account, or make a failed stamp on a pending row fail the link.
  4. should-fix — pending-sso-users.ts:343 and the second pass at :633. Re-adding a pending person answers ok with alreadyPending and drops the positions, permission sets and manager sent with it. Neither the answer nor authentication.mdx says so, so a corrected CSV reads as applied. Say what was not applied, on the row and in the docs, or apply it.
  5. should-fix — admin-ban-endpoints.ts:167 and pending-sso-users.ts:426. Under an impersonation session actor.id is the impersonated administrator, and the event carries their cloud id; actor.impersonatedBy is dropped. Ruling 3 attributes each invitation to the acting administrator. Carry impersonatedBy on the event; the handoff's by needs the same slot.
  6. should-fix — pending-sso-users.ts:618. The import's per-row catch answers the raw thrown message of any fault, driver text included. The single-add door answers the generic INTERNAL_ERROR_MESSAGE for the same fault. Use the generic sentence for an undeclared 5xx.
  7. One should-fix was reported privately.
  8. nit — user-session-admission.ts: the expired-ban lift writes no updated_at, and a failed lift surfaces as an unshaped error.
  9. nit — admin-ban-endpoints.ts:265: the host is asked before the write. If the write is then refused (the engine's last-administrator guard) or faults, the host has acted and the door answers 500 with no host field. Repeating the call recovers it. This belongs with the outbox on plugin-auth: the environment-user host seam cannot cancel a pending person's invitation, and has no durable retry or per-row invitation status #33228.
  10. nit — three comments disagree with the code:
    • auth-plugin.ts:385-387: the environmentUserHost jsdoc says the host is told "after the operation has committed" and "never undoes the operation". The ban and unban doors ask before the write, and a typed refusal does undo an add.
    • environment-user-host.ts:46: occurredAt is described as the time the operation committed.
    • auth-plugin.ts:4059: "a session that predates the marker" describes a state a born-pending row cannot have.
  11. nit — a re-add that re-tells the host writes no audit row. manager_id names a column that holds an email address.

No must-fix. Findings 1 to 7 each earn a delta review of their fix.

This record was rendered by an isolated sub-agent at CONTRACT_REVIEW_TIER, fed only the brief's inputs.

Implemented-by: claude/project-thread-cl505i
Reviewed-by: session_01L9qibvhGW6DCBPjrcAPjvh

VERDICT: PASS

…ivation waits for the host, re-adds name what they skip

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

Contract-review-claim: session_01L9qibvhGW6DCBPjrcAPjvh · head 032dcc5 · 2026-11-14T15:09Z

The delta review of record 6879138774 should-fixes 1 to 7, by the same isolated reviewer at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 032dcc50d16098c43dcbf446e54e98095f96cc37
Local-runs: none
Scope: packages/plugins/plugin-auth/src/admin-ban-endpoints.ts, packages/plugins/plugin-auth/src/auth-manager-database-hooks.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/environment-user-host.ts, packages/plugins/plugin-auth/src/implicit-account-linking.test.ts, packages/plugins/plugin-auth/src/pending-sso-seams.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.ts, packages/plugins/plugin-auth/src/user-session-admission.ts + .changeset/33181-pending-sso-users.md, content/docs/permissions/authentication.mdx

This is the delta review of record 6879138774 (head f5a084c7), rendered by the same isolated sub-agent at CONTRACT_REVIEW_TIER, fed only the brief's inputs.

Inputs: the delta f5a084c7..032dcc50 (4 commits, 13 files), the PR body as it stands on this head, the changeset, the seat rulings relayed with the delta order, and the head's check-runs. The regenerated tenant audit census and its counts file are generated output and were not reviewed. Cited code read, not reviewed: isPlatformIdpSource in implicit-account-linking.ts, auth-manager-auth-instance.ts, and packages/types/src/error-leak.ts through its index.

Check-runs when read (46 total):

  • success: Governed Surface Queue Guard, PR-body guards (the later run), Spec property liveness, Flag docs affected by code changes, Check Documentation Links, Check PR Size, Auto Label, filter, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests.
  • cancelled: PR-body guards (the earlier run, superseded by the later success).
  • in progress: Check Changeset (both runs), Plan Test Core shards, Build Core, Build Docs, Test UI, Dogfood Regression Gate, Live E2E UI, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Type Check · workspace, Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates.
  • failed: none. The rest are skipped.
  • This verdict is a design verdict. The in-progress runs remain the gate verdicts, and the PR does not land before they conclude.

① Derived judgments

  • Narrowing arm: unchanged by the delta and still right. admin/ban-user now works without the admin plugin, and a banned row is refused a session.
  • New refusal, right: with a host wired, a host failure that is not a typed refusal now answers 503 SERVICE_UNAVAILABLE on admin/ban-user and writes nothing. This is new on a door that is itself new in this PR for hosts, so it narrows nothing on main. The changeset, the docs and the PR body all state it.
  • Widening, declared, right:
    • impersonatedBy on EnvironmentUserEvent.
    • The exports EnvironmentUserActor and environmentUserAttribution. They fall under "the exports of environment-user-host" already on the line.
    • notApplied on the add answer and on ImportSsoUserRowResult.
  • Behaviour change, right: stampIdentitySource leaves idp_pending on a link from any provider but the platform one. It costs one extra sys_user read per non-platform federated link on every deployment, and changes nothing for a row that is not pending.
  • Behaviour change, right: session admission stamps and admits a pending row that already holds its platform account. One more sys_account read, on pending rows only.
  • Answer change, right: an import row that faults at 5xx answers the generic sentence; a declared refusal keeps its own.

② Semver level

Clause-②: yes (narrowing) — the line is byte-identical in the PR body and the changeset, and matches the tree.

  • The widened side now names every helper, class, type and optional member the package root publishes from this diff.
  • Level patch on both packages, Metadata-protocol: none, the BREAKING banner and the ADR-0087 disposition are unchanged and still right.
  • The changeset's "What to do" paragraph now carries an operator step to run before upgrading. It replaces "nothing" and is consistent with the narrowing arm.
  • Both Check Changeset runs were still in progress when read.

③ Boundary flags

④ Design judgment

Each finding of record 6879138774, against the delta:

  1. Resolved. The eight published names are on the Clause-②: line in both places.
  2. Resolved. admin-ban-endpoints.ts returns the 503 before applyUserBan when the outcome is host_failed.
    • Both failure sources produce host_failed: an accessor that throws, and a member that throws untyped.
    • A typed refusal with a status the seam does not honour also lands here.
    • Reactivation still goes ahead, with host_failed on the answer.
    • Pinned, with three CONTROL legs.
  3. Resolved. healPendingPlatformLink in user-session-admission.ts admits and stamps a pending row that holds an objectstack-cloud account.
    • This covers both a stamp that failed and a stamp skipped because a credential account exists.
    • A read or write fault refuses 503.
    • The ban check still runs after the heal, on the same row.
    • Pinned, with a CONTROL for a row holding only another provider's account.
  4. Resolved. A re-add names the fields or columns it did not apply as notApplied, on the add answer and on the import row. The docs and the changeset say the re-add changes nothing else.
  5. Resolved as ruled. environmentUserAttribution reads impersonatedBy from the gate's actor, never from the body. Both doors use it; pinned on ban and add.
  6. Resolved. pending-sso-users.ts answers INTERNAL_ERROR_MESSAGE for a row fault at 5xx. Pinned, with a CONTROL for a declared refusal.
  7. Resolved. The fix holds against the code it cites, and is pinned with its CONTROL.
  8. Resolved (nit). The expired-ban lift writes updated_at, and a failed lift refuses 503.
  9. Resolved (nit). The environmentUserHost jsdoc, the occurredAt jsdoc and the set-initial-password comment now agree with the code.

New in the delta:

  • nit A — admin-ban-endpoints.ts:284. The fail-closed arm keys on host_failed only. An accessor that is wired but answers no host (no_host) lets the deactivation go ahead, as an unwired option does. That matches "read per call, so a host wired late is still found". It does mean a managed host must return a host or throw; it must never answer undefined while it starts. This is a sentence for the host's contract in environment-user-host.ts, or for plugin-auth: the environment-user host seam cannot cancel a pending person's invitation, and has no durable retry or per-row invitation status #33228.
  • nit B — PR body, "What a pending row is refused": it still gives rule 1 as the only reason an enterprise provider does not bind. The stamp's provider scope is the second reason, and the body states it two paragraphs later. Prose only.
  • One nit was reported privately.

No must-fix and no should-fix. The nits end the round.

Implemented-by: claude/project-thread-cl505i
Reviewed-by: session_01L9qibvhGW6DCBPjrcAPjvh

VERDICT: PASS

…double

The sys_account matcher in the admission test double read `$and`/`$or` as
field names. It now refuses any operator key, per the where-matcher
conformance gate. The host accessor's contract comment also states that a
managed host answers a host or throws, never `undefined` while starting.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh
The import-fault test wrapped the engine's `insert` and passed a third
argument its declared signature does not take, which the test-layer
typecheck reports. The wrapper now forwards whatever it receives.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 84a7069f3fc371157db6d470bb0f6f49178a0cc4
Local-runs: none
Scope: packages/plugins/plugin-auth/src/environment-user-host.ts, packages/plugins/plugin-auth/src/pending-sso-seams.test.ts, packages/plugins/plugin-auth/src/pending-sso-users.test.ts

The seat's own read of the part of this head no record covers: 032dcc50..84a7069f, two commits, three files, +14 / -5. Everything else on this head is judged by record 6879138774 (f5a084c7, PASS) and its delta 6879497274 (032dcc50, PASS), both under brief 6879131946.

① Derived judgments

  • environment-user-host.ts: a jsdoc on EnvironmentUserHostAccessor only (delta nit A). It states what the code at admin-ban-endpoints.ts already does: an accessor that answers no host is treated as no host wired, and the deactivation goes ahead. No type, value or export moves. Right.
  • pending-sso-seams.test.ts: the sys_account test double now throws on any $ key in where instead of reading it as a field name (the WHERE-matcher conformance gate). A test double only. Right.
  • pending-sso-users.test.ts: the import-fault test's insert wrapper forwards rest arguments instead of a declared third parameter the signature lacks (TS2554 under check:test-typecheck). Test only; the assertion is unchanged. Right.

② Semver level

Unchanged: Clause-②: yes (narrowing), patch on both packages, Metadata-protocol: none. Nothing in this delta publishes.

③ Boundary flags

None new. Delta nit A is answered by the comment; nit B was a PR-body edit.

④ Design judgment

No design moves in this delta. The comment narrows nothing and the two test edits keep their assertions. Check-runs on this head: 38 success, 15 skipped, 0 failed.

Implemented-by: claude/project-thread-cl505i
Reviewed-by: session_01L9qibzuZJBQY4wUQFJbkfx

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

维护者速读 · PR #33227 @ 84a7069f · 2026-11-14T16:12Z

  • 改了什么:托管环境的管理员可以通过两个新接口(POST /admin/add-sso-user、POST /admin/import-sso-users)直接加人或导入 CSV,带上岗位、权限集和上级。这些人以「待激活」(sys_user.source = idp_pending)存在,没有密码,在通过平台登录绑定之前,设密码、重置密码和其他登录方式都会被拒绝。没有 admin 插件的部署上,停用/恢复(admin/ban-user / unban-user)开始生效,被停用的账号拿不到会话。
  • 为什么改:plugin-auth: pending, SSO-only users on a managed environment — add and CSV import with position, permission sets and manager, refused by every password door, with a host seam on create, deactivate and reactivate #33181(cloud#4465 子项 1a):种子数据和审批流需要在员工第一次登录前就能指向他们;Free/Team 托管环境没有 admin 插件,原来既不能加人也不能停用人。
  • 权限与边界:这是收窄(标题带 !):对没有 admin 插件的部署,停用开始生效;装了 admin 插件的部署行为不变。宿主接上时,若宿主不回答「此人是否组织所有者/管理员」,停用以 503 拒绝(按裁定 5,失败即关闭)。两个新接口只对平台管理员开放,非托管环境答 409。
  • 风险与回滚:升级前,没有 admin 插件的部署应先检查已被标记停用的行(尤其唯一的平台管理员)并先恢复,changeset 的 "What to do" 写了步骤。回滚即 revert;idp_pending 只出现在新接口建的行上。
  • 先看:packages/plugins/plugin-auth/src/pending-sso-users.ts(两个入口)、user-session-admission.ts(会话拒绝与自愈)、admin-ban-endpoints.ts 与 user-ban-write.ts(无插件时的停用)。
  • 需要你做的:改动 3392 行,超过 3000 行人工合并线,需要你(os-zhuang)在 GitHub 上批准,批准后席位按队列落地。现 head 84a7069f 的 PASS:席位记录 6880408199(覆盖 032dcc50..84a7069f),其余由 6879138774 与 6879497274 覆盖。
  • 席位意见:落地。Clause-② yes (narrowing),CONTRACT_REVIEW_TIER 复审 PASS(7 条应修),修复后 delta PASS;CI 无失败,完整 dogfood 通过,非受管面。

席位核对

  • 形态: draft → main,Fixes #33181,合并后卡关闭;正文其他地方没有挨着别的 open 卡号的闭合关键词。
    • 申报 Clause-②: yes (narrowing)(收窄面:无插件时停用生效;加宽面逐项列出,含 delta 后补上的八个公开名字)、Metadata-protocol: none、Skills-docs: none,changeset 对 @objectstack/plugin-auth 与 @objectstack/platform-objects 各 patch,带 BREAKING 横幅与 ADR-0087 标记。
    • 文件面 = 认领 6869596244 加修订 6869729952、6870557312;本席恢复认领 6880041500。
  • 前提实测: dev 报告 6872845977:在 origin/main 上先测了卡里的行号与 Free 托管内核上 admin/ban-user 的 501;宿主拒绝、动作者云 id 等 B1-B9 消融全部变红。修复轮每条应修都有钉(含 CONTROL)。plugin-auth 全量 5177 条通过(合并 main 后)。
  • 门禁 check-runs @ 84a7069f: 38 success、15 skipped、0 失败、0 进行中。
  • 完整 dogfood(标题带 !)@ e1637d82: 先重建 plugin-auth dist,os-verify-lock: VERDICT command-exit 0,测试文件 464 通过 / 3 跳过,测试 3927 通过 / 42 跳过。84a7069f 只在其上改了一个测试文件的类型(dogfood 不加载它),结论覆盖当前头。
  • 契约复审: CONTRACT_REVIEW_TIER(收窄面)。卡线程的隔离审读子代理按简报 6879131946 出记录,认领 6879011881;本席核对简报、范围与独立性后采纳。
    • 全量记录 6879138774 在 f5a084c7 上 PASS,7 条应修:1 公开导出未申报;2 宿主不回答时停用照样进行(席位改为失败即关闭,裁定 5);3 待激活标记只靠一次失败即放过的写清除;4 重复添加悄悄丢掉岗位等;5 代管会话下事件丢了 impersonatedBy;6 导入逐行把驱动原文回给调用方;7 一条私下报告的应修。
    • delta 记录 6879497274 在 032dcc50 上 PASS:1-7 全部解决,细节 8、10 解决。
    • 032dcc50..84a7069f 只改测试替身(WHERE 匹配器遇到 $ 运算符即抛出)、pending-sso-users.test.ts 的一处类型错误、environment-user-host.ts 的一句注释(delta 细节 A);席位自读记录 6880408199 在 84a7069f 上 PASS。
  • 接受的偏差:
    • PR 正文顶部仍写着「Owes a contract review」和维护者速读里「delta 复审进行中」,已过期,以本评论为准:两轮复审都已 PASS。
    • delta 细节 A(宿主访问器接上但返回空时停用照样进行):已在 environment-user-host.ts 的合同注释里写明宿主必须返回宿主或抛出。
    • 细节 11(重复添加再次通知宿主时不写审计行;manager_id 存的是邮箱)。
  • 范围外去向: 待激活行的删除与取消邀请、持久化发件箱与重试、逐行邀请状态 → plugin-auth: the environment-user host seam cannot cancel a pending person's invitation, and has no durable retry or per-row invitation status #33228(细节 9「宿主在写之前已行动」同去);控制台入口与 requiresFeature: 'admin' → Managed environments: re-show the add-people actions in Setup → Users (reverse #25295 / #25309's visible predicates for adding) #33186;交接的 by 字段 → 裁定 9 的工作。
  • 落地条款: 维护者常设落地令 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z),按 「车道座位应该使用pm-dispatch 技能审核和落地pr」 (2026-11-14T14:32Z)。
    • 非受管面:check-governed-merges --pr 33227 判定 0/51 命中。3392 行(+3279 / -113)> 3000:人工合并线(2026-09-18 裁定)。本席不转 ready、不入队、不开自动合并,直到 GOVERNED_APPROVERS 中的账号批准;同笔 PR 加 pm:awaiting-merge,并请 os-zhuang 与 hotlong 审查。
    • K = 6(PRBASE 199e3624,取自 Plan Test Core shards 的 Affected-set diff base;其后 main 中 fix(plugin-auth): a SCIM bearer write carrying a Cookie and no Origin is no longer refused 403 MISSING_OR_NULL_ORIGIN #33243 a2be78d0 也改 plugin-auth(SCIM 写的 Origin 检查),与本 PR 的文件不重叠)。git merge-tree 无冲突。
    • 没有新的语种包。

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 14, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review November 14, 2026 21:30
@os-zhuang
os-zhuang marked this pull request as ready for review November 14, 2026 21:30
@os-zhuang
os-zhuang enabled auto-merge November 14, 2026 21:30
Conflicts with the direct sign-in work, both sides kept: the
`environmentUserHost` and `directSignIn` plugin options, the pending-SSO
and direct sign-in exports, and both describe blocks in the implicit
account-linking suite.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvhGW6DCBPjrcAPjvh
@os-zhuang
os-zhuang disabled auto-merge November 14, 2026 21:46
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b8af0027e6bc974331c5e295d2c180ad4b5f7034
Local-runs: none
Scope: packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/implicit-account-linking.test.ts, packages/plugins/plugin-auth/src/index.ts, packages/platform-objects/src/apps/translations/ar.source-hashes.generated.ts

The seat's own read of the part of this head that no record covers: 84a7069f..b8af0027, one merge of main (19cc8976) with no rebase. The branch's own code is unchanged, and records 6879138774, 6879497274 and 6880408199 still judge it. This read covers only the combined diff (git show --cc b8af0027), which is where both sides touched the same lines.

① Derived judgments

  • auth-plugin.ts: the PR's environmentUserHost option and its jsdoc, followed by main's directSignIn option and its jsdoc (feat(plugin-auth): with single sign-on configured, a signed-out visit can go straight to the identity provider and back, with a configurable local sign-in path #33221). Both are kept word for word, and the only hand edit is a /** opener. Two independent optional fields. Right.
  • index.ts: the PR's three export * lines (pending-sso-marker, environment-user-host, pending-sso-users), then main's export type { DirectSignInOptions, DirectSignInLocalPath }. No name collides. Right.
  • implicit-account-linking.test.ts: the PR's pending-person describe is whole and closed, followed by main's direct sign-in describe, also whole. The only line in both sides is a blank line. Right.
  • ar.source-hashes.generated.ts: main's two tokenEndpoint entries and the PR's idp_pending entry, each from its own side. A union of generated keys; CI's generated check judges it.
  • auth-manager.ts, auth-route-ledger.ts and the ledger conformance test merged without conflict and have no lines in the combined diff.

② Semver level

Unchanged: Clause-②: yes (narrowing), Metadata-protocol: none. The merge publishes nothing of its own.

③ Boundary flags

None new. Main's direct sign-in door and this PR's pending-person doors are separate routes. The ledger auto-merged, and its conformance test covers both sides.

④ Design judgment

No design moves. Check-runs on this head: 38 success, 15 skipped, 0 failed, 0 in progress.

Implemented-by: claude/project-thread-cl505i
Reviewed-by: session_01L9qibzuZJBQY4wUQFJbkfx

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

Re-land · 2026-11-14T22:27Z · PR #33227 @ b8af0027

  • What moved since ACCEPT 6880418210 (84a7069f): one merge of main (19cc8976), made because feat(plugin-auth): with single sign-on configured, a signed-out visit can go straight to the identity provider and back, with a configurable local sign-in path #33221 conflicted in auth-plugin.ts, implicit-account-linking.test.ts and index.ts. The seat read the combined diff: both sides are kept and there is no hand edit beyond a jsdoc opener and a blank line. Record 6885176188, PASS.
  • Approval: os-zhuang review 5907242051 on 84a7069f stands. The maintainer confirmed it stays valid across this merge: 「33227 我的历史批准也是有效的你,你可以自主落地」 (2026-11-14T22:15Z).
  • Gates at b8af0027: 38 success, 15 skipped, 0 failed, 0 in progress.
  • K = 6 (PRBASE d683e6f3 from Plan Test Core shards). No new locale pack on main since then, no file shared with the six commits after it, and git merge-tree is clean.
  • pm:awaiting-merge is removed; ready + queue.

@objectstack-fleet objectstack-fleet Bot removed the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 14, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 43651866154 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (7/8) — 失败步骤: Run this shard's tests

    @objectstack/plugin-approvals:test:  FAIL  src/action-page-member.integration.test.ts > [#22578] handleActionPage — the ADR-0043 action page from a Request > GET renders the confirm page for a live to
      ↳ 失败原因: @objectstack/plugin-approvals:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 21 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 14, 2026

This branch has not been deployed

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

3 participants