Skip to content

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

Open

Description

@objectstack-fleet

repo:cloud#5 · hotlong · session_01Q4EjjPP1zuWBNq3nBXVei9

Filing gate: ④ a child of the topic card objectstack-ai/cloud#4465 (the maintainer's rulings 1-10 of 2026-11-13/14 are recorded there; read it first).

维护者速读

托管环境的「设置 → 用户」可以直接加人或导入 CSV,同时填好岗位、权限集和上级。这些人先以「待激活」状态存在,没有密码,只能通过云端登录,种子数据和审批流马上就能指向他们。加人、停用、恢复时,框架会通知宿主(托管时就是云端)。

Work (objectstack-ai/cloud#4465, child 1a; execution terms "Environment")

  • Add a dedicated door that does not depend on the better-auth admin plugin. On Free and Team hosted kernels that plugin mounts only with SCIM (auth-manager-plugin-list.ts:211), and admin/import-users answers 501 there (auth-plugin.ts:4604).
  • The door accepts email, name, position, permission sets and manager; CSV import reports per-row results.
  • Give the pending marker its own value, never env_native: set-user-password admits env_native rows (admin-user-endpoints.ts:1120-1136). Every password door refuses the pending marker.
  • First platform-SSO sign-in binds to the pending row (implicit-account-linking.ts). Confirm by a run whether email_verified is needed; requireLocalEmailVerified is deprecated.
  • Add a host seam on create, deactivate and reactivate, so a host can forward an invitation or a removal. The cloud implements it (cloud card C4).
  • Reword the managed-mode 409 messages from objectstack#25083 ("Invite them from the organisation in the cloud console").
  • The pending marker coexists with idp_provisioned and SCIM adoption (SCIM cannot adopt an account that SSO JIT already created: Entra ID / Okta provisioning fails with 409 for every user who signed in before SCIM was turned on #32148). There is no fan-out.
  • Measure first: does today's deactivation (admin/ban-user) answer on a Free hosted kernel? If not, add a deactivation door here.

Suggested priority:p1 (the maintainer: core onboarding flow). Routing: triage (domain:server).

priority:p1 on the maintainer's word, verbatim (seat session session_01Q4EjjPP1zuWBNq3nBXVei9, 2026-11-14): 「都是p1,车道自己认领就可以」. Triage routes it to its lane; the lane claims it.

Activity

  1. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: routed → domain:server · p1 · pm:queue · 2026-11-14T00:45Z

    ⛔ Not a claim.

  2. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim: PM loop round 14, card thread · 2026-11-14T01:44Z · p1 on the maintainer's word 「都是p1,车道自己认领就可以」 (triage 6868743330)
    Session: session_01L9qibvhGW6DCBPjrcAPjvh
    Account: os-tesla
    Branch: claude/project-thread-cl505i
    Worktree: objectstack-issue-33181
    Domain: domain:server
    Seat: domain:server#7
    File surface: packages/plugins/plugin-auth/src/ — a new pending-user door file, the password doors' refusal of the pending marker, implicit-account-linking.ts (the first-SSO bind), cloud-managed.ts (the managed-mode 409 wording), the plugin wiring and the host seam's type; packages/spec/src/ only if the shape note names it and the seat accepts it; their tests; the auth docs page that lists these doors; one root changeset. Every file is named in a shape note before code. Stop on breach.
    Container & model: L, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; PM default), effort: high
    Clause-②: yes (widening) — a new door adds pending users; refusing the new pending marker at the password doors narrows nothing, since no row carries it on main; any existing door that newly refuses something is declared as a narrowing arm in the shape note
    Metadata-protocol: none — unless the marker lands in a spec enum, which the shape note states before code
    Responsibility: managed environments have no working add-person door on Free / Team kernels (plugin-auth: the better-auth admin plugin mounts only with SCIM, and admin/import-users answers 501), objectstack | the cloud adds people only through its own console | every managed-environment admin who needs a person to exist before their first sign-in, and the seeds and approval flows that point at them
    Thread-read: 6868743330
    Premise: re-measure — the card's line references (auth-manager-plugin-list.ts:211, auth-plugin.ts:4604, admin-user-endpoints.ts:1120-1136) and whether admin/ban-user answers on a Free hosted kernel are measured at origin/main first
    Serial constraints cleared: NOT yet for code — security#2's PR #33177 (#32087 item 5) changes the same password doors; measurement and the shape note proceed now, code starts only after #33177 has merged, on top of it. The maintainer's rulings 1–10 on objectstack-ai/cloud#4465 are outside this session's reach and are being quoted onto this card; nothing is inferred from them meanwhile.

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  4. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim amendment to this seat's claim 6869596244 on #33181 · 2026-11-14T01:52Z: the seat accepts the shape note out/33181-proposed-shape.md (measured at origin/main 8835532), with these choices.

    The file surface adds:

    • packages/platform-objects/src/identity/sys-user.object.ts: one new option idp_pending on the existing source select, plus its description. The pending marker has no other home.
    • the eleven translations/*.objects.generated.ts that check-i18n-bundles.mjs --write regenerates from it. They are regenerated, never hand-edited.
    • inside plugin-auth, the files the note names: pending-sso-users.ts (new), auth-plugin.ts, admin-user-endpoints.ts, auth-manager-before-hook.ts, sso-only-local-password.ts, auth-manager-database-hooks.ts, auth-manager.ts, auth-manager-plugin-list.ts / auth-schema-config.ts, cloud-managed.ts, index.ts, and the header of implicit-account-linking.ts;
    • content/docs/permissions/authentication.mdx.

    The seat's choices:

    • The doors answer only on a cloud-managed environment, and elsewhere answer 409.
    • Metadata-protocol: none: a platform-object select option, not a spec enum.
    • A host failure leaves the operation standing. The answer reports it per row, and re-adding a pending person re-fires the seam.
    • Deactivation reuses the existing admin/ban-user / unban-user doors on kernels without the admin plugin, rather than adding a second vocabulary. That carries a declared narrowing arm, so the PR declares Clause-②: yes (widening, narrowing) with the BREAKING banner, and the contract review runs at CONTRACT_REVIEW_TIER.
    • Sessions on non-SSO channels are refused for a row still marked pending. This narrows nothing on main.

    The exact sentences and any point the maintainer's rulings on objectstack-ai/cloud#4465 fix are taken from those rulings once they are quoted on this card.

    Code still starts only after PR #33177 has merged.

  5. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Second claim amendment to this seat's claim 6869596244 on #33181 · 2026-11-14T02:54Z: the file surface adds, all in packages/plugins/plugin-auth/src/ unless named otherwise:

    • leaf modules pending-sso-marker.ts, environment-user-host.ts, user-session-admission.ts (split out so the plugin wiring has no import cycle);
    • admin-ban-endpoints.ts and user-ban-write.ts: the engine writer behind admin/ban-user / unban-user on kernels without the admin plugin;
    • set-initial-password.ts: an optional beforeWrite, so the pending check runs after the body read;
    • service-account-identity.ts: the one sys_user read per session also carries the admission columns;
    • auth-route-ledger.ts and its conformance list (the new routes);
    • packages/platform-objects/src/identity/sys-user-identity-source-label.test.ts.

    auth-manager-plugin-list.ts and auth-schema-config.ts leave the surface; they are not touched.

    The pending row is written once, born pending, by the engine as the system (the service-account door's pattern), not through the vendor user factory. The exact wording of the managed-mode 409 sentences sits in one block in cloud-managed.ts and is replaced by the maintainer's text from objectstack-ai/cloud#4465 once it is quoted on this card.

  6. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    repo:cloud#5 · hotlong · session_01Q4EjjPP1zuWBNq3nBXVei9

    Ruling text relayed 2026-11-14T03:06Z, on the maintainer's request in seat session session_01Q4EjjPP1zuWBNq3nBXVei9: 「server agent 项目读不到 cloud 仓库,请把原文贴到两张卡上」. This card is child 1a of objectstack-ai/cloud#4465 (private repo). Below is the verbatim text of that card's body as last edited at 2026-11-14T03:05Z: the rulings that bear on child 1a (1–5, 9, 10), the environment-side execution terms, the edge paths, and the open facts that name child 1a. Rulings 6 (console invite door), 7 (ADR note) and 8 (direct redirect, carried by #33183) do not bear on this card and are left out. The cloud code paths cited below are in the cloud repo and are given as references, not as files in this repo.

    Rulings (the maintainer, 2026-11-13), verbatim

    1. The split. The production environment is the source of truth for the roster and authorization: people, positions, permission sets, managers, lifecycle. Credentials stay in the cloud: every person is their own cloud account (email verification, password, social login).

    2. Who may add people. An environment administrator who may add users has the same authority as an organization administrator inviting from the console. No separate authorization model.

    3. No daily cap on environment-originated invitations. The basics are kept: each invitation is attributed to the acting administrator, and it is auditable.

    4. The invited person's organization role is the ordinary member. The console is not needed, but it is not blocked either. The account is the person's own and can, for example, create a new organization.

    5. Deactivation. Deactivating a plain member in the environment also removes them from the cloud organization. Their environment row and all their history stay. Reactivation re-sends the invitation. Exception: an organization owner or admin cannot be removed from the environment side; that is done in the console.

    6. Organization owners and admins are environment administrators, projected one way. The maintainer: 「云端的组织管理员应该就是环境的管理员才合理吧」.

      • The control plane sends the environment the organization's current owner and admin ids on every change and at kernel build.
      • The environment grants admin_full_access to each linked account, matched by cloud user id, on every entry path. The grant is owned by the Default Organization.
      • Demotion or removal withdraws only a grant the projection wrote; a grant an environment administrator made stands. Removal also revokes credentials through the existing fan-out.
      • Nothing flows up: an environment-only administrator stays an organization member, and no environment call writes an organization role.
      • An environment administrator cannot withdraw a projected grant. That is a role change in the console.
      • 「打开」 enters a non-creator admin as themself. Fact: today every 「打开」 enters as the environment's creator (routes/environment-lifecycle/sso.ts:318-325, auth-proxy-plugin.ts:605-609), so a non-creator admin has no admin standing of their own on any path.
    7. Hosted has no local break-glass admin. The maintainer chose 「A」.

    • No hosted environment sets a local password for anyone, and existing owner break-glass passwords are invalidated.
    • Emergency access is a platform-operator procedure, audited: the platform-admin disable_platform_sso / enable_platform_sso from cloud#3134, or a one-time recovery link.
    • Self-hosted deployments keep local admins.
    • This supersedes cloud#3132's "keep a break-glass local admin" for hosted.

    Deferred by the maintainer: 「代理用户先不考虑」. Audited "log in as" is a later item only.

    Execution terms for the environment (children 1a / 2b), verbatim

    Environment (children 1a / 2b):

    • A platform-admin gate. The environment id comes from the kernel binding.
    • The actor's cloud id comes from the linked objectstack-cloud account, never from request input. An admin who entered through the owner handoff is attributed through the handoff's by (the handoff signs in as the creator; handoff-sign-out.ts:9-12, cloud#2506).
    • Hosted has no local admin (ruling 10). An admin's attribution comes from their own linked cloud account (ruling 9 makes 「打开」 enter as self).
    • A durable outbox with retry (mirror sys_environment_revocation), with per-row invitation status in Setup → Users.
    • Deleting or deactivating a pending row cancels its cloud invitation (the email-typo case).
    • An owner or admin 409 refuses the whole deactivation.
    • No database transaction is held across the invite call. The cap calls back into the environment (GET /_admin/environments/:id/ai-seats, which fails closed with 503).
    • The pending marker must not read as env_native: set-user-password admits env_native rows (admin-user-endpoints.ts:1120-1136), and a row with no sys_account reads env_native today. Every password door refuses the pending marker.
    • The pending-user door does not depend on the better-auth admin plugin. On Free and Team hosted kernels that plugin mounts only with SCIM (auth-manager-plugin-list.ts:211; admin/import-users answers 501 there, auth-plugin.ts:4604). Mounting it on hosted would also expose impersonate and ban, which needs its own ruling.

    Edge paths the children must cover, verbatim

    • Console-side removal or role change of a person the environment lists as active: the environment row stays active but cannot sign in. The roster shows that state.
    • A person invited directly from the console has no pending row and arrives by JIT, as today.
    • Adding an email that already has an environment row: decide in child 1a whether that is a refusal or an update.
    • Reactivation, or a CSV row, at the developer cap is refused with 403 DEVELOPER_ORGANIZATION_MEMBER_LIMIT_REACHED. This is shown per row, and a CSV import reports partial failures row by row.
    • A pending row that never accepts: it stays pending, can be re-sent, and can be deleted, which cancels the invitation.

    Open facts naming child 1a, verbatim

    • Does admin/ban-user (today's deactivation) answer on a Free hosted kernel? If not, child 1a needs its own deactivation door.

    Rulings recorded after the body (comment 6870693029 on cloud#4465)

    d-2 应急入口不考虑 d-3 同意

    For this card: D-3 means stored local password hashes on hosted are refused, not deleted, so every password door refuses rather than clears. D-2 means no one-time recovery link is built; ruling 10's emergency access is not extended here.

  7. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibvhGW6DCBPjrcAPjvh

    os-dev-report

    {
      "issue": 33181,
      "status": "done",
      "branch": "claude/project-thread-cl505i",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33227",
      "head": "be43f4e1",
      "session": "session_01L9qibvhGW6DCBPjrcAPjvh",
      "premise_still_valid": true,
      "summary": "Draft PR, one commit on origin/main 7257f57a. On a cloud-managed environment, POST /admin/add-sso-user and /admin/import-sso-users (platform-admin gated, no admin-plugin dependency, 409 RESOURCE_CONFLICT off managed) write a person born sys_user.source=idp_pending by one engine insert as the system, attributed to the administrator, with positions, permission sets (caller's context, one unit of work), membership and manager. Every password door (set-user-password, set-initial-password, reset-password, phone-number/reset-password) answers 409 for a pending row; session creation answers 403; the first platform-IdP sign-in binds (implicit-linking rule 2) and stampIdentitySource moves it to idp_provisioned. admin/ban-user and unban-user now work without the admin plugin (engine writer, session refusal with the vendor's 403 BANNED_USER). Host seam environmentUserHost: told after an add, asked before a ban/unban, never inside a transaction; a typed EnvironmentUserHostRefusal (409 owner/admin, 403 DEVELOPER_ORGANIZATION_MEMBER_LIMIT_REACHED) refuses the operation (ban/unban write nothing; a refused add is taken back out, per row on import); other host failures are fail-open and reported per row. Events carry actorCloudUserId from the administrator's linked objectstack-cloud account only. Managed 409 wording is provisional in one block of cloud-managed.ts. The PR owes a contract review at CONTRACT_REVIEW_TIER (narrowing) from the new review agent.",
      "tests": "plugin-auth pending-sso-users.test.ts 35 tests and pending-sso-seams.test.ts, plus appended describes in implicit-account-linking.test.ts and password-reset-ends-sessions.test.ts; CONTROL legs for env_native and idp_provisioned rows and for the admin plugin mounted. 25 ablations, all red: A1-A16 (password arms, session refusals, ban writer, door refusals, marker on insert, one-read columns) and B1-B9 (host refusals on ban/unban, refusal status filter, refusal recognition, take-back on add, refused re-add, cloud id provider filter, cloud id on the ban event, take-back of grants and membership). plugin-auth full suite 270 files, 5169 passed, 10 skipped; platform-objects 122 files, 1958 passed; typecheck exit 0.",
      "gates": "dispatch-gates --commands: 108 families, all run with </dev/null, exit codes recorded; --ran: 108 derived, 108 run, 0 NOT-MEASURED (derived zero). Gates reading test sources re-run after the dogfood-table change: all exit 0. pnpm lint exit 0. Record: out/33181-gates-ran-be43f4e1.list.",
      "dogfood_battery": "ran (the title carries ! and Clause-② is narrowing): the full run failed only the two admin-route sweeps, naming the two new doors; both are now classified (gate first, the admin gets 409 off managed) and the two files re-run green, 15 passed; 461 other files passed.",
      "mcp_calls": "reads only (issue_read, pull_request_read); 0 writes",
      "api_writes": "2: relay pr_create (#33227, assignee os-tesla, read back 14614/14614 bytes identical) and this os-dev-report comment via post-stamped",
      "open_questions": [
        "The four managed 409 sentences are provisional until the maintainer's text is quoted on this card."
      ],
      "follow_ups": [
        "Seat to file out/33181-fu-seam.md: a pending-only remove door that cancels the invitation, and where the durable outbox with retry and per-row invitation status lives (comment 6870710213).",
        "For #33186: no console entry for the two doors yet; the sys_user ban_user / unban_user actions keep requiresFeature: 'admin'."
      ],
      "security_findings": "none new in this report",
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Release: session_01L9qibzuZJBQY4wUQFJbkfx · seat domain:server#7 signs off on the maintainer's word 「所有座位当前任务处理完,合并后就下班」 (2026-11-14T04:35Z, project chat) and 「各座位自己的契约复审也暂停,等新的 review agent 上线后复审」 (2026-11-14T04:37Z) · back to pm:queue for the next claimant · 2026-11-14T06:10Z
    Resume-from: claude/project-thread-cl505i@be43f4e112bf6e279bc4c8ebb8ac25cdf938bf61

  9. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Nov 14, 2026
  10. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Maintainer decision recorded · 2026-11-14T14:37Z: the maintainer chose 「用 dev 的文案」 on the seat's decision card, which asked whether to use the dev's wording for the managed-mode 409 sentences or wait for the original text. The sentences in cloud-managed.ts on PR #33227 ("Add them in Setup → Users instead; they sign in through that identity provider") are therefore final, not provisional. A later rewording is its own card.

  11. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim: resume after the 04:35Z sign-off, card thread · 2026-11-14T15:47Z · the seat is back on duty for review and landing only (「你现在可以自己契约复审了。」 2026-11-14T14:17Z; 「车道座位应该使用pm-dispatch 技能审核和落地pr」 2026-11-14T14:32Z); PR #33227
    Session: session_01L9qibvhGW6DCBPjrcAPjvh
    Account: os-tesla
    Branch: claude/project-thread-cl505i
    Worktree: objectstack-issue-33181
    Domain: domain:server
    Seat: domain:server#7
    File surface: unchanged — claim 6869596244 with amendments 6869729952 and 6870557312. Stop on breach.
    Container & model: L, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; PM default), effort: high
    Clause-②: yes (narrowing), as declared on PR #33227 and reviewed at CONTRACT_REVIEW_TIER (records 6879138774, 6879497274)
    Metadata-protocol: none
    Responsibility: managed environments' add-person and ban doors on Free / Team kernels (plugin-auth), objectstack | every managed-environment admin | 18.4.0 onward
    Thread-read: 6879497274
    Premise: re-measured by the card thread on 84a7069 (dogfood battery green on e1637d8)
    Serial constraints cleared: #33177 merged before code; no other open PR of this seat changes these doors

  12. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  13. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: H14 · +pm:blocking · 2026-11-14T17:50Z

    ⛔ Not a claim; claim and state unchanged.

    Open cards name this one in Blocked-by:: #33186, #33187 (and #33228 waits on it). A blocker in flight carries pm:blocking.

  14. added
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    on Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions