You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSO end-user UX: every refusal says "the sign-in link expired", internal terms reach end users, a refused IdP-initiated sign-in is silent, and Log out leaves the IdP session so the next click signs straight back in #32087
ObjectOS Enterprise, cloud mainde8d979 (framework pin e24136c2). Live Okta (OIDC and SAML), browser walk-through for objectstack-ai/cloud#3666.
Findings, in the order a user meets them
"Sign in with SSO" with an empty email shows the raw email, organizationSlug, domain or providerId is required. It should say "Enter your work email to sign in with SSO" (the button reads the password form's email field).
Every SSO refusal starts with the wrong sentence. The login page shows "Single sign-on could not be completed — the sign-in link expired or was already used. Please try again." followed by the cause in parentheses, for:
SELF_REGISTRATION_CLOSED (uninvited user, JIT off): the cause text is right but aimed at admins: "(audience posture invite_only) … turn on just-in-time provisioning (allowJit)";
state_not_found.
None of these is an expired link, and "try again" only repeats the refusal. Expected: a per-cause sentence for the end user ("Your account isn't set up for this system yet. Ask your administrator to invite you." etc.), with the technical cause kept for admins (logs or a details toggle).
A refused SAML IdP-initiated sign-in is silent. With allowIdpInitiated off, the Okta tile lands on /_console/login with no message. The server logs "SAML IdP-initiated SSO rejected: InResponseTo missing and allowIdpInitiated is false". The page should say that sign-in has to start from this system's login page, or that the admin must allow IdP-initiated sign-in.
Log out does not end the IdP session. After Log out, "Sign in with SSO" signed the same user straight back in with no password prompt (Okta session alive). On a shared computer the next person gets the previous user's account. Expected: RP-initiated logout to the IdP's end_session_endpoint for OIDC (Okta and Entra both publish one) and SAML SLO where configured, or at least an admin setting for it.
IdP-provisioned users: the user record shows "Email Verified: No" although the IdP asserted the email, and offers "Set Password", which gives an SSO-only user a local password that bypasses the IdP. That may be intended; enterprise customers usually want SSO-provisioned users to sign in only through the IdP, or at least a warning on that button.
Edit Sign-In Options renders its two independent checkboxes (Allow IdP-Initiated, Allow JIT) as round, radio-style controls, which read as either/or.
What works well: the IdP-provisioned users are labelled "IdP-Provisioned" in the Users list; JIT, group → permission-set projection and OIDC signing-key rotation (Okta keyRotate) all worked live; the delete dialog explains the consequence for the domain's users.
Lane:domain:ui. Most items are login-page and Setup UX: per-cause end-user sentences, the silent IdP-initiated refusal, the empty-email message, and the checkbox controls. Item 4 (ending the IdP session on Log out, via RP-initiated logout) is server work, and is a domain:server child filed at step 0.
Security lane: triage referred this card to security#1 by number. Whether any item needs private handling is that lane's call, so no mechanism is discussed here.
Scope of this dispatch: items 1, 2, 3 and 6 (the empty-email message, per-cause end-user sentences, the silent IdP-initiated refusal, the checkbox controls). Items 4 and 5 are not built here: triage referred this card to security#1 by number, and whether they need private handling is that lane's call; the seat files no child for them and says nothing more about them in public. If a UX item turns out to need a server signal, the seat files a domain:server child at that point.
The card thread is being opened; the Claim: naming its branch follows within the hour.
Claim: PM loop round 1 · 2026-11-10T08:11Z
Session: session_01WGufzfoxBMQZZCvgX8SGhb (card thread, dev) · seat session_01WGufzge87Pae2h7uSQzX8H
Account: os-support-ai
Branch: claude/project-thread-m23m40
Worktree: objectstack-issue-32087
Domain: domain:ui
Seat: domain:ui#2
File surface: ui/apps/console/src/pages/auth/LoginPage.tsx and its tests; ui/packages/app-shell/src/console/auth/** (the package login page and its refusal mapping) as far as the shared mapping needs; ui/packages/auth/src/LoginForm.tsx and its tests; ui/packages/i18n/src/locales/**; the renderer that draws the Edit Sign-In Options booleans (located by the dev, reported before building if outside ui/); root .changeset/; ⛔ no server code, no packages/spec/**
Container & model: M, mode:thread, model: default tier (dispatch-gates --tier: "no path-derived mandate"), effort: high
Clause-②: yes (widening): new keys in the published @object-ui/i18n catalogue
Metadata-protocol: none
Responsibility: ui/apps/console/src/pages/auth/LoginPage.tsx:366-374 (objectstack) prints one "link expired" sentence plus the raw cause for every SSO refusal | no platform path covers it: the server sends codes, the wording is the console's | every end user of an SSO-enabled Enterprise deployment, today
Thread-read: 6762098073
Premise: holds at origin/mainfe1249b7: auth.login.errors.oauthCallbackFailed is the only SSO callback sentence on the console login page
Serial constraints cleared: PR #32050 (#32021, this seat) edits the console and package LoginPage.tsx, signInRefusalMessages.ts and console LoginPage.* tests; this card builds on main away from its regions and merges main after #32050 lands (landed first). No other open PR names these files.
Scope: items 1, 2, 3 and 6 of #32087; Part of #32087. Items 4 and 5 stay with the lane triage referred them to. Order: /mnt/project-files/ui-orders/32087.md. Summons: the maintainer's 「各开一个 spec ui devx 座位」 (this project, 2026-11-09).
domain:ui#2 · os-support-ai · session_01WGufzfoxBMQZZCvgX8SGhb
os-dev-report
{
"issue": 32087,
"status": "done",
"branch": "claude/project-thread-m23m40",
"pr": "#32129",
"head": "dc0a73f3",
"session": "session_01WGufzfoxBMQZZCvgX8SGhb",
"premise_still_valid": true,
"summary": "Items 1, 2 and 6 ship whole; item 3 ships for OIDC (state_not_found) and is a reported deviation for SAML, so the PR says Part of #32087. (1) LoginForm refuses an empty or whitespace-only email before any SSO request with the new optional label ssoEmailRequiredText, which both login pages pass. (2) ssoCallbackRefusal.ts maps SELF_REGISTRATION_CLOSED, account_not_linked, saml_error / invalid_saml_response and state_not_found to their own sentences, the generic oauthCallbackFailed only for anything else; code and description sit behind a Technical details toggle. DefaultLoginPage reads no ?error, so one reader. (6) The 16px Radix checkbox drew rounded-sm at 10px (theme --radius 0.875rem); console index.css sets --radius-sm 0.25rem on [role=checkbox] only, measured live 10px before, 4px after. No packages/** metadata moved.",
"line_budget": "22 files, +494 / -19 against origin/main (code: LoginForm.tsx, ssoCallbackRefusal.ts new, two LoginPage.tsx, index.css; 11 locale packs; 5 test files; 1 changeset)",
"files_changed": [
".changeset/32087-sso-login-refusal-sentences.md",
"ui/apps/console/src/index-css-scan-excludes-tests.test.ts",
"ui/apps/console/src/index.css",
"ui/apps/console/src/pages/auth/LoginPage.tsx",
"ui/apps/console/src/pages/auth/tests/LoginPage.error-banner.test.tsx",
"ui/apps/console/src/pages/auth/ssoCallbackRefusal.ts",
"ui/packages/app-shell/src/console/auth/LoginPage.tsx",
"ui/packages/app-shell/src/views/ActionParamDialog.test.tsx",
"ui/packages/auth/src/LoginForm.tsx",
"ui/packages/auth/src/tests/LoginForm.test.tsx",
"ui/packages/i18n/src/tests/de-quote-pairing-3876.test.ts",
"ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt-BR,ru,zh-Hans,zh-Hant}.ts"
],
"tests": "Under os-verify-lock at dc0a73f: vitest over packages/auth, packages/i18n, console pages/auth, console index-css test, app-shell console/auth, app-shell ActionParamDialog and the ADR-0060 product-neutral-renderer ratchet: 213 files passed, 2871 tests passed, 13 skipped, exit 0. type-check auth, i18n, app-shell, console: all exit 0. eslint over every changed file exit 0. Ablations via scripts/ablation-replace.mjs, each restored with git diff HEAD empty: item 2 generic banner back 9 failed / 1 passed; item 1 empty-email guard removed 4 failed / 27 passed; item 6 --radius-sm line deleted 1 failed / 3 passed. Browser: dev boot with OS_SSO_ENABLED=true and one SAML provider, en and zh-CN, 11 shots plus measured landings in /mnt/project-files/ui-orders/32087-shots/.",
"gates": "dispatch-gates --commands at dc0a73f: 38 lines, all under os-verify-lock, 38 exit 0; --ran: 38 derived, 38 run, 0 NOT-MEASURED. check-sdui-manifest exit 0 (in the 38). pnpm --dir ui run check:spec-symbols exit 0; check:new-line-citations exit 0; check:nul-bytes exit 0. First sweep on be5bf7c caught 2 i18n pins (zh-Hant generator output, de quote-pair count), fixed in dc0a73f. CI check-runs on #32129 not yet read (PR just opened).",
"dogfood_battery": "n/a: no ! in the PR title and Clause-② is widening",
"deviations": [
"item 3 SAML: an IdP-initiated SAML refusal answers 302 to the site root with ?error=unsolicited_response, and the root redirect (packages/cli/src/utils/console.ts) sends /_console/ without the query, so the login page gets no signal. Per the order no server code changed; seat to file a domain:server child (land the refusal on the login page, or keep the query through the root redirect). UI side is then one row for unsolicited_response in ssoCallbackRefusal.ts.",
"harness asked for a PR assignee and reviewer via MCP; skipped per os-dev rule 2 and the order (no assignee, no reviewer, no MCP writes)."
],
"mcp_calls": "read-only only (issue and claim reads on #32087); no write tool.",
"api_writes": "2: POST dispatches (relay pr_create, read-back identical, #32129) and this os-dev-report comment via scripts/pm/post-stamped.mjs. Plus 3 git pushes of the branch (52043d2, be5bf7c merge of main, dc0a73f).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: exception: maintainer (the order names this domain:server child) · a refused IdP-initiated SAML sign-in lands on / with ?error=unsolicited_response and the root redirect to /_console/ drops the query, so the user sees no message; ask: land it on the login page like the other SAML refusals, or keep the query through the redirect · dedupe: SAML IdP-initiated refusal landing, root redirect query drop"
]
}
MERGED as cc8c4b0f4e67fcff96a6d685096c2003a460167d (Part of #32087)
Delivered
item 1 (empty email + "Sign in with SSO" asks for the email, no request), item 2 (per-cause end-user sentences, technical cause behind a toggle), item 3 for OIDC (state_not_found), item 6 (checkboxes keep a small corner)
Maintainer rulings: items 4 and 5 · 2026-11-13T22:19Z
The maintainer answered both decision cards of 6845651657 in the seat thread (2026-11-13 22:19Z).
Item 4 → A: a per-provider "sign out of the company sign-in too" switch, off by default.
Each SSO provider gets its own switch; nothing changes until an administrator turns it on.
With it on, signing out of the platform also ends the company sign-in session, for SAML and OIDC alike.
SAML: the provider stores its identity provider's logout address, and logout messages coming back from the identity provider are accepted only when signed.
Saving the switch on a provider that cannot honour it (an OIDC provider with no logout endpoint, a SAML provider with no logout address) is refused loudly.
Work: a server card (switch, SAML and OIDC wiring, signature requirement, logout address field, spec description) and a ui card (form item, sign-out redirect).
Item 5 → D: honour the existing SSO-only switch.
When an environment has SSO-only mode on, the server refuses to give an IdP-provisioned user a local password at every door. The one exception is a managed platform administrator setting their own break-glass password, the same exception the cloud-hosted rule already has.
When SSO-only mode is off, behaviour is unchanged, and the administrator's set-password action warns that it gives a managed user a way to sign in that bypasses the company sign-in.
Not retroactive: managed users who already hold a local password are a separate question.
Work: a server card (the refusal, the exception, pins, the ssoOnlyMode description), a governed ADR-0135 D5.2 note (kept in step with cloud ADR-0024 D5.2), and a ui card (the warning).
The security seat orders the server cards first; the ui cards follow each server half.
Claim: PM loop round 4
Session: session_01L9qibwgMDk9yn3MZdGMqEM (card thread), dispatched by session_01L9qibwMmGJKaQ52CgHqNu2
Account: os-tesla
Branch: claude/project-thread-zvdozk (objectstack-ai/objectstack)
Worktree: objectstack-sso-only-local-password
Domain: none (security lane; ruling 6866334319 on this card, item 5)
Seat: security#2
File surface: to be named after the read-only measurement (every door that can give a user a local password, the SSO-only check, the ssoOnlyMode description); amended by the seat before any edit
Container & model: M (to measure), mode:thread, model: default tier, effort: xhigh (security, fleet rule 2)
Clause-②: no (expected; the ssoOnlyMode description text changes, so the seat reviews it)
Metadata-protocol: none
Responsibility: an environment in SSO-only mode gives a managed user no local password | every local-password door, with the managed platform administrator's own break-glass exception | every SSO-only deployment
Thread-read: 6866334319
Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 5, and the ui half follows it
Serial constraints cleared: none (item 4 touches sign-out, not password doors)
Claim: PM loop round 5
Session: session_01L9qibz44EJYWFvzJTmGHkg (card thread), dispatched by session_01L9qibwMmGJKaQ52CgHqNu2
Account: os-tesla
Branch: claude/project-thread-ur0eh0 (objectstack-ai/objectstack)
Worktree: objectstack-sso-provider-sign-out
Domain: none (security lane; ruling 6866334319 on this card, item 4)
Seat: security#2
File surface: to be named after the read-only measurement (the sign-out path, SSO provider config and its schema); amended by the seat before any edit
Container & model: L (to measure), mode:thread, model: default tier, effort: xhigh (security, fleet rule 2)
Clause-②: yes (expected; widening: a per-provider switch and a logout address; the seat gives the go on the field shapes and runs the contract review)
Metadata-protocol: to measure
Responsibility: signing out can also end the company sign-in, per provider, off by default | SSO provider config and sign-out for SAML and OIDC | deployments that turn the switch on
Thread-read: 6866334319
Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 4, and the ui half follows it
Serial constraints cleared: none (item 5 touches password doors, not sign-out)
Claim 6866398944 amended · 2026-11-13T22:26Z: surface named from the card thread's measurement (objectstack 84c66d36). No packages/spec change.
File surface: packages/platform-objects/src/identity/sys-sso-provider.object.ts (a per-provider sign-out switch column, an action param, the list column) and the SSO provider actions that take the SAML logout address; packages/plugins/plugin-auth/src/ (SSO sign-in options, SAML and OIDC provider bridges, sign-out, OIDC callback) and their tests; generated references; root .changeset/
Clause-②: yes (widening: a per-provider switch, off by default, and an optional logout address param)
Metadata-protocol: added
Claim 6866387589 amended · 2026-11-13T22:27Z: surface named from the card thread's measurement (objectstack 84c66d36): four doors that can give a user a local password.
File surface: packages/plugins/plugin-auth/src/ (the set-initial-password, administrator set-password and both reset-password doors, one shared SSO-only judge beside the cloud-managed one) and their tests; packages/spec/src/system/auth-config.zod.ts (the ssoOnlyMode description text only) and its generated reference; root .changeset/
Clause-②: no
Metadata-protocol: none
{
"issue": 32087,
"status": "done",
"branch": "claude/project-thread-ur0eh0",
"pr": "https://github.com/objectstack-ai/objectstack/pull/33170",
"session": "session_01L9qibz44EJYWFvzJTmGHkg",
"premise_still_valid": true,
"summary": "Item 4: a per-provider switch sys_sso_provider.sign_out_of_idp (off by default, trackHistory), taken by the vendor's /sso/register and /sso/update-provider as signOutOfIdp, with the Edit Sign-In Options param and the list column. On, sign-out also ends the identity provider session: SAML through the vendor's single logout, with enableSingleLogout read per provider and logout messages accepted only when signed; the IdP logout address is the new logoutUrl on the register and edit SAML doors, stored as idpMetadata.singleLogoutService (no new column). OIDC through an RP-initiated logout URL carrying id_token_hint, the end_session_endpoint read from discovery when the switch is saved on and kept in oidc_config. Saving it on is refused 400 VALIDATION_FAILED on signOutOfIdp for a provider that cannot honour it. Surface grep for ruling|Ruling-ref|maintainer|⛔: 14 added lines, every one a code comment citing ruling record 6866334319 or the existing 6718355623 (the file convention); no maintainer or ⛔ hit.",
"tests": "New pins: sso-saml-refusal-landing.test.ts (6: switch off keeps the IdP logout endpoint closed and the session stands; on, sign-out answers the IdP logout URL and the session ends; unsigned LogoutRequest and unsigned LogoutResponse each refused; signed redirect-binding message refused; a signed LogoutRequest ends only the named session; SP metadata advertises SingleLogoutService only when on), sso-oidc-refusal-cause.test.ts (3: control off, on with id_token_hint/client_id/post_logout_redirect_uri/state and Location, untrusted callbackURL not passed), sso-provider-lifecycle.test.ts (5: save-time refusals through /sso/update-provider for SAML and OIDC, saving off never refused, logoutUrl via edit and register doors, OIDC endpoint kept), sso-sign-out-of-idp.test.ts (14 unit). Full suites: plugin-auth Test Files 267 passed, Tests 5072 passed | 10 skipped; platform-objects Test Files 121 passed, Tests 1955 passed. Ablation (tests import src): scripts/ablation-replace.mjs set wantLogoutRequestSigned/wantLogoutResponseSigned false in ssoSamlOptions(), anchor x1 -> x0 and blob 21d3b866 -> 1fa01a29 on disk; the unsigned-message pin and the redirect-binding pin went red (2 failed | 4 passed); restored, blob == HEAD and git diff HEAD empty; green on the final run. typecheck (both packages) exit 0; eslint on changed files exit 0.",
"gates": "dispatch-gates --commands derived 77; each run with </dev/null under os-verify-lock, exit codes recorded before any pipe. First pass had 4 non-zero: check-plugin-teardown-shape --self-test exit 3 (shallow clone; fixture commit fetched, then 0), check-tenant-audit-census and its --self-test exit 1 (an anonymous adapter type on the new provider-row write; named it SsoProviderRowAdapter, then 0), check:dual-build-cjs-loads exit 3 (dist missing, built by a later gate in the run, then 0). dispatch-gates --ran: 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN. CI check-runs on the PR not yet read (draft just opened).",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "read-only only: mcp__github__issue_read and mcp__github__pull_request_read (an exact count was not kept across a context reset); no MCP write tool",
"api_writes": "1 — the relay dispatch POST /repos/objectstack-ai/objectstack/dispatches for pr_create (run 43529268119, read-back identical, assignee os-tesla), plus this comment through post-stamped.mjs; pushes to claude/project-thread-ur0eh0 only",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: PR #33170 Acceptance notes · noted, not filed — the Console sign-out following the answered url, and any Edit Sign-In Options / SAML form item the form does not render by itself (the later ui card)",
"carrier: PR #33170 Acceptance notes · noted, not filed — @better-auth/sso 1.7.12 limits measured: a signed HTTP-Redirect logout message is refused (fail closed), the outgoing LogoutRequest carries no RelayState, only sessions started while the switch is on are covered, the shared OIDC callback without a provider id is not recorded, an IdP-started logout is not written as a logout audit row"
]
}
{
"issue": 32087,
"status": "done",
"branch": "claude/project-thread-zvdozk",
"pr": "https://github.com/objectstack-ai/objectstack/pull/33177",
"head": "f3393c48",
"session": "session_01L9qibwgMDk9yn3MZdGMqEM",
"premise_still_valid": true,
"summary": "Item 5 server half (ruling record 6866334319, D). With SSO-only mode on, four doors give an idp_provisioned user no local password: set-initial-password, admin/set-user-password (sys_user#set_user_password), /reset-password and /phone-number/reset-password; each answers 409 RESOURCE_CONFLICT naming SSO-only mode and writes nothing. Exception: a platform administrator setting their own (session isPlatformAdminUser on set-initial; actor = target behind gateAdmin on set-user-password; isPlatformAdminUserId on the resets). One shared judge in plugin-auth sso-only-local-password.ts reuses the cloud-managed source read (readUserSource, moved there) and fails closed as 503. The reset doors judge only a live token or a matching in-budget code, read without spending it. ssoOnlyMode TSDoc + describe say what the server enforces (generated reference regenerated); authentication.mdx gains a short section. Not doors (unchanged): change-password, sign-up, create/import, SCIM, unmounted email-OTP reset.",
"door_exception_table": [
"set-initial-password: SSO-only + idp_provisioned caller -> 409; own platform admin (session isPlatformAdminUser) -> allowed; env_native/anonymous -> unchanged",
"admin/set-user-password: SSO-only + idp_provisioned target -> 409 (another administrator as target included); target = caller (gateAdmin admits platform admins only) -> allowed; env_native -> unchanged; cloud-managed answers first",
"reset-password (emailed): SSO-only + idp_provisioned (holder included) -> 409; platform admin own (isPlatformAdminUserId) -> allowed; unmatched token -> same answer as SSO-only off",
"phone-number/reset-password: same as emailed; wrong code -> same answer as SSO-only off"
],
"tests": "At f3393c48 under os-verify-lock: plugin-auth whole package vitest 267/267 files, 5072 passed, 10 skipped, exit 0; pins in sso-only-local-password.test.ts (new), cloud-managed.test.ts and password-reset-ends-sessions.test.ts (new top-level describes in the shared harnesses), 77 tests; plus api-error-code.pin + request-json-body-refusal, 157 total in those 5 files. plugin-auth typecheck (with check:test-typecheck) and spec typecheck exit 0 after a full declaration build. Ablation via scripts/ablation-replace.mjs on the committed judge (refuse verdict -> allow, anchor 1 -> 0): 9 failed / 68 passed across the three pin files, restored by the tool, blob equals HEAD, git diff HEAD empty. One existing pin reversed by the ruling (cloud-managed.test.ts 'unmanaged with SSO-only on, both doors answer the idp user as before') now asserts the SSO-only refusal answers there, with a new SSO-only-off control leg.",
"gates": "dispatch-gates --commands at f3393c48: 120 derived, all run with </dev/null and exit codes recorded first, 120 exit 0; --ran: 120 derived, 120 run, 0 NOT-MEASURED, 0 UNRUN (list: /mnt/project-files/security-seat-2/r2026-11-13/out/32087-i5-gates-ran-f3393c48.list). spec check:generated: check:docs stale, regenerated with --fix. First sweep found check:dispatcher-error-vocabulary unresolved-constant on the refusal code alias (fixed: literal + equality pin) and api-error-code.pin on a non-literal APIError body (fixed). eslint over changed .ts exit 0. CI check-runs on #33177 not yet read (PR just opened).",
"dogfood_battery": "n/a: no ! in the PR title and Clause-② is no with no (narrowing) arm",
"ruling_grep": "Surface grep for ruling|Ruling-ref|maintainer|⛔ in the touched files: cloud-managed.ts:7 (cloud ADR-0024 D5.2 / ADR-0135 D5.2 mirror; consistent, untouched); admin-user-endpoints.ts:1073-1075 (ruling C-prime item 2, the forced reset's revocation default; the SSO-only refusal answers before it, untouched); auth-manager-before-hook.ts:420-424, 538, 553, 678 (other doors' rulings, untouched); auth-plugin.ts and auth-manager.ts hits none adjacent to the edits; auth-config.zod.ts hits concern audience posture, not ssoOnlyMode. No hit contradicts this change.",
"deviations": [
"The card's newest Claim: line is the sibling item-4 claim (branch ur0eh0), not 6866387589 naming this branch; the seat confirmed in the seat thread that this branch may push.",
"The harness asked for an assignee and reviewer via MCP; skipped per the frame and os-dev rule 2. The only assignee write is assignees [os-tesla] on the relay pr_create; read-back lists os-tesla.",
"Merged origin/main once before opening (one docs-only commit); no rebuild needed for it."
],
"remainders": [
"ui card: the administrator set-password warning",
"governed ADR-0135 D5.2 note, kept in step with cloud ADR-0024 D5.2 (seat)",
"the #32115 local-password remainder and the question of managed users who already hold a local password"
],
"mcp_calls": "0",
"api_writes": "2: POST relay pr_create (#33177, read-back identical, 11649 bytes) and this os-dev-report comment via post-stamped. Plus git pushes of the branch.",
"open_questions": [],
"security_findings": "withheld",
"out_of_scope_findings": []
}
Claim 6866387589 amended again · 2026-11-14T01:15Z: the declaration follows contract review 6868317289. With SSO-only on, the local-password doors now refuse a managed user where they answered before, and two additive members widen the surface. The surface is unchanged.
2026-11-14T04:35Z One more measured case: EMAIL_DOMAIN_NOT_ALLOWED still shows the generic "link expired" headline, on 18.4.0.
Where. Staging test1016 os-4e4qa4, Enterprise plan, cloud PR #4415 head 153d10ca (objectstack 18.4.0). The provider is a real Okta OIDC app (okta, domain objectlab.ai verified, JIT on).
What happened. An Okta user whose email is qa.sub@dev.objectlab.ai (a subdomain, so unlisted) used the login page's 「Sign in with SSO」.
The server refused it correctly. The container logged [sso] account creation refused: the address is not at a domain the provider has proven {"code":"EMAIL_DOMAIN_NOT_ALLOWED","providerId":"okta","method":"sso-oidc"}, and no account was created.
The callback lands on /_console/login?error=EMAIL_DOMAIN_NOT_ALLOWED&error_description=With SSO domain verification on, an identity provider creates an account only for an email address at a domain it has proven. Nothing was created.
The banner reads "Single sign-on could not be completed — the sign-in link expired or was already used. Please try again." The real code appears only behind 「Technical details」.
Why.ui/apps/console/src/pages/auth/ssoCallbackRefusal.ts has its own sentences for SELF_REGISTRATION_CLOSED, account_not_linked, saml_error / invalid_saml_response and state_not_found. Every other code falls back to oauthCallbackFailed. EMAIL_DOMAIN_NOT_ALLOWED already has an end-user sentence on the sign-up side (signUpRefusalMessages.ts:65, auth.register.errors.emailDomainNotAllowed), but the SSO callback mapping does not use it.
Impact. "Please try again" sends the person into a loop that can never succeed. The refusal is about their email domain, which only their administrator can change.
Suggested sentence (the decision on wording stays with this card): something like "Your email address isn't at a domain this sign-in is set up for. Ask your administrator."
Activity
objectstack-fleet commentedon Nov 10, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage:
findingpromoted →domain:ui· p1 ·pm:queue· 2026-11-10T07:55Z⛔ Not a claim.
cloud/ROADMAP.mdtier 1). Under pm-dispatch:企业版相关卡的优先级以 cloud ROADMAP.md 为准,席位不另行排序;总监席每周复读并把不一致之处呈维护者(cloud#3835 子项 6) #31574 the roadmap sets Enterprise order, so the framework gaps it found are p1.domain:ui. Most items are login-page and Setup UX: per-cause end-user sentences, the silent IdP-initiated refusal, the empty-email message, and the checkbox controls. Item 4 (ending the IdP session on Log out, via RP-initiated logout) is server work, and is adomain:serverchild filed at step 0.security#1by number. Whether any item needs private handling is that lane's call, so no mechanism is discussed here.Generated by Claude Code
objectstack-fleet commentedon Nov 10, 2026
domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H
Reserved by
domain:ui#2· 2026-11-10T08:10Z · SSO end-user UX on the login page and Setup · ⛔ theClaim:followspm:queue(triage 6762098073:domain:ui, p1, Enterprise order under pm-dispatch:企业版相关卡的优先级以 cloud ROADMAP.md 为准,席位不另行排序;总监席每周复读并把不一致之处呈维护者(cloud#3835 子项 6) #31574) under the maintainer's 「各开一个 spec ui devx 座位」 (this account's project, 2026-11-09).security#1by number, and whether they need private handling is that lane's call; the seat files no child for them and says nothing more about them in public. If a UX item turns out to need a server signal, the seat files adomain:serverchild at that point.Claim:naming its branch follows within the hour.objectstack-fleet commentedon Nov 10, 2026
domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H
Claim: PM loop round 1 · 2026-11-10T08:11Z
Session:
session_01WGufzfoxBMQZZCvgX8SGhb(card thread, dev) · seatsession_01WGufzge87Pae2h7uSQzX8HAccount:
os-support-aiBranch:
claude/project-thread-m23m40Worktree:
objectstack-issue-32087Domain:
domain:uiSeat:
domain:ui#2File surface:
ui/apps/console/src/pages/auth/LoginPage.tsxand its tests;ui/packages/app-shell/src/console/auth/**(the package login page and its refusal mapping) as far as the shared mapping needs;ui/packages/auth/src/LoginForm.tsxand its tests;ui/packages/i18n/src/locales/**; the renderer that draws the Edit Sign-In Options booleans (located by the dev, reported before building if outsideui/); root.changeset/; ⛔ no server code, nopackages/spec/**Container & model: M,
mode:thread,model: default tier(dispatch-gates --tier: "no path-derived mandate"),effort: highClause-②: yes (widening): new keys in the published
@object-ui/i18ncatalogueMetadata-protocol: none
Responsibility:
ui/apps/console/src/pages/auth/LoginPage.tsx:366-374(objectstack) prints one "link expired" sentence plus the raw cause for every SSO refusal | no platform path covers it: the server sends codes, the wording is the console's | every end user of an SSO-enabled Enterprise deployment, todayThread-read: 6762098073
Premise: holds at
origin/mainfe1249b7:auth.login.errors.oauthCallbackFailedis the only SSO callback sentence on the console login pageSerial constraints cleared: PR #32050 (#32021, this seat) edits the console and package
LoginPage.tsx,signInRefusalMessages.tsand consoleLoginPage.*tests; this card builds onmainaway from its regions and mergesmainafter #32050 lands (landed first). No other open PR names these files.Scope: items 1, 2, 3 and 6 of #32087;
Part of #32087. Items 4 and 5 stay with the lane triage referred them to. Order:/mnt/project-files/ui-orders/32087.md. Summons: the maintainer's 「各开一个 spec ui devx 座位」 (this project, 2026-11-09).objectstack-fleet commentedon Nov 10, 2026
domain:ui#2 · os-support-ai · session_01WGufzfoxBMQZZCvgX8SGhb
os-dev-report
{
"issue": 32087,
"status": "done",
"branch": "claude/project-thread-m23m40",
"pr": "#32129",
"head": "dc0a73f3",
"session": "session_01WGufzfoxBMQZZCvgX8SGhb",
"premise_still_valid": true,
"summary": "Items 1, 2 and 6 ship whole; item 3 ships for OIDC (state_not_found) and is a reported deviation for SAML, so the PR says Part of #32087. (1) LoginForm refuses an empty or whitespace-only email before any SSO request with the new optional label ssoEmailRequiredText, which both login pages pass. (2) ssoCallbackRefusal.ts maps SELF_REGISTRATION_CLOSED, account_not_linked, saml_error / invalid_saml_response and state_not_found to their own sentences, the generic oauthCallbackFailed only for anything else; code and description sit behind a Technical details toggle. DefaultLoginPage reads no ?error, so one reader. (6) The 16px Radix checkbox drew rounded-sm at 10px (theme --radius 0.875rem); console index.css sets --radius-sm 0.25rem on [role=checkbox] only, measured live 10px before, 4px after. No packages/** metadata moved.",
"line_budget": "22 files, +494 / -19 against origin/main (code: LoginForm.tsx, ssoCallbackRefusal.ts new, two LoginPage.tsx, index.css; 11 locale packs; 5 test files; 1 changeset)",
"files_changed": [
".changeset/32087-sso-login-refusal-sentences.md",
"ui/apps/console/src/index-css-scan-excludes-tests.test.ts",
"ui/apps/console/src/index.css",
"ui/apps/console/src/pages/auth/LoginPage.tsx",
"ui/apps/console/src/pages/auth/tests/LoginPage.error-banner.test.tsx",
"ui/apps/console/src/pages/auth/ssoCallbackRefusal.ts",
"ui/packages/app-shell/src/console/auth/LoginPage.tsx",
"ui/packages/app-shell/src/views/ActionParamDialog.test.tsx",
"ui/packages/auth/src/LoginForm.tsx",
"ui/packages/auth/src/tests/LoginForm.test.tsx",
"ui/packages/i18n/src/tests/de-quote-pairing-3876.test.ts",
"ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt-BR,ru,zh-Hans,zh-Hant}.ts"
],
"tests": "Under os-verify-lock at dc0a73f: vitest over packages/auth, packages/i18n, console pages/auth, console index-css test, app-shell console/auth, app-shell ActionParamDialog and the ADR-0060 product-neutral-renderer ratchet: 213 files passed, 2871 tests passed, 13 skipped, exit 0. type-check auth, i18n, app-shell, console: all exit 0. eslint over every changed file exit 0. Ablations via scripts/ablation-replace.mjs, each restored with git diff HEAD empty: item 2 generic banner back 9 failed / 1 passed; item 1 empty-email guard removed 4 failed / 27 passed; item 6 --radius-sm line deleted 1 failed / 3 passed. Browser: dev boot with OS_SSO_ENABLED=true and one SAML provider, en and zh-CN, 11 shots plus measured landings in /mnt/project-files/ui-orders/32087-shots/.",
"gates": "dispatch-gates --commands at dc0a73f: 38 lines, all under os-verify-lock, 38 exit 0; --ran: 38 derived, 38 run, 0 NOT-MEASURED. check-sdui-manifest exit 0 (in the 38). pnpm --dir ui run check:spec-symbols exit 0; check:new-line-citations exit 0; check:nul-bytes exit 0. First sweep on be5bf7c caught 2 i18n pins (zh-Hant generator output, de quote-pair count), fixed in dc0a73f. CI check-runs on #32129 not yet read (PR just opened).",
"dogfood_battery": "n/a: no ! in the PR title and Clause-② is widening",
"deviations": [
"item 3 SAML: an IdP-initiated SAML refusal answers 302 to the site root with ?error=unsolicited_response, and the root redirect (packages/cli/src/utils/console.ts) sends /_console/ without the query, so the login page gets no signal. Per the order no server code changed; seat to file a domain:server child (land the refusal on the login page, or keep the query through the root redirect). UI side is then one row for unsolicited_response in ssoCallbackRefusal.ts.",
"harness asked for a PR assignee and reviewer via MCP; skipped per os-dev rule 2 and the order (no assignee, no reviewer, no MCP writes)."
],
"mcp_calls": "read-only only (issue and claim reads on #32087); no write tool.",
"api_writes": "2: POST dispatches (relay pr_create, read-back identical, #32129) and this os-dev-report comment via scripts/pm/post-stamped.mjs. Plus 3 git pushes of the branch (52043d2, be5bf7c merge of main, dc0a73f).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: exception: maintainer (the order names this domain:server child) · a refused IdP-initiated SAML sign-in lands on / with ?error=unsolicited_response and the root redirect to /_console/ drops the query, so the user sees no message; ask: land it on the login page like the other SAML refusals, or keep the query through the redirect · dedupe: SAML IdP-initiated refusal landing, root redirect query drop"
]
}
Stamp: 2026-11-10T09:50Z
Generated by Claude Code
objectstack-fleet commentedon Nov 10, 2026
domain:ui#2 · os-support-ai · session_01WGufzge87Pae2h7uSQzX8H
Landed (part) · 2026-11-10T11:01Z
Blocked-by: #32132
cc8c4b0f4e67fcff96a6d685096c2003a460167d(Part of #32087)state_not_found), item 6 (checkboxes keep a small corner)Remaining, and whose:
domain:serverA refused IdP-initiated SAML sign-in lands on the site root and loses its error, so the login page shows nothing #32132 (the refusal lands on/and the root redirect drops?error=); thendomain:uiadds oneunsolicited_responserow tossoCallbackRefusal.ts.pm:dispatched→pm:blocked, assignee cleared.25 remaining items
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
待维护者裁决:第 4、5 项 · 2026-11-13T06:10Z
本卡余下第 4、5 项,两项都要改产品行为,呈维护者裁决。分析经两轮隔离契约档复核(第 4 项 PASS;第 5 项的字母未被推翻,复核意见已吸收)。读数基于
main83bd36de,静态阅读,未实机。第 4 项:退出本平台后,再点 SSO 登录会被直接放行
Governing text:none found(ADR-0135 与
content/docs/permissions/sso.mdx无退出条款);相关裁决 #30967 评论 6718355623(按提供方开关、默认关)。prompt=login四棱:① 长远:主流服务方的单点登出是按连接、可选、默认关(Salesforce SAML 设置的 Single Logout),也有不少 SaaS 不提供(C 是常见选择);两年后形态是 A。② 业务:拉动仅一次内部对接测试,弱。③ 防错:开了必须两种协议都兑现,缺登出端点/地址时保存即拒;接收登出必须验签。④ 不扩散:A 工作量约为 C 的 3 倍以上,可先 C 后 A。
只看①选 A;②③④ 是否翻转:否(只影响时序)。
第 5 项:公司登录系统开通的员工,仍可能拿到本地密码
Governing text:ADR-0135 D5.2(受管身份不持有本地凭据;break-glass 条款保留初始密码与管理员代设)、spec
AuthConfigSchema.ssoOnlyMode描述(SSO-only 下受管用户不持有本地凭据,环境所有者保留应急密码)。四棱:① 长远:主流是「可选设置 + 按用户权限」(Salesforce「禁止用平台凭据登录」+「Is Single Sign-On Enabled」,管理员通常保留密码);D 就是这个形状,并把 spec 已声明的开关变成强制。② 业务:拉动仅一次内部测试,报告人最低诉求是警告,弱。③ 防错:D/E/B 都是服务端响亮拒绝,A 只是界面提示。④ 不扩散:D 不新增配置面。
只看①选 D;②③④ 是否翻转:否。
置信缺口:SSO-only 默认关,默认部署仍是现状(只多警告);D 不追溯已持有本地密码的员工。
Related: #31448, #25399, #30967, #32669, #32188, #32115.
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Maintainer rulings: items 4 and 5 · 2026-11-13T22:19Z
The maintainer answered both decision cards of 6845651657 in the seat thread (2026-11-13 22:19Z).
Item 4 → A: a per-provider "sign out of the company sign-in too" switch, off by default.
Item 5 → D: honour the existing SSO-only switch.
ssoOnlyModedescription), a governed ADR-0135 D5.2 note (kept in step with cloud ADR-0024 D5.2), and a ui card (the warning).The security seat orders the server cards first; the ui cards follow each server half.
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim: PM loop round 4
Session:
session_01L9qibwgMDk9yn3MZdGMqEM(card thread), dispatched bysession_01L9qibwMmGJKaQ52CgHqNu2Account:
os-teslaBranch:
claude/project-thread-zvdozk(objectstack-ai/objectstack)Worktree:
objectstack-sso-only-local-passwordDomain: none (security lane; ruling 6866334319 on this card, item 5)
Seat:
security#2File surface: to be named after the read-only measurement (every door that can give a user a local password, the SSO-only check, the
ssoOnlyModedescription); amended by the seat before any editContainer & model:
M(to measure),mode:thread,model: default tier,effort: xhigh(security, fleet rule 2)Clause-②: no (expected; the
ssoOnlyModedescription text changes, so the seat reviews it)Metadata-protocol: none
Responsibility:
an environment in SSO-only mode gives a managed user no local password | every local-password door, with the managed platform administrator's own break-glass exception | every SSO-only deploymentThread-read: 6866334319
Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 5, and the ui half follows it
Serial constraints cleared: none (item 4 touches sign-out, not password doors)
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim: PM loop round 5
Session:
session_01L9qibz44EJYWFvzJTmGHkg(card thread), dispatched bysession_01L9qibwMmGJKaQ52CgHqNu2Account:
os-teslaBranch:
claude/project-thread-ur0eh0(objectstack-ai/objectstack)Worktree:
objectstack-sso-provider-sign-outDomain: none (security lane; ruling 6866334319 on this card, item 4)
Seat:
security#2File surface: to be named after the read-only measurement (the sign-out path, SSO provider config and its schema); amended by the seat before any edit
Container & model:
L(to measure),mode:thread,model: default tier,effort: xhigh(security, fleet rule 2)Clause-②: yes (expected; widening: a per-provider switch and a logout address; the seat gives the go on the field shapes and runs the contract review)
Metadata-protocol: to measure
Responsibility:
signing out can also end the company sign-in, per provider, off by default | SSO provider config and sign-out for SAML and OIDC | deployments that turn the switch onThread-read: 6866334319
Premise: the maintainer ruled items 4 and 5 (6866334319); this is the server half of item 4, and the ui half follows it
Serial constraints cleared: none (item 5 touches password doors, not sign-out)
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim 6866398944 amended · 2026-11-13T22:26Z: surface named from the card thread's measurement (objectstack
84c66d36). Nopackages/specchange.File surface:
packages/platform-objects/src/identity/sys-sso-provider.object.ts(a per-provider sign-out switch column, an action param, the list column) and the SSO provider actions that take the SAML logout address;packages/plugins/plugin-auth/src/(SSO sign-in options, SAML and OIDC provider bridges, sign-out, OIDC callback) and their tests; generated references; root.changeset/Clause-②: yes (widening: a per-provider switch, off by default, and an optional logout address param)
Metadata-protocol: added
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim 6866387589 amended · 2026-11-13T22:27Z: surface named from the card thread's measurement (objectstack
84c66d36): four doors that can give a user a local password.File surface:
packages/plugins/plugin-auth/src/(the set-initial-password, administrator set-password and both reset-password doors, one shared SSO-only judge beside the cloud-managed one) and their tests;packages/spec/src/system/auth-config.zod.ts(thessoOnlyModedescription text only) and its generated reference; root.changeset/Clause-②: no
Metadata-protocol: none
objectstack-fleet commentedon Nov 13, 2026
security#2 · os-tesla · session_01L9qibz44EJYWFvzJTmGHkg
os-dev-report
{ "issue": 32087, "status": "done", "branch": "claude/project-thread-ur0eh0", "pr": "https://github.com/objectstack-ai/objectstack/pull/33170", "session": "session_01L9qibz44EJYWFvzJTmGHkg", "premise_still_valid": true, "summary": "Item 4: a per-provider switch sys_sso_provider.sign_out_of_idp (off by default, trackHistory), taken by the vendor's /sso/register and /sso/update-provider as signOutOfIdp, with the Edit Sign-In Options param and the list column. On, sign-out also ends the identity provider session: SAML through the vendor's single logout, with enableSingleLogout read per provider and logout messages accepted only when signed; the IdP logout address is the new logoutUrl on the register and edit SAML doors, stored as idpMetadata.singleLogoutService (no new column). OIDC through an RP-initiated logout URL carrying id_token_hint, the end_session_endpoint read from discovery when the switch is saved on and kept in oidc_config. Saving it on is refused 400 VALIDATION_FAILED on signOutOfIdp for a provider that cannot honour it. Surface grep for ruling|Ruling-ref|maintainer|⛔: 14 added lines, every one a code comment citing ruling record 6866334319 or the existing 6718355623 (the file convention); no maintainer or ⛔ hit.", "tests": "New pins: sso-saml-refusal-landing.test.ts (6: switch off keeps the IdP logout endpoint closed and the session stands; on, sign-out answers the IdP logout URL and the session ends; unsigned LogoutRequest and unsigned LogoutResponse each refused; signed redirect-binding message refused; a signed LogoutRequest ends only the named session; SP metadata advertises SingleLogoutService only when on), sso-oidc-refusal-cause.test.ts (3: control off, on with id_token_hint/client_id/post_logout_redirect_uri/state and Location, untrusted callbackURL not passed), sso-provider-lifecycle.test.ts (5: save-time refusals through /sso/update-provider for SAML and OIDC, saving off never refused, logoutUrl via edit and register doors, OIDC endpoint kept), sso-sign-out-of-idp.test.ts (14 unit). Full suites: plugin-auth Test Files 267 passed, Tests 5072 passed | 10 skipped; platform-objects Test Files 121 passed, Tests 1955 passed. Ablation (tests import src): scripts/ablation-replace.mjs set wantLogoutRequestSigned/wantLogoutResponseSigned false in ssoSamlOptions(), anchor x1 -> x0 and blob 21d3b866 -> 1fa01a29 on disk; the unsigned-message pin and the redirect-binding pin went red (2 failed | 4 passed); restored, blob == HEAD and git diff HEAD empty; green on the final run. typecheck (both packages) exit 0; eslint on changed files exit 0.", "gates": "dispatch-gates --commands derived 77; each run with </dev/null under os-verify-lock, exit codes recorded before any pipe. First pass had 4 non-zero: check-plugin-teardown-shape --self-test exit 3 (shallow clone; fixture commit fetched, then 0), check-tenant-audit-census and its --self-test exit 1 (an anonymous adapter type on the new provider-row write; named it SsoProviderRowAdapter, then 0), check:dual-build-cjs-loads exit 3 (dist missing, built by a later gate in the run, then 0). dispatch-gates --ran: 77 derived, 77 run, 0 NOT-MEASURED, 0 UNRUN. CI check-runs on the PR not yet read (draft just opened).", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "read-only only: mcp__github__issue_read and mcp__github__pull_request_read (an exact count was not kept across a context reset); no MCP write tool", "api_writes": "1 — the relay dispatch POST /repos/objectstack-ai/objectstack/dispatches for pr_create (run 43529268119, read-back identical, assignee os-tesla), plus this comment through post-stamped.mjs; pushes to claude/project-thread-ur0eh0 only", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: PR #33170 Acceptance notes · noted, not filed — the Console sign-out following the answered url, and any Edit Sign-In Options / SAML form item the form does not render by itself (the later ui card)", "carrier: PR #33170 Acceptance notes · noted, not filed — @better-auth/sso 1.7.12 limits measured: a signed HTTP-Redirect logout message is refused (fail closed), the outgoing LogoutRequest carries no RelayState, only sessions started while the switch is on are covered, the shared OIDC callback without a provider id is not recorded, an IdP-started logout is not written as a logout audit row" ] }Generated by Claude Code
objectstack-fleet commentedon Nov 14, 2026
security#2 · os-tesla · session_01L9qibwgMDk9yn3MZdGMqEM
os-dev-report
Stamp: 2026-11-14T00:07Z
{ "issue": 32087, "status": "done", "branch": "claude/project-thread-zvdozk", "pr": "https://github.com/objectstack-ai/objectstack/pull/33177", "head": "f3393c48", "session": "session_01L9qibwgMDk9yn3MZdGMqEM", "premise_still_valid": true, "summary": "Item 5 server half (ruling record 6866334319, D). With SSO-only mode on, four doors give an idp_provisioned user no local password: set-initial-password, admin/set-user-password (sys_user#set_user_password), /reset-password and /phone-number/reset-password; each answers 409 RESOURCE_CONFLICT naming SSO-only mode and writes nothing. Exception: a platform administrator setting their own (session isPlatformAdminUser on set-initial; actor = target behind gateAdmin on set-user-password; isPlatformAdminUserId on the resets). One shared judge in plugin-auth sso-only-local-password.ts reuses the cloud-managed source read (readUserSource, moved there) and fails closed as 503. The reset doors judge only a live token or a matching in-budget code, read without spending it. ssoOnlyMode TSDoc + describe say what the server enforces (generated reference regenerated); authentication.mdx gains a short section. Not doors (unchanged): change-password, sign-up, create/import, SCIM, unmounted email-OTP reset.", "door_exception_table": [ "set-initial-password: SSO-only + idp_provisioned caller -> 409; own platform admin (session isPlatformAdminUser) -> allowed; env_native/anonymous -> unchanged", "admin/set-user-password: SSO-only + idp_provisioned target -> 409 (another administrator as target included); target = caller (gateAdmin admits platform admins only) -> allowed; env_native -> unchanged; cloud-managed answers first", "reset-password (emailed): SSO-only + idp_provisioned (holder included) -> 409; platform admin own (isPlatformAdminUserId) -> allowed; unmatched token -> same answer as SSO-only off", "phone-number/reset-password: same as emailed; wrong code -> same answer as SSO-only off" ], "tests": "At f3393c48 under os-verify-lock: plugin-auth whole package vitest 267/267 files, 5072 passed, 10 skipped, exit 0; pins in sso-only-local-password.test.ts (new), cloud-managed.test.ts and password-reset-ends-sessions.test.ts (new top-level describes in the shared harnesses), 77 tests; plus api-error-code.pin + request-json-body-refusal, 157 total in those 5 files. plugin-auth typecheck (with check:test-typecheck) and spec typecheck exit 0 after a full declaration build. Ablation via scripts/ablation-replace.mjs on the committed judge (refuse verdict -> allow, anchor 1 -> 0): 9 failed / 68 passed across the three pin files, restored by the tool, blob equals HEAD, git diff HEAD empty. One existing pin reversed by the ruling (cloud-managed.test.ts 'unmanaged with SSO-only on, both doors answer the idp user as before') now asserts the SSO-only refusal answers there, with a new SSO-only-off control leg.", "gates": "dispatch-gates --commands at f3393c48: 120 derived, all run with </dev/null and exit codes recorded first, 120 exit 0; --ran: 120 derived, 120 run, 0 NOT-MEASURED, 0 UNRUN (list: /mnt/project-files/security-seat-2/r2026-11-13/out/32087-i5-gates-ran-f3393c48.list). spec check:generated: check:docs stale, regenerated with --fix. First sweep found check:dispatcher-error-vocabulary unresolved-constant on the refusal code alias (fixed: literal + equality pin) and api-error-code.pin on a non-literal APIError body (fixed). eslint over changed .ts exit 0. CI check-runs on #33177 not yet read (PR just opened).", "dogfood_battery": "n/a: no ! in the PR title and Clause-② is no with no (narrowing) arm", "ruling_grep": "Surface grep for ruling|Ruling-ref|maintainer|⛔ in the touched files: cloud-managed.ts:7 (cloud ADR-0024 D5.2 / ADR-0135 D5.2 mirror; consistent, untouched); admin-user-endpoints.ts:1073-1075 (ruling C-prime item 2, the forced reset's revocation default; the SSO-only refusal answers before it, untouched); auth-manager-before-hook.ts:420-424, 538, 553, 678 (other doors' rulings, untouched); auth-plugin.ts and auth-manager.ts hits none adjacent to the edits; auth-config.zod.ts hits concern audience posture, not ssoOnlyMode. No hit contradicts this change.", "deviations": [ "The card's newest Claim: line is the sibling item-4 claim (branch ur0eh0), not 6866387589 naming this branch; the seat confirmed in the seat thread that this branch may push.", "The harness asked for an assignee and reviewer via MCP; skipped per the frame and os-dev rule 2. The only assignee write is assignees [os-tesla] on the relay pr_create; read-back lists os-tesla.", "Merged origin/main once before opening (one docs-only commit); no rebuild needed for it." ], "remainders": [ "ui card: the administrator set-password warning", "governed ADR-0135 D5.2 note, kept in step with cloud ADR-0024 D5.2 (seat)", "the #32115 local-password remainder and the question of managed users who already hold a local password" ], "mcp_calls": "0", "api_writes": "2: POST relay pr_create (#33177, read-back identical, 11649 bytes) and this os-dev-report comment via post-stamped. Plus git pushes of the branch.", "open_questions": [], "security_findings": "withheld", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 14, 2026
security#2 · os-tesla · session_01L9qibwMmGJKaQ52CgHqNu2
Claim 6866387589 amended again · 2026-11-14T01:15Z: the declaration follows contract review 6868317289. With SSO-only on, the local-password doors now refuse a managed user where they answered before, and two additive members widen the surface. The surface is unchanged.
Clause-②: yes (narrowing)
Metadata-protocol: none
objectstack-fleet commentedon Nov 14, 2026
director#1 · hotlong · session_f4c42f815d5e47b39de5e9fb0d6d7864
2026-11-14T04:35Z One more measured case:
EMAIL_DOMAIN_NOT_ALLOWEDstill shows the generic "link expired" headline, on 18.4.0.Where. Staging test1016
os-4e4qa4, Enterprise plan, cloud PR #4415 head153d10ca(objectstack 18.4.0). The provider is a real Okta OIDC app (okta, domainobjectlab.aiverified, JIT on).What happened. An Okta user whose email is
qa.sub@dev.objectlab.ai(a subdomain, so unlisted) used the login page's 「Sign in with SSO」.[sso] account creation refused: the address is not at a domain the provider has proven {"code":"EMAIL_DOMAIN_NOT_ALLOWED","providerId":"okta","method":"sso-oidc"}, and no account was created./_console/login?error=EMAIL_DOMAIN_NOT_ALLOWED&error_description=With SSO domain verification on, an identity provider creates an account only for an email address at a domain it has proven. Nothing was created.Why.
ui/apps/console/src/pages/auth/ssoCallbackRefusal.tshas its own sentences forSELF_REGISTRATION_CLOSED,account_not_linked,saml_error/invalid_saml_responseandstate_not_found. Every other code falls back tooauthCallbackFailed.EMAIL_DOMAIN_NOT_ALLOWEDalready has an end-user sentence on the sign-up side (signUpRefusalMessages.ts:65,auth.register.errors.emailDomainNotAllowed), but the SSO callback mapping does not use it.Impact. "Please try again" sends the person into a loop that can never succeed. The refusal is about their email domain, which only their administrator can change.
Suggested sentence (the decision on wording stays with this card): something like "Your email address isn't at a domain this sign-in is set up for. Ask your administrator."