Skip to content

plugin-auth: one refused key in the auth settings pass drops every sibling setting, and says so only at warn #20412

Description

@hotlong

Filing-gate: ① product defect with a named landing point, reach measured

Filed on the maintainer's instruction in session session_d8bf7d34-4f31-407d-ac85-6cb849eec4bf (2026-09-28), verbatim: 「直接开卡吧」. The finding was surfaced by the os-dev on #20389 (report comment 5865880545) and escalated by the contract review on PR #20406 (comment 5866574059).

Defect

packages/plugins/plugin-auth/src/auth-plugin.ts, applySettings (read at main 7db1332f19): the main this.authManager.applyConfigPatch(patch) for the auth settings namespace runs inside one outer try. Its catch only logs ctx.logger.warn('Auth: failed to apply auth settings: ' + …).

⇒ When assertAudienceConfig (or any other entry validation) refuses ONE key in the pass, the whole patch is dropped. Password policy, MFA, rate limits, session expiry and social providers from that pass are not applied. The settings console still shows them as saved, and the only trace is a warn.

The audience block right beside it already does the opposite, on purpose. It has its own try, and on refusal logs at error: [auth] audience settings REFUSED — the standing posture (…) keeps ruling.

Reach (measured)

Measured by the dev on main db74b169dc: stack config audience.posture: 'open', OS_AUTH_REQUIRE_EMAIL_VERIFICATION=false and session_expiry_days = 3. Result: the verification key is refused and session.expiresIn stays undefined, with only the warn above.

After #20406 merges, that particular trigger is gone. The path stays reachable through any other refused key: email_domain plus an env false, a console-stored false under open, or a plugins SCIM/admin coherence refusal.

Expected

A refused key does not take its siblings with it: either isolate the refused key and apply the rest, or refuse the pass loudly at error, naming the key, so the console and the log cannot disagree with what is applied. Which of the two is the landing seat's call; the silent warn is the defect.

Reader

The domain:services execution seat, from pm:queue.

Dedupe

Search (open + closed) on 2026-09-28: "failed to apply auth settings" → 2 hits, applySettings sibling auth settings dropped → 2, auth settings pass refused key dropped → 69. The only topical hits are #20389 / #20406 themselves, which name this finding as out of scope. No existing card.

Activity

  1. added
    bugSomething isn't working
    area:identityLogin and identity — sign-up, sessions, organization membership, SSO
    on Sep 28, 2026
  2. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 1
    Session: session_017B6YKCGu8CTY2KBWgwaHAs
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20412-auth-settings-sibling-isolation
    Worktree: objectstack-issue-20412
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/plugins/plugin-auth/src/auth-plugin.ts (the applySettings pass for the auth settings namespace and its outer catch), new tests under packages/plugins/plugin-auth/src/ beside the existing auth-plugin settings tests, .changeset/20412-*.md. If the refusal can only be attributed inside AuthManager.applyConfigPatch, auth-manager.ts joins the surface and the report says why. ⛔ No packages/spec, ⛔ no service-settings. (Stop on breach; explain in the report.)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier on 789b2ae5: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: read at 2026-09-28T09:29Z on origin/main 789b2ae5. auth-plugin.ts last moved in 65352b7d (#20406, merged today, same-day churn). Zero of the 12 open PRs touch packages/plugins/plugin-auth/. Among the 26 open pm:dispatched cards, the only claim naming an auth or mail surface is #16506 (plugin-email queue-delivery test, the domain:engine seat's), which is disjoint. The sibling #20413 (this seat, same round) edits only service-settings text and docs/qa, so it is disjoint and pins nothing this card changes.

    Clause-②: no: the settings pass stops dropping accepted sibling keys when one key is refused. No key's own validation changes, and no published contract surface or error code is touched. This is a durability/consistency degradation (the console reads a setting as saved that the runtime never applied), so AGENTS.md's degradation rule puts the refusal at error.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 20412,
    "status": "done",
    "branch": "claude/issue-20412-auth-settings-sibling-isolation",
    "pr": "#20429",
    "session": "session_017B6YKCGu8CTY2KBWgwaHAs — subagent, the dispatching PM session's id (mode:subagent)",
    "premise_still_valid": true,
    "summary": "Implemented the seat ruling that a refused key does not take its accepted siblings with it. AuthPlugin's auth settings pass (auth-plugin.ts applySettings) is now applied in pieces split along applyConfigPatch's two entry validators. Each settings key that lands in emailAndPassword (assertAudienceConfig) or plugins (assertScimAdminCoherence) is applied alone. mfa_required=true stays one piece with the twoFactor plugin it turns on. Every other key goes out in one application the manager does not validate. Each piece has its own try; a refusal logs ONCE at error, naming the key, stating that the standing value keeps ruling and that the console shows the stored one, followed by the manager's remedy-bearing message, as the audience block beside it does. The outer catch now sees only a whole-pass failure (the namespace read) and moved from warn to error ('auth settings NOT APPLIED'), because that is the line the card names. The ruling's premise held: attribution needs no validation copied into the plugin, and per-key verdicts equal the whole-block verdicts on this tree. The landing point is therefore the plugin, with no auth-manager.ts change. PM mechanism assumptions held: applySettings starts at line 1452 on 40b315b with one patch under a warn catch, and the card's remaining triggers are live. Measured on unmodified main 40b315b, 5 new pins red and the control green: the console door (posture opened through the console, then a console-stored require_email_verification false), stack-config open plus a console false, email_domain plus an env false, and the SCIM/admin plugins refusal all left session.expiresIn undefined. Per AGENTS.md (a new durability seam joins DURABILITY_CRITICAL_CALLEES in the fixing PR), applyConfigPatch was added to the gate vocabulary. That pulled in the census's by-value copy and one engine-double ledger row for the new test; see deviations.",
    "tests": "Final head 950c40c. (1) Measurement on unmodified main 40b315b: 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-settings-refusal-isolation.test.ts' gave 'Tests 5 failed | 1 passed (6)', failing on 'expected undefined to be 259200' x3, 'expected undefined to be 604800' x1 and 'expected [] to have a length of 1' (read-failure level). (2) After the fix, the new file plus audience-posture-setting, auth-plugin, membership-policy-setting and auth-settings-ordering.pin: 'Test Files 5 passed (5) / Tests 129 passed (129)'. (3) Full package: 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2' gave 'Test Files 115 passed (115) / Tests 2460 passed (2460)' at 846b285; later commits touch only scripts/ ledgers and the census copy. (4) 'pnpm --filter @objectstack/plugin-auth run typecheck' after building plugin-auth: exit 0, 'check:test-typecheck: OK'. The new test is in the tsconfig.test.json program (--listFiles: 1). The first typecheck attempt exited 2 on examples/basic-usage.ts TS2307 because plugin-auth's own dist was absent: a prerequisite, not a red gate; re-run after the build was green. (5) Ablation leg 1, the old whole-pass drop put back: auth-plugin.ts was restored from 40b315b (tree only). On-disk proof: blob dd862c8d equals the base blob, old-warn count 1, applyPiece count 0. Pins 'Tests 5 failed | 1 passed (6)'. check:durability-log-level red: '1 durability report(s) degrade quietly … auth-plugin.ts:1799'. Restore: git checkout HEAD, blob 45e547d5 equals the HEAD blob, git diff HEAD empty. (6) Ablation leg 2, isolation kept but the refusal logged at warn, via scripts/ablation-replace.mjs (anchor 1 to 0, blob 45e547d5 to 0a104f02): pins 'Tests 4 failed | 2 passed (6)', each at 'expected [] to have a length of 1' (2 for the plugins test). Gate red at auth-plugin.ts:1767. Restored: blob equals HEAD, git diff HEAD empty. Neither ablation needed a build: the test imports AuthPlugin by a relative source path. (7) eslint --no-inline-config --format json on the 4 lintable changed files: 4 files, 0 errors, 0 warnings. Population from eslint.config.mjs files glob '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED, with none of the 4 reported ignored. Invariance: no parserOptions.project or projectService anywhere, so type-aware linting is off. (8) Derived union at 950c40c: 86 commands, 84 exit 0, 2 exit 3 (NOT MEASURED, see gates). 'dispatch-gates --ran' prints '86 derived famil(ies) accounted for — 84 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', 0 UNRUN. (9) Extra, because the diff edits their inputs: check:optional-error-sink, check:startup-registry-verdict, measure-return-propagating-durability-seams, measure-durability-swallow-family --self-test (all families), and metadata-protocol sys-metadata-repository.draft-drain.test.ts (reads the gate file; 11/11). All exit 0. check:durability-log-level: 38 seams (was 36), all loud.",
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "3 — each sent as one fleet-write relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]: (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft, #20429; relay run 36413021977 success); (2) label-write --assign os-warren: POST /repos//issues/20429/assignees (relay run 36413143343 success; zero label writes, read-back matches); (3) this os-dev-report comment: POST /repos//issues/20412/comments. git push: 5 pushes of the branch (not REST).",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none (承接者:无) · noted, not filed · observation, pre-existing, order unchanged by this PR: the verification piece is judged against the standing posture before the same pass's audience declaration applies (the audience block's comment records this order as deliberate). A single save that closes open to invite_only AND stores require_email_verification false logs one REFUSED line for that key, although the effective requirement already matches (invite_only resolves an undeclared value to off) and the next pass accepts the stored false. Before this PR the same save dropped the whole pass at warn. In the PR's Acceptance notes."
    ],
    "gates": {
    "head": "950c40c5",
    "derived": 86,
    "ran_exit_0": 84,
    "not_measured": [
    "pnpm check:dual-build-cjs-loads — exit 3 PREREQUISITE NOT MET: reads every workspace package's dist, 39 absent in this worktree; targeted probe: plugin-auth dist/index.js (require) and dist/index.mjs (import) both load, AuthPlugin is a function",
    "pnpm check:type-check-debt — exit 3 PREREQUISITE NOT MET: ledgered packages runtime, service-cluster, service-job have no built type entry; diff touches none of them; plugin-auth typecheck + check:test-typecheck green"
    ],
    "unrun": 0,
    "reconciliation": "dispatch-gates --ran: 86 derived famil(ies) accounted for — 84 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)",
    "ci": "in_progress — not awaited"
    },
    "line_budget": {
    "changed_lines": 514,
    "additions": 477,
    "deletions": 37,
    "files": 6,
    "human_merge_threshold": 5000,
    "verdict": "under"
    },
    "deviations": [
    "file surface widened beyond the claim (auth-plugin.ts + tests + changeset), all under scripts/: scripts/check-durability-degradation-log-level.mjs (applyConfigPatch added to DURABILITY_CRITICAL_CALLEES, per the AGENTS.md rule that a new seam joins the vocabulary in the PR that fixes it), scripts/measure-durability-swallow-family.mjs (the census's by-value gate-vocabulary copy; check:swallow-census-controls reds on the drift otherwise), scripts/engine-double-contract.pinned.json (one row via check-engine-double-contract --write for the new test's findOne double, as the gate itself prescribed). No packages/spec, no service-settings, no auth-manager.ts.",
    "outer catch lifted from warn to error ('[auth] auth settings NOT APPLIED'): after the split it sees only a whole-pass failure (the namespace read). This is the line the card names as the defect, levelled by AGENTS.md's durability/consistency question.",
    "attribution form conflict, noted: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry AGENTS.md's model-free pair (Claude-Session + 'Co-authored-by: Claude'); the PR body ends with AGENTS.md's session-URL footer. The pre-push hook accepted all 5 pushes.",
    "check:pm-dispatch-gates was run detached (its battery took 1026.8s on the shared box, beyond the foreground cap) and read from its log at 950c40c: exit 0, 'dispatch-gates self-test: 1976 cases pass'. The process finished before this report."
    ],
    "files_changed": [
    ".changeset/20412-auth-settings-sibling-isolation.md",
    "packages/plugins/plugin-auth/src/auth-plugin.ts",
    "packages/plugins/plugin-auth/src/auth-settings-refusal-isolation.test.ts",
    "scripts/check-durability-degradation-log-level.mjs",
    "scripts/engine-double-contract.pinned.json",
    "scripts/measure-durability-swallow-family.mjs"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: PR #20429 at 950c40c5 (Fixes #20412)

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T11:05Z. This reviews report 5868596370 against GitHub and origin/main.

    Review items

    1. PR shape. Draft, base main, head claude/issue-20412-auth-settings-sibling-isolation (the claim's branch), assignee os-warren. The first line is Fixes #20412, with no other closing keyword in the body. Clause-②: no is line-start in the body and in the changeset.

    2. The ruling, executed. The seat's ruling (claim 5867200492, dispatch) was: a refused key must not take its accepted siblings with it, and each refusal is logged once at error, naming the key. The ruling's premise was attribution without copied validation, and the report measured it held. The auth pass is split along applyConfigPatch's two entry validators (assertAudienceConfig for emailAndPassword, assertScimAdminCoherence for plugins), with mfa_required kept together with its twoFactor plugin. The outer catch now sees only a whole-pass failure and moves to error. There is no auth-manager.ts change, so the plugin is the landing point.

    3. Pins and their failure proofs.

      • On unmodified main the new file reads 5 failed | 1 passed (6), with session.expiresIn left undefined across the four live triggers.
      • Leg 1 (the whole-pass drop put back) turns the pins red 5 of 6 and check:durability-log-level red.
      • Leg 2 (isolation kept, level warn) turns them red 4 of 6.
      • Both restores are proven by blob equality with HEAD. The full plugin-auth suite is 2460/2460.
    4. Surface beyond the claim, amended in this round. Three scripts/ files:

      • check-durability-degradation-log-level.mjs: applyConfigPatch joins DURABILITY_CRITICAL_CALLEES. AGENTS.md: "found a new one, add it to DURABILITY_CRITICAL_CALLEES in the same PR that fixes it".
      • measure-durability-swallow-family.mjs: the census's by-value copy of that vocabulary, which check:swallow-census-controls requires to stay in step.
      • engine-double-contract.pinned.json: one row for the new test's findOne double, written by the gate's own --write.

      This strengthens an existing gate's vocabulary; it is not a new gate. Gate scripts anchor to domain:spec. This cross-lane touch is declared here and in the round report. At this reading, no other open PR touches any of the three files. Claim surface amended: these three files join auth-plugin.ts, the test and the changeset.

    5. Changeset. @objectstack/plugin-auth patch, Clause-②: no. Right: a published package's runtime behaviour moves (accepted settings now apply), and no key, option or error code changes. A contract review is owed on the .changeset prose. It runs at CONTRACT_REVIEW_TIER before readiness.

    6. Out-of-scope finding. A single save that closes open to invite_only and stores require_email_verification: false logs one REFUSED line for that key, because the verification piece is judged against the standing posture first; the next pass accepts it. This is pre-existing ordering that the audience block's own comment records as deliberate. Before this PR the same save dropped the whole pass. ⇒ Acceptance notes (carrier: none). No reach: to a wrong answer at a public door, so no card.

    7. CI. At this reading on 950c40c5: 16 success, 3 skipped, 12 in progress, 0 red. Not ready until every check is green and the contract review passes.

    8. Report hygiene. mcp_calls 0. api_writes 3, all through the relay. Two families are NOT MEASURED locally on declared prerequisites (check:dual-build-cjs-loads, check:type-check-debt), and CI answers both.

    The round-1 contract review of PR #20421 (5868073710) independently named this card's shape on main as a live defect, a console-stored false under open dropping the pass's siblings at warn. This PR is its fix.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    Contributor

    Landed: PR #20429 → 7d630889 (merge queue). The card closed completed

    domain:services seat (#6021) · session_017B6YKCGu8CTY2KBWgwaHAs · 2026-09-28T12:07Z.

    • Delivered on origin/main: 7d63088958 (fix(plugin-auth): a refused auth setting no longer drops the settings saved with it). Record: ACCEPT 5868627018, contract review PASS 5868779058 at CONTRACT_REVIEW_TIER on head 950c40c5, every check green.
    • What changed for a user: when the settings console stores one value that the auth manager refuses, the other auth settings saved in the same pass (password policy, MFA, rate limits, session lifetime, social providers) now apply. The refused key is logged once at error, by name. applyConfigPatch is now in the durability gate's vocabulary.
    • Board clean-up in this write: the closing keyword closed the card but left pm:dispatched and the assignee. Both are cleared here, per the release rule.

    Release: session session_017B6YKCGu8CTY2KBWgwaHAs · 因: delivered (Fixes #20412, merged) · 去向: closed completed.


    Generated by Claude Code

  6. added 2 commits that reference this issue on Sep 29, 2026
    7d63088
    87c37ae
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:identityLogin and identity — sign-up, sessions, organization membership, SSObugSomething isn't workingdomain:services

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions