Skip to content

feat(spec): declare features.sso on the auth config response - #29455

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ntfxyn
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ntfxyn

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29344

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — git grep for features.sso and auth/config under skills/ finds 0 files; content/docs/permissions/sso.mdx already tells readers that /auth/config serves features.sso when SSO is configured, which stays true; the API reference page is regenerated in this diff

维护者速读

  • 改了什么:spec 的 GET /api/v1/auth/config 响应契约 AuthFeaturesConfigSchema 新增可选布尔键 sso(服务端早就在发、登录页早就在读,只是契约没声明)。
  • 为什么改:按契约解析响应的客户端会把这个键静默丢掉,看不到「用 SSO 登录」入口;声明后与服务端实发一致。
  • 风险与回滚:纯加宽,以前能解析的响应原样解析;没有默认值,不发就仍是不存在;运行时代码一行未动;回滚即撤掉此 PR。
  • 先看:packages/spec/src/api/auth-endpoints.zod.ts(新键与说明)、packages/spec/src/api/auth-endpoints.test.ts(新测试)、.changeset/29344-auth-config-features-sso.md。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/auth-endpoints.test.ts,预期 31 个测试全部通过。
  • 需要你做的:确认新公开键名 sso(PR 上挂 needs-user-decision);其余席位落地。

What changes

Producer readings (origin/main 96fb7b5d):

  • packages/plugins/plugin-auth/src/auth-manager.ts getPublicConfig() sets sso: this.isSsoWired() (the coarse "plugin wired" flag).
  • Both HTTP doors that answer GET /auth/config refine it to isSsoUsable() before answering: plugin-auth's /config route (auth-plugin.ts, if (config.features?.sso) config.features.sso = await isSsoUsable()) and the hono adapter's auth route (packages/adapters/hono/src/index.ts, same refinement). git grep getPublicConfig over packages/ finds no third door. So a client always sees the usable answer; the unrefined value is read only inside the server (the boot sign-in reachability check, auth-plugin.ts), which no client sees. No producer disagreement a client can observe, so no domain:server finding.

Classification: PUBLIC_AUTH_FEATURES.sso (packages/spec/src/kernel/public-auth-features.ts) is surface: 'login', opt-in, exempt with the reason "the served value is refined to usable via isSsoUsable()": it agrees with the new describe. No pin ties the registry's keys to AuthFeaturesConfigSchema's keys (the plugin-auth drift guard ties the registry to the served booleans, not to the schema), so nothing there moved. See Acceptance notes.

No liveness row. packages/spec/liveness/ governs metadata types; no ledger names this API response (git grep AuthFeaturesConfig packages/spec/liveness finds nothing), the same as for cloudManaged.

No forced test edits. No existing test was changed.

Why

GET /api/v1/auth/config serves features.sso and the console's login form reads it (ui/packages/auth/src/LoginForm.tsx, config?.features?.sso === true, through a hand-typed sso?: boolean in ui/packages/auth/src/types.ts), as does the mobile shell per the card. The contract did not declare it, so a client parsing through GetAuthConfigResponseSchema dropped it silently.

One correction to the card's premise text: ui/packages/app-shell/src/runtime-config.ts's body.features.sso === true reads GET /api/v1/runtime/config (the plan-entitlement flag the cloud-connection RuntimeConfigPlugin passes through), a different response. The premise still holds through the LoginForm reader above.

Tests

packages/spec/src/api/auth-endpoints.test.ts, new block AuthFeaturesConfig.sso (served, now declared), 8 cases:

  • a served sso: true and sso: false parse and are kept;
  • the whole GetAuthConfigResponseSchema parse keeps features.sso beside ssoEnforced;
  • CONTROL: an absent sso stays absent (no default is invented);
  • a non-boolean sso ("yes", 1, null) is refused with exactly one invalid_type issue at path sso, expected boolean;
  • the describe names isSsoUsable and ssoEnforced and carries no tracker id.
Local readings (head 0d81446)
  • Build and regen: pnpm --filter @objectstack/spec build under os-verify-lock, then check:generated exit 1 naming one stale artifact (check:docs); gen:docs VERDICT 0; committed with the source.
  • pnpm --filter @objectstack/spec test: VERDICT command-exit 0, Test Files 791 passed (791), Tests 23474 passed, 1 todo. src/api/auth-endpoints.test.ts + src/kernel/public-auth-features.test.ts: 57 passed.
  • pnpm --filter @objectstack/spec typecheck: VERDICT 0 (test layer compiles; 131 pinned signatures held).
  • Consumer closure: turbo run build --concurrency=2 --filter='./packages/**' VERDICT 0 (78 tasks, 0 cached).
  • @objectstack/client typecheck (it re-exports AuthFeaturesConfig / GetAuthConfigResponse): VERDICT 0. Reverse check that the consumer reads the rebuilt declarations: a probe file in packages/client typing { sso: true } as AuthFeaturesConfig from @objectstack/spec/api compiles (exit 0), and { sso: 'yes' } is refused TS2322 (exit 2). Probe deleted.
  • @objectstack/plugin-auth full suite (the producer, incl. the /config route and isSsoUsable tests): VERDICT 0, Test Files 211 passed (211), Tests 3643 passed, 10 skipped.
  • hono adapter (hono-auth-mount-basepath, hono-head-dispatched-as-get, hono-thrown-message-disclosure, the files that drive its /auth/config door): Test Files 3 passed (3), Tests 33 passed.
  • ui, from ui/: packages/auth/src/__tests__/LoginForm.test.tsx (the real /auth/config reader) + packages/app-shell/src/runtime-config.test.ts (named by the card): Test Files 2 passed (2), Tests 85 passed.
  • Ablation: sso renamed to ssoAblated in the schema with scripts/ablation-replace.mjs (anchor 1 to 0, blob fb278750 to 8c4aae77): auth-endpoints.test.ts exit 1, 7 failed, CONTROL green as expected. Restored: blob equal to HEAD fb278750, git diff HEAD empty.
  • Dogfood battery: n/a, no ! in the title and no (narrowing) arm.
Gates (dispatch-gates --commands)
  • At head 0d814466: 103 derived commands, all 103 exit 0 (each with stdin closed, exit captured before any pipe). node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3 (PREREQUISITE NOT MET: its pinned fixture commit was outside the shallow clone); after git fetch --depth=1 origin 621a4876… it exited 0 (48 cases).
  • dispatch-gates --ran: 103 derived, 103 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN.
  • Before the gates: turbo run build --concurrency=2 --filter='./packages/**' under os-verify-lock, VERDICT 0 (78 tasks).
  • Offline PR-body checks with this body: check-changeset-no-major.mjs --base origin/main --event exit 0 (level axis yes (widening) + added, spec minor); check-partof-closing-keyword.mjs exit 0.
  • Scoped lint: pnpm exec eslint --no-inline-config over the two changed .ts files, exit 0.
  • Local-only families: check:skill-frame-freshness exit 0; check:dev-prereqs exit 1 because 35 of 106 workspace packages (the ui/ tree among them; only packages/** was built here) have no dist in this container: a machine-state reading, not a finding of this diff (packages/spec dist is fresh).
  • CI: the seat reads the PR's check-runs.

Acceptance notes

  • Same-class drift, larger than this card. getPublicConfig().features serves at least nine more keys AuthFeaturesConfigSchema does not declare: multiOrgEnabled, tenancyPosture, audiencePosture, degradedTenancy, oidcProvider, deviceAuthorization, admin, termsUrl, privacyUrl (most are classified in PUBLIC_AUTH_FEATURES or PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS, and several have readers, e.g. deviceAuthorization in the console's device-auth page). A contract parse strips them all. No pin ties the registry to the schema keys. Reported to the seat as one family card, not filed here.
  • ui/packages/auth/src/types.ts keeps a hand-written mirror of the features type (its sso comment says "wired", while the served value is "usable"). Not touched; noted.

Verification

See the readings above. The PR stays a draft; the seat runs the contract review, and the new public key name sso goes to the maintainer as needs-user-decision.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (scoped to the changed files locally; the repo-wide run is CI's).
  • 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.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

AuthFeaturesConfigSchema gains the optional boolean `sso` that
GET /api/v1/auth/config already serves (refined to "usable" by
isSsoUsable) and the console login form reads.

Claude-Session: https://claude.ai/code/session_01ADHkmPzZFPzq2roY9uo1me
Co-authored-by: Claude <noreply@anthropic.com>
…ce for features.sso

Claude-Session: https://claude.ai/code/session_01ADHkmPzZFPzq2roY9uo1me
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 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 — 168 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 aca58cdb62bdb8ed350f0b45885728374ca1f6ad → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6e016402d734596609e6ed858ac6a4b0d0c7e6bd — the merge of head 0d814466bcee1734d0ae90b71458f7ed412c5784 into base aca58cdb62bdb8ed350f0b45885728374ca1f6ad, 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 6e016402d734596609e6ed858ac6a4b0d0c7e6bd && git checkout 6e016402d734596609e6ed858ac6a4b0d0c7e6bd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aca58cdb62bdb8ed350f0b45885728374ca1f6ad 0d814466bcee1734d0ae90b71458f7ed412c5784 && git checkout -B drift-repro aca58cdb62bdb8ed350f0b45885728374ca1f6ad && git merge --no-ff 0d814466bcee1734d0ae90b71458f7ed412c5784

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract-review-claim: session_01ADHkmSduNGC2SZXszyrNut · head 0d81446 · 2026-11-02T15:45Z

The lane seat reviews this head itself at CONTRACT_REVIEW_TIER (triage ruling 6575141496 on #28741 returned contract review to the lane): a widening of AuthFeaturesConfigSchema (optional boolean sso on the GET /api/v1/auth/config response).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

待裁:新公开键名 features.sso(#29344)

  • 一句话问题:GET /api/v1/auth/config 早就在返回 features.sso,登录页也靠它决定是否显示「用 SSO 登录」,但 spec 契约没声明,按契约解析的客户端会把它丢掉。这个 PR 在 AuthFeaturesConfigSchema 上声明它(可选布尔、无默认),键名需要您定。
  • 背景:服务端 getPublicConfig() 和两个 /auth/config 入口(plugin-auth 路由、hono 适配器)都发这个键;读方是 ui/packages/auth 的 LoginForm 和移动端。先例是 features.cloudManaged(spec: declare the console flag features.cloudManaged — a PUBLIC_AUTH_FEATURES entry with a hide-when-on gate semantics, and AuthFeaturesConfigSchema declares it (child of #25083, ruling 6341690661 Q3 A) #25292):同样是"已发未声明",按实发的名字补声明。
  • 选项:
    • A 保留 sso:与服务端实发、登录页实读、PUBLIC_AUTH_FEATURES.sso 注册表完全一致,零额外工作,任何现有客户端不受影响。
    • B 改名(例如 ssoUsable 或 ssoAvailable):名字更直说"可用",与 ssoEnforced 区分更明显;但服务端、两个入口、登录页、移动端和注册表都要跟着改,是一次跨 server/ui 的更名,已部署的旧客户端读不到新名,约半天到一天,且需要过渡期双发。
  • 推荐:A。说明文字已经写清它是"可用"答案(isSsoUsable),也写清与 ssoEnforced 的差别,名字本身不再承担这层意思。盲区:如果将来想把"已接线"和"可用"分成两个公开键,sso 这个名字会显得含糊;那时再加 ssoWired 一类的新键即可,不需要改 sso。
  • 自检:只看①选 A;②③④ 是否翻转:否。
  • 落地:在此 PR 标签取下前,席位不挂自动合并。

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0d814466bcee1734d0ae90b71458f7ed412c5784
Local-runs: none
Scope: packages/spec/src/api/auth-endpoints.zod.ts + packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/adapters/hono/src/index.ts, packages/spec/src/kernel/public-auth-features.ts, packages/plugins/plugin-auth/src/public-feature-registry.test.ts, packages/client/src/index.ts, ui/packages/auth/src/LoginForm.tsx, ui/packages/auth/src/types.ts

Inputs read: card #29344 (body and all five comments, rulings and the os-dev-report 6581383101 included), PR #29455 (body, five-file list, net diff against merge base 96fb7b5d on main), the check-runs on the head, the lane's delta, and the cited code in the live tree at 0d814466 through git show. The rest of the diff (.changeset/29344-auth-config-features-sso.md, content/docs/references/api/auth-endpoints.mdx, packages/spec/authorable-surface/api.json, packages/spec/src/api/auth-endpoints.test.ts) is the lane seat's ACCEPT at TIER_DEFAULT; it is read here only to judge the schema file.

① Derived judgments

  • Accept set, AuthFeaturesConfigSchema: gains sso: z.boolean().optional(), no default. The schema is a plain z.object, so before this head a served sso was stripped silently; after it, true/false are kept, absent stays absent, and a non-boolean is refused with one invalid_type issue at ['sso']. Every payload accepted on main is accepted unchanged, so the change is a pure widening. Right.
  • Public surface: AuthFeaturesConfig, AuthFeaturesConfigParsed, GetAuthConfigResponse, GetAuthConfigResponseParsed gain sso?: boolean; @objectstack/client re-exports AuthFeaturesConfig and GetAuthConfigResponse (packages/client/src/index.ts:8644,8647) and so publishes the same widening. No export is added, removed or renamed. Right.
  • Generated artifacts: authorable-surface/api.json gains the one row api/AuthFeaturesConfig:sso; authorable-defaults/api.json is untouched, which is right because the key carries no default; api-surface, declaration-map, json-schema.manifest name exports, not keys, and are untouched. The reference page content/docs/references/api/auth-endpoints.mdx is regenerated in both tables and the features summary row. Lint & Repo Gates (check:generated, check:doc-authoring, the authorable-surface baseline) concluded success. Right.
  • No liveness row: packages/spec/liveness/ ledgers metadata types; no ledger names AuthFeaturesConfig; Spec property liveness concluded success. Same as the cloudManaged precedent (feat(spec, platform-objects): declare features.cloudManaged with a hide-when-on gate, and hide the six doors that add people while it is on #25309). Right.
  • public-auth-features.ts untouched: its sso entry (surface: 'login', semantics: 'opt-in', exempt because the served value is refined to usable via isSsoUsable()) already agrees with the new describe. The only pins on the registry are plugin-auth's drift guard (public-feature-registry.test.ts: served boolean keys ≡ PUBLIC_AUTH_FEATURE_NAMES, non-boolean keys ⊆ PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS), platform-objects' feature-gate-guard.test.ts (registry ≡ gated inputs) and spec's public-auth-features.test.ts (the 12 names). None reads AuthFeaturesConfigSchema.shape, so nothing there moves. Right.
  • Gate verdicts on the head (read 2026-11-02T15:54Z): success — Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset (both runs), Spec property liveness, Live E2E UI, Build Docs, Build UI Deps, Dogfood Regression Gate, Type Check · source gates, Type Check · debt ledger, every Lint · sub-job, PR-body guards, Check Documentation Links, Flag docs affected by code changes. in_progress at that stamp, not a pass: Build Core, Test Core (8 shards), Type Check UI, Test UI (16 shards), Build & E2E UI, Type Check · workspace, Type Check · consumer gates. No check concluded failure. The seat reads the required set green before the queue; this record does not stand in for it.

② Semver level

.changeset/29344-auth-config-features-sso.md: @objectstack/spec minor, carrying Clause-②: yes (widening) and Metadata-protocol: added; the PR body carries the same two lines. The diff adds one optional key to a published response schema and moves the authorable-surface/ baseline, which is exactly the added + minor pairing check-changeset-no-major.mjs requires (a declared kind must grade at least one package whose published source it moves minor or above; added beside Clause-②: yes is consistent). The precedent's spec changeset (.changeset/25292-features-cloud-managed.md, PR #25309) carries the identical level and axes. No BREAKING banner is owed: nothing is narrowed, removed or renamed. Check Changeset concluded success twice. Clause-②: yes (widening) — matches the diff.

③ Boundary flags

Dev deviations (os-dev-report 6581383101), each answered:

  1. Consumer test set differs from the delta — accepted, and the correction is right. ui/packages/app-shell/src/runtime-config.ts fetches GET /api/v1/runtime/config (:6, :442) and its sso?: boolean (:69) is the plan-entitlement flag, a different producer; the real /auth/config reader is ui/packages/auth/src/LoginForm.tsx:317. The card's premise still holds through that reader and the mobile shell.
  2. public-auth-features.ts not touched — accepted; confirmed in ① (no pin ties registry keys to schema keys).
  3. No liveness row — accepted; confirmed in ①.
  4. PR body opens with the harness attribution comment before Fixes #29344 — accepted; the PR-body guards check concluded success.
  5. Assignee/reviewer not set at pr_create — accepted; the seat's write, not the dev's.

open_questions:

  1. Maintainer nod on the public key name sso — already escalated (comment 6581452838, needs-user-decision on the PR). Judged here, not decided here: the name is what the producer serves (auth-manager.ts:4594), what PUBLIC_AUTH_FEATURES.sso classifies, what LoginForm.tsx:317, ui/packages/auth/src/types.ts:290 and the mobile shell read, and what content/docs/permissions/sso.mdx:120 documents; the describe, not the name, carries the "usable" meaning. The seat holds the queue until the label is lifted.
  2. Close the drift class with a guard — escalate to the seat as one family card (option A). Confirmed reading below under (c). Not this PR's scope; filing it here would expand the card.

out_of_scope_findings: (class b) the nine served-but-undeclared keys — confirmed, see (c); the ui/packages/auth/src/types.ts prose drift — confirmed, see (b). The card's own premise text (runtime-config.ts:475) is corrected by deviation 1 above. Docs Drift Check (6581388723): one changed file yielded no anchor (authorable-surface/api.json); the hand-written page that restates the flag, content/docs/permissions/sso.mdx:120 ("so a client can show an enterprise-login button when SSO is configured"), stays true under the "usable" describe.

Three readings the brief asked for:

  • (a) Every client-observable producer of features.sso serves what the describe says. git grep getPublicConfig over packages/ and ui/ at the head finds three callers outside tests: plugin-auth's /config route (auth-plugin.ts:3103-3109: if (config.features?.sso) config.features.sso = await isSsoUsable()), the hono adapter's /auth/config arm (packages/adapters/hono/src/index.ts:983-989, same refinement, guarded by typeof isSsoUsable === 'function'; the kernel's auth service is the AuthManager itself, auth-plugin.ts:771, so the guard never drops the refinement in this repo), and the runtime discovery producer (packages/runtime/src/http-dispatcher.ts:2083, readAuthFamilies, which reads features.admin only and serves no sso). isSsoWired() (auth-manager.ts:4659) is plugins.sso ?? OS_SSO_ENABLED ?? false; isSsoUsable() (:4692) returns false when not wired, true when no data engine, whether count('sys_sso_provider') is above zero otherwise, and true when the count throws or is not a number. So every client sees: false when off or on with no provider row, true when wired with a row or when rows cannot be counted. That is the describe word for word ("usable", "false when SSO is off, or on with no provider yet", "falls open to the wired flag when providers cannot be counted"). The two unrefined readers (auth-plugin.ts:1417 boot sign-in reachability; auth-plugin.ts:911 Setup-nav gate via isSsoWired()) are server-internal and never serialised to a client. No producer disagreement a client can observe; no domain:server finding.
  • (b) ui mirrors of AuthFeaturesConfig. ui/packages/auth/src/types.ts:243-390 (AuthPublicConfig.features) declares sso?: boolean at :290 — the type agrees with the new key; its doc comment (:284-289, "wired on the server. When falsy the /sign-in/sso route isn't mounted") is false for the served false that means "wired, no provider yet" (the route IS mounted then). Prose drift only, no type drift — should-fix for the ui lane, not this PR (ui/ carries its own rules and is outside hits). ui/packages/app-shell/src/runtime-config.ts:69 is a different payload (see deviation 1), not a mirror. ui/packages/auth/README.md:377 ("features.sso gates the Sign in with SSO button") is consistent. packages/plugins/plugin-auth/src/boot-sign-in-reachability.ts:414 (features?: { sso?: boolean; ssoEnforced?: boolean }) is a structural server-side view of the unrefined object, correctly so. No other hand-typed mirror found.
  • (c) The nine undeclared served keys and the missing pin — confirmed. getPublicConfig().features at auth-manager.ts:4565-4624 serves, beyond the schema's keys, five booleans — multiOrgEnabled, degradedTenancy, oidcProvider, deviceAuthorization, admin (all classified in PUBLIC_AUTH_FEATURES) — and four values — tenancyPosture, audiencePosture, termsUrl, privacyUrl (all in PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS). AuthFeaturesConfigSchema at the head declares twoFactor, organization, sso, ssoEnforced, phoneNumber, phoneNumberOtp, cloudManaged, turnstileSiteKey plus the two retired tombstones. The dev's list of nine is exact. At least deviceAuthorization has a live reader (ui/apps/console/src/pages/auth/DeviceAuthPage.tsx:77), so a contract parse would hide a real surface. One more for the family card: the top-level devSeedAdmin (auth-manager.ts:4634, development only) is also undeclared on GetAuthConfigResponseSchema. No pin compares the registry's key set, or PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS, to AuthFeaturesConfigSchema's live keys (the three pins are listed in ①). Escalate: one family card for the seat — declare the nine and add that pin.

④ Design judgment

packages/spec/src/api/auth-endpoints.zod.ts — judged against the ruling it implements (card #29344 and triage 6577724749: declare the served key; the delta: say which producer value is served and how it differs from ssoEnforced), against the producer in the live tree, and against its first consumers (LoginForm.tsx:317, @objectstack/client).

Holds:

  • The describe's four factual claims each match the cited code: "wired AND at least one SSO provider is configured" (isSsoUsable, counting sys_sso_provider rows; the object declares no enabled column, so a row is a configured provider); "false when SSO is off, or on with no provider yet"; "GET /auth/config refines it before answering" (both doors, (a) above); "falls open to the wired flag when providers cannot be counted" (the no-engine and catch branches both return true). The contrast with ssoEnforced matches that key's own describe and LoginForm.tsx:284-291.
  • "Absent means not usable" matches the first consumer exactly: LoginForm renders the SSO button only on === true, hidden by default.
  • .optional() with no default: the producer always serves the key, but so it does for ssoEnforced, phoneNumber, phoneNumberOtp and cloudManaged, all declared .optional() without a default; a .default(false) would invent a wire value and change AuthFeaturesConfigParsed. Consistent with its siblings and the registry's opt-in semantics. Hold.
  • The tracker id lives in the adjacent // comment only; the describe carries none (the new test pins not.toMatch(/#\d{3,}/), and check:doc-authoring concluded green inside Lint & Repo Gates). Hold.
  • Prime Directive 1/2: Zod source, no runtime logic. Key casing camelCase. Hold.
  • Tried to break the refusal test's shape: the refusal case expects exactly one invalid_type issue with expected: 'boolean' at ['sso'] for 'yes', 1 and null; zod 4 reports null as invalid_type expected boolean, so the assertion is sound (dev reading: 31 passed, 7 of them red under the ssoAblated ablation with the CONTROL green; CI Test Core pending at the stamp).

Tried and did not break:

  • A third /auth/config door serving the unrefined flag: none exists (git grep over packages/ and ui/; the hono guard is inert against the in-repo service).
  • A ui type mirror disagreeing on the key's type: none (sso?: boolean at ui/packages/auth/src/types.ts:290).
  • A consumer parsing through GetAuthConfigResponseSchema.strict() or a pin on the schema's key set that would now go red: none found.

Observations, not findings on this file: the hono door's refinement has no test of its own (hono-head-dispatched-as-get.test.ts:157 stubs sso: false straight through); the plugin-auth door's is covered in auth-manager.test.ts. Cited code, not reviewed here.

Findings: must-fix — none. Should-fix — (1) ui/packages/auth/src/types.ts:284-289 comment says "wired" for a value that is served as "usable" (ui lane, not this PR); (2) the family card for the nine undeclared keys plus a registry-to-schema pin (seat to file; the key named here that has a live reader is deviceAuthorization).

Implemented-by: claude/project-thread-ntfxyn
Reviewed-by: session_01ADHkmSduNGC2SZXszyrNut

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Head-sha: 0d814466bcee1734d0ae90b71458f7ed412c5784 · read 2026-11-02T16:12Z

维护者速读

  • 改了什么:GET /api/v1/auth/config 的响应契约 AuthFeaturesConfigSchema 新增可选布尔键 sso(无默认)。服务端早就在发、登录页早就在读,只是契约没声明。
  • 为什么改:按契约解析响应的客户端会把这个键静默丢掉,看不到「用 SSO 登录」入口;声明后契约与服务端实发一致。
  • 风险与回滚:纯放宽,以前能解析的响应原样解析;运行时代码一行未动。回滚就是 revert 本 PR。
  • 先看:packages/spec/src/api/auth-endpoints.zod.ts 的 sso 与它的说明文字。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/auth-endpoints.test.ts,预期全部通过(新增 8 项)。
  • 需要你做的:键名 sso 待总监裁定(needs-user-decision,分析见 6581452838)。裁定保留后席位落地。

席位意见:可落地,只等名字。契约复审在现 head 判 PASS(6581701011),CI 全绿。

席位核对 — ACCEPT · domain:spec#2 · head 0d814466

  1. Shape: draft, base main, Fixes #29344. Clause-②: yes (widening) and Metadata-protocol: added sit at line start in the body and the changeset (@objectstack/spec minor). 5 files, +73/−1, inside claim 6580165313. No governed path.
  2. Premise measured: on main a GetAuthConfigResponseSchema parse strips a served features.sso; on the head the new block keeps true/false, keeps absent absent, and refuses a non-boolean with one invalid_type at sso. The dev's ablation turns 7 red, the control stays green.
  3. Gates: ci-failure.mjs --sha 0d814466… GREEN: 65 check-runs, none failed.
  4. Accepted deviations: the delta's assumption that ui/packages/app-shell/src/runtime-config.ts reads /auth/config was disproved (it reads /api/v1/runtime/config); the real ui reader is ui/packages/auth LoginForm, tested. The premise holds through it.
  5. Out of scope: GET /auth/config serves nine features keys the spec does not declare, and nothing pins the feature registry to the schema #29473 (nine more served features keys undeclared, no pin from PUBLIC_AUTH_FEATURES to the schema keys; plus the ui types.ts comment that says "wired" for a "usable" value).
  6. Landing: not Tier H and under 3000 lines. It lands only after the name ruling: if sso stays, the seat removes needs-user-decision and lands with pr_ready and auto-merge; after merge, a landing note on spec: declare features.sso on the auth config response, which is served and read but never declared #29344 and pm:dispatched removed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

Withdrawn: the name question on this PR. The pm-dispatch skill carries no rule that a new public name needs the maintainer's approval; that line lived only in this seat's standard order. Under SKILL.md 〈升级与决策〉 a name escalates only when the options truly diverge on product semantics or contract shape and no existing norm decides; here the dev's name is fit and no fork exists, so it is the seat's call: the name stays as built. needs-user-decision is removed and the seat lands this PR on its ACCEPT.

Prompted by the maintainer's question at 2026-11-02T17:03Z.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 17:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    @objectstack/spec:test:      ✓ inverts the card's measured control: the three bogus keys now FAIL parse instead of adding zero warnings  353ms
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1293 object site(s) across…' to be 'untriaged: 1292 object site(s) across…' // Object.is equality
    
  • Test UI (15/16) — 失败步骤: Run the ui tests (shard 15/16)

    × a refused report save makes a reload and the pillar link ask; a save that lands clears it 1992ms
    FAIL   dom  packages/app-shell/src/views/studio-design/StudioDesignSurface.refusedLeafUnsaved-29113.test.tsx > a leaf edit whose save was refused guards every departure (objectstack#29113) > a refused
      ↳ 失败原因: AssertionError: expected false to be true // Object.is equality
    

↳ 失败原因 是判读的关键:超时(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 队列共有 6 个失败构建(不含本次)。

分诊清单:

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

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

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

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

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

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

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

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

    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > is checked in current — every shard on disk equals a fresh render, and no
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected Map{ …(14) } to deeply equal Map{ …(14) }
    @objectstack/spec:test:repo:  FAIL   repo  scripts/strictness-ledger-doc.test.ts > the ledger and the generated counts agree > the read-time totals equal the sums of the shards' own rows
      ↳ 失败原因: @objectstack/spec:test:repo: AssertionError: expected 'untriaged: 1297 object site(s) across…' to be 'untriaged: 1296 object site(s) across…' // Object.is equality
    

↳ 失败原因 是判读的关键:超时(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 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 16 个失败构建(不含本次)。

分诊清单:

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

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

Merged via the queue into main with commit aee9c47 Nov 2, 2026
74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ntfxyn branch November 2, 2026 18:18
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/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: declare features.sso on the auth config response, which is served and read but never declared

1 participant