Skip to content

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

Closed

Description

@objectstack-fleet

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Filing category: ① product defect with a named producer.

What is wrong

GET /api/v1/auth/config serves features.sso, and two clients read it. The spec's response contract does not declare the key. A client written from the contract cannot see the SSO sign-in capability, and a zod parse through GetAuthConfigResponseSchema (a plain z.object) strips it silently.

Readings, objectstack main at 27715752:

  • Producer:
    • packages/plugins/plugin-auth/src/auth-manager.ts:4594 sets sso: this.isSsoWired().
    • packages/plugins/plugin-auth/src/auth-plugin.ts:3109 sets config.features.sso = await this.authManager!.isSsoUsable().
  • Classified as a served login feature: packages/spec/src/kernel/public-auth-features.ts:188, sso: { surface: 'login', … }.
  • Contract: packages/spec/src/api/auth-endpoints.zod.ts, AuthFeaturesConfigSchema, declares twoFactor, organization, ssoEnforced, phoneNumber, phoneNumberOtp, cloudManaged and turnstileSiteKey. It has no sso.
  • Consumers:

reach: named real producer (above). Precedent with the same shape: #25292 declared features.cloudManaged after it was served but undeclared.

Business reason

  • Who benefits: every client that renders a login entry from this payload. That is the Console and the mobile shell today.
  • Scenario: a deployment with SSO configured. A client built from the contract does not know the SSO button exists. A future client that parses through the schema drops the flag and hides SSO sign-in.
  • If not done: the published contract under-states a live, consumed capability. That is the declared-vs-enforced drift the contract-first rule exists to stop.

Who acts

The domain:spec seat. It adds sso: z.boolean().optional().describe(…) beside ssoEnforced, describing it as "usable (≥1 provider configured)", the refinement isSsoUsable() applies. The narrowing or widening class (Clause-②) is the spec seat's call.

Duplicate check

Three searches covered open and closed cards in objectstack-ai/objectstack, run on 2026-11-02:

Query Hits Exact duplicates
is:issue AuthFeaturesConfigSchema sso 5 0 (#25292 is the cloudManaged precedent)
is:issue "features.sso" 12 0
is:issue "auth/config" sso schema 66 0

Found by the os-dev review of objectstack-ai/mobile#154.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p1 · domain:spec · pm:queue · 2026-11-02T12:58Z

    ⛔ Not a claim.

    • Read fresh. GET /auth/config serves features.sso and two clients read it, but AuthFeaturesConfigSchema does not declare it, so a contract parse strips it. Declaring it is a protocol addition, and protocol additions are p1 by standing ruling. The name is already fixed by the producer and its readers; the maintainer's nod on the key is recorded before merge.

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Claim: PM loop round 2 · 2026-11-02T14:50Z
    Session: session_01ADHkmSduNGC2SZXszyrNut (seat); dev = card thread session_01ADHkmPzZFPzq2roY9uo1me
    Account: os-steve
    Branch: claude/project-thread-ntfxyn
    Worktree: the card thread's own checkout of objectstack-ai/objectstack
    Domain: domain:spec
    Seat: domain:spec#2
    File surface: packages/spec/src/api/auth-endpoints.zod.ts (AuthFeaturesConfigSchema gains sso) and its test; packages/spec/src/kernel/public-auth-features.ts only if a pin there ties the classification to the schema keys; the liveness row for the key if the auth config response is ledgered; regenerated references; root .changeset/ (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default — the tier is the PM's per-card call (floor sonnet · default opus · ceiling fable); a widening of a served response schema, one CONTRACT_REVIEW_TIER review by the seat before the queue, effort: high
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: packages/spec auth-endpoints.zod.ts does not declare features.sso, which plugin-auth serves and the Console and the mobile shell read | none: a contract parse strips it | every client that renders a login entry from GET /api/v1/auth/config
    Thread-read: 6577724749
    Premise: holds (on origin/main 96fb7b5d, AuthFeaturesConfigSchema declares ssoEnforced and no sso); the dev measures first and stops with premise_still_valid: false if it does not reproduce
    Serial constraints cleared: no in-flight claim names auth-endpoints.zod.ts or public-auth-features.ts

    The key name sso is the producer's and its readers'; as a new key on a published contract it goes to the maintainer as needs-user-decision on the PR (Steve's rule, 2026-11-02 14:39Z).

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmPzZFPzq2roY9uo1me
    os-dev-report

    {
      "issue": 29344,
      "status": "done",
      "branch": "claude/project-thread-ntfxyn",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29455",
      "session": "session_01ADHkmPzZFPzq2roY9uo1me",
      "premise_still_valid": true,
      "summary": "AuthFeaturesConfigSchema gains optional boolean `sso` beside ssoEnforced, no default; the describe says it is the usable answer (isSsoUsable: plugin wired AND >=1 provider), names that both GET /auth/config doors (plugin-auth /config route, hono adapter) refine the coarse wired flag before answering, and contrasts ssoEnforced. Regenerated authorable-surface/api.json and the auth-endpoints reference; spec minor changeset. Premise holds (on 96fb7b5d the schema has no sso). One premise detail disproved: ui/packages/app-shell/src/runtime-config.ts reads GET /api/v1/runtime/config (cloud-connection plan flag), not /auth/config; the real ui reader is ui/packages/auth/src/LoginForm.tsx. No producer disagreement a client can see (both HTTP doors refine; the unrefined value is read only server-internally), so no domain:server finding.",
      "tests": "Head 0d814466. spec test VERDICT 0: 791/791 files, 23474 passed, 1 todo. spec typecheck VERDICT 0 (131 pinned held). client typecheck VERDICT 0 + reverse probe ({sso:true} as AuthFeaturesConfig compiles exit 0; {sso:'yes'} TS2322 exit 2; probe deleted). plugin-auth full suite VERDICT 0: 211/211 files, 3643 passed, 10 skipped. hono adapter 3 auth-config files: 33 passed (lock VERDICT batch-last-exit 0, the vitest was the last part). ui from ui/: LoginForm.test.tsx + app-shell runtime-config.test.ts 2/2 files, 85 passed. New block in auth-endpoints.test.ts: 8 cases (keep true/false, full response parse keeps it, CONTROL absent stays absent, non-boolean yes/1/null refused with one invalid_type at sso, describe names isSsoUsable+ssoEnforced, no tracker id). Ablation: sso renamed ssoAblated via ablation-replace (anchor 1->0, blob fb278750->8c4aae77): 7 failed of 31, CONTROL green; restored blob = HEAD fb278750, git diff HEAD empty. Scoped eslint on the 2 changed .ts files exit 0.",
      "gates": "dispatch-gates --commands at 0d814466: 103 derived, 103 exit 0 (stdin closed, exit before pipe); check-plugin-teardown-shape --self-test first exit 3 (fixture commit outside shallow clone), 0 after git fetch --depth=1 origin 621a4876. --ran: 103 derived, 103 run, 0 NOT-MEASURED (derived zero), 0 UNRUN. Prior: turbo build packages/** VERDICT 0 (78 tasks); spec build + check:generated exit 1 naming check:docs only, gen:docs VERDICT 0. Offline body checks: check-changeset-no-major --event exit 0 (yes (widening) + added, spec minor); check-partof-closing-keyword exit 0. Local-only: check:skill-frame-freshness 0; check:dev-prereqs 1 = 35/106 packages (ui/ tree) unbuilt here, machine state, spec dist fresh. CI check-runs: in_progress at report time, the seat reads them.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "7 — all reads: issue_read x4 (get #29344, get_comments #29344 x2, get #25292), pull_request_read x3 (get_files #25309 precedent, get #29401 body shape, get_files #29455 read-back)",
      "api_writes": "2 — relay pr_create (#29455, run 41474194165, 10681 bytes sent = stored, no labels, no assignees) and this os-dev-report comment via post-stamped; plus git push (not REST)",
      "line_budget": "+73 / -1 over 5 files: zod +9, test +44, changeset +16, reference mdx +3/-1, authorable-surface +1",
      "files_changed": [".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", "packages/spec/src/api/auth-endpoints.zod.ts"],
      "deviations": [
        "Consumer test set differs from the delta: ran ui/packages/auth LoginForm.test.tsx (the real /auth/config reader) beside the delta's app-shell runtime-config.test.ts, which reads a different response (/api/v1/runtime/config); also ran the hono adapter's 3 /auth/config door files, the second producer.",
        "public-auth-features.ts not touched: its sso entry (login, opt-in, exempt: refined to usable via isSsoUsable) agrees, and no pin ties its keys to AuthFeaturesConfigSchema's keys.",
        "No liveness row: packages/spec/liveness governs metadata types, no ledger names this response (same as the cloudManaged precedent).",
        "PR body starts with the harness attribution block, then Fixes #29344 (os-dev exception for the harness first-line comment).",
        "PR assignee/reviewer not set: the standard order forbids MCP writes and os-dev forbids assignees at pr_create."
      ],
      "open_questions": [
        { "question": "Maintainer nod on the new public key name `sso` (fixed by the producer and its readers).", "options": ["A keep `sso` as served", "B rename (would need a producer + reader change and a migration)"], "recommendation": "A, because the producer, the console LoginForm and the mobile shell already use it; the seat puts it as needs-user-decision on the PR." },
        { "question": "Close the whole drift class with a guard?", "options": ["A family card: declare the 9 other served-undeclared feature keys and add a pin that PUBLIC_AUTH_FEATURES + PUBLIC_AUTH_CONFIG_NON_FLAG_KEYS equal AuthFeaturesConfigSchema's live keys", "B per-key cards as readers appear"], "recommendation": "A, one guard ends the class (os-dev: prefer a guard over single-point fixes)." }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: b · reach: named real producer packages/plugins/plugin-auth/src/auth-manager.ts getPublicConfig() features, served on GET /api/v1/auth/config · evidence: it serves multiOrgEnabled, tenancyPosture, audiencePosture, degradedTenancy, oidcProvider, deviceAuthorization, admin, termsUrl, privacyUrl, none declared in AuthFeaturesConfigSchema (on 96fb7b5d it declares twoFactor, organization, ssoEnforced, phoneNumber, phoneNumberOtp, cloudManaged, turnstileSiteKey + 2 retired), so a contract parse strips them; no pin ties PUBLIC_AUTH_FEATURES to the schema keys · Seam: spec:AuthFeaturesConfigSchema → runtime:auth-manager.ts getPublicConfig | renderer:ui/packages/auth types.ts hand mirror · family card for the seat to file · dedupe words: AuthFeaturesConfigSchema undeclared served keys, features.deviceAuthorization schema, auth config contract drift guard",
        "carrier: none · noted, not filed: ui/packages/auth/src/types.ts hand-typed features mirror documents sso as 'wired' while the served value is 'usable'"
      ]
    }

    Stamped 2026-11-02T15:43Z.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:spec#2 · os-steve · session_01ADHkmSduNGC2SZXszyrNut

    Landed: PR #29455, merged as aee9c471 at 2026-11-02T18:20Z.
    ACCEPT → PR #29455 comment 6582030426 · contract review PASS 6581701011 · name kept by the seat 6583305638.
    Follow-up filed: #29473 (nine undeclared auth feature keys and the registry pin).

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions