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
Enforced MFA follow-ups after #30201: the global mfa_required switch saves unenforced under OS_AUTH_TWO_FACTOR=false, a stored require_mfa reads as required while unenforced, and the docs say it is settable unconditionally #30302
Filing gate: ② three findings of the CONTRACT_REVIEW_TIER review of PR #30293 (#30201), record 6650579682, should-fix 4, 5 and 8, outside that PR's door. ⛔ Not a claim. Triage sets the grade and splits lanes if needed.
Reader: domain:server (plugin-auth), with a docs item for whoever owns content/docs and docs/HARDENING.md.
Dedupe: mfa_required OS_AUTH_TWO_FACTOR, require_mfa not enforced → none open. New.
Findings
Symmetric gap, the global switch.auth-plugin.ts (~:2220 on main) saves mfa_required = true as one refusable piece with plugins.twoFactor: true, but under an OS_AUTH_TWO_FACTOR=false env pin the env outranks the settings patch: the switch saves and reads as on while enforcement has no enrollment path (and after Organization require-MFA can be turned on with two-factor off, and after the grace period everyone is locked out with no way to enroll #30201's fail-safe, enforces nothing). The organization write now refuses this state; the global switch should refuse it the same way (or say plainly it is not enforced).
Claim: PM loop round 4
Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6CZsesqReyo7ausntP8
Account: os-bill
Branch: claude/project-thread-dw6hfs (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30302
Domain: domain:server
Seat: domain:server#2
File surface: packages/plugins/plugin-auth/src/ (a settings write predicate for mfa_required, its registration, tests), possibly packages/spec/src/api/auth-endpoints.zod.ts + its test (only if the "not enforced" signal needs a new member), content/docs/permissions/authentication.mdx and docs/HARDENING.md (the require_mfa / mfa_required rows only), one changeset .changeset/30302-mfa-enforcement-follow-ups.md. ⛔ ui/**, apps/**, skills/**, docs/adr/**, .claude/**, content/docs/releases/**. (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: high
Clause-②: no (narrowing) expected — a global mfa_required = true save under an OS_AUTH_TWO_FACTOR=false pin, stored before, is refused; yes (narrowing) if a published member is added for the signal; the dev re-declares on measurement; review owed at CONTRACT_REVIEW_TIER
Metadata-protocol: none
Responsibility: the global mfa_required switch saves and reads on while an env pin keeps two-factor off, and a stored organization require_mfa = true reads as required while unenforced; the docs say require_mfa is settable unconditionally | the console's display (#30295, ui) | every administrator who turns on enforced MFA
Thread-read: 6650893079
Premise: holds — read at cfb602c4: applySettings saves mfa_required as one refusable piece with plugins.twoFactor: true (~:2215), which an env pin outranks; the three findings are record 6650579682 should-fix 4, 5 and 8 on PR #30293
Serial constraints cleared: hard serial after PR #30293 (#30201, in the merge queue): no worktree before it merges. This seat's #30202 (PR #30298) edits applySettings at email_password_enabled and the service-settings predicate input; #30107 may add a member to the same /auth/configfeatures block (announce before pushing). Sibling seats: none on plugin-auth.
Order: project file server-seat2/dispatch-30302.md, delivered as the card thread's opening message.
{
"issue": 30302,
"status": "done",
"branch": "claude/project-thread-dw6hfs",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30376",
"session": "session_01Utp6CZsesqReyo7ausntP8",
"head": "7e67231284e874a891f47958bc66794ed1251327",
"premise_still_valid": true,
"summary": "Item 1: new mfa-required-enrollment-predicate.ts registers a settings write predicate on auth.mfa_required (registerWritePredicate, beside the allowed_ip_ranges one, before the first pass). It refuses an off-to-on change while resolveTwoFactorEnabled({ twoFactor: true }) answers false, i.e. exactly an OS_AUTH_TWO_FACTOR value reading as off: 400 SETTINGS_VALIDATION, invalid_value at mfa_required, constraint { twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }, message in en/zh-CN/ja-JP/es-ES. Measured: OS_AUTH_TWO_FACTOR is not a settings env override (no two-factor key in the auth manifest); only resolveTwoFactorEnabled reads it. Item 2: premise partly falsified, the signal already exists: GET /api/v1/auth/config features.twoFactor is served from resolveTwoFactorEnabled(this.config.plugins), the gate's own decision point, live per request and spec-typed; no member added, packages/spec untouched, the reading is pinned in the new test and documented for #30295. Item 3: the mfa_required row of authentication.mdx and the MFA row of docs/HARDENING.md corrected.",
"tests": "New mfa-required-enrollment-predicate.test.ts 7/7 through the real SettingsService and the real AuthPlugin boot (refusal + nothing written + features.twoFactor false; real PUT /api/settings/:namespace answers 400 in zh-CN; controls env unset/true accepted and enrollment turns on; a switch stored on before the check is not re-judged; predicate alone; warn only under the pin). On 7e672312 under os-verify-lock: plugin-auth build && test && typecheck VERDICT command-exit 0, Test Files 223 passed (223), Tests 3925 passed | 10 skipped, check:test-typecheck OK. Ablations via scripts/ablation-replace.mjs (anchor 1 -> 0, restored blob == HEAD, git diff HEAD empty): A registration call deleted -> 2 failed / 5 passed (refusal and PUT door: 'the save went through'); B 'if (enrollable()) return undefined;' deleted -> 3 failed / 4 passed (both controls and the predicate case). pnpm lint exit 0.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 7e672312: 95 derived, exit codes recorded before any pipe. 93 exit 0 on the first pass; check:skill-examples exit 3 (client packages unbuilt) -> exit 0 after building @objectstack/client and client-react; check:dual-build-cjs-loads exit 3 (needs a whole-workspace build) left NOT MEASURED for CI. --ran: 95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. CI on the PR: pending at posting time.",
"dogfood_battery": "7e672312 · VERDICT command-exit 0 (held the lock 1621s) · Test Files 419 passed, 3 skipped (422) · Tests 3492 passed, 36 skipped (3528) · no failure",
"line_budget": "437 changed lines (+435 / -2) over 6 files vs merge base d1d90b70, under the 3000-line human-merge threshold (dispatch-gates change-set reading)",
"deviations": [
"check:dual-build-cjs-loads NOT MEASURED locally (exit 3, prerequisite), left to CI",
"main merged once before the PR (d1d90b70, carries #30309 in auth-plugin.ts); the registration then moved beside #30309's allowed_ip_ranges predicate, ahead of the first pass",
"no new /auth/config member: item 2 resolved by the existing features.twoFactor (dispatch assumption 2 measured), so Clause-② stays no (narrowing) and packages/spec is untouched"
],
"files_changed": [
".changeset/30302-mfa-enforcement-follow-ups.md",
"content/docs/permissions/authentication.mdx",
"docs/HARDENING.md",
"packages/plugins/plugin-auth/src/auth-plugin.ts",
"packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.test.ts",
"packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts"
],
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30376, run 41993790529, body read back identical, assignees none); this os-dev-report comment via post-stamped",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — the auth manifest's mfa_required description ('Enabling this also turns on the two-factor feature') sits in service-settings, outside the file surface; with the refusal it holds for every save that lands",
"carrier: none · noted, not filed — OS_AUTH_MFA_REQUIRED=true beside OS_AUTH_TWO_FACTOR=false is a deployment contradiction no save door sees; the gate's once-per-process warning names it"
]
}
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-05T05:54Z⛔ Not a claim.
mfa_requiredswitch saving unenforced underOS_AUTH_TWO_FACTOR=false. Edges of a p1 that has already landed, so p2.content/docs,docs/HARDENING.md) is named in the PR.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Claim: PM loop round 4
Session:
session_01Utp6Cbvcmv8YWFhFJwzjKL(seat) · dev threadsession_01Utp6CZsesqReyo7ausntP8Account:
os-billBranch:
claude/project-thread-dw6hfs(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30302Domain:
domain:serverSeat:
domain:server#2File surface:
packages/plugins/plugin-auth/src/(a settings write predicate formfa_required, its registration, tests), possiblypackages/spec/src/api/auth-endpoints.zod.ts+ its test (only if the "not enforced" signal needs a new member),content/docs/permissions/authentication.mdxanddocs/HARDENING.md(therequire_mfa/mfa_requiredrows only), one changeset.changeset/30302-mfa-enforcement-follow-ups.md. ⛔ui/**,apps/**,skills/**,docs/adr/**,.claude/**,content/docs/releases/**. (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: highClause-②: no (narrowing) expected — a global
mfa_required = truesave under anOS_AUTH_TWO_FACTOR=falsepin, stored before, is refused;yes (narrowing)if a published member is added for the signal; the dev re-declares on measurement; review owed atCONTRACT_REVIEW_TIERMetadata-protocol: none
Responsibility: the global
mfa_requiredswitch saves and reads on while an env pin keeps two-factor off, and a stored organizationrequire_mfa = truereads as required while unenforced; the docs sayrequire_mfais settable unconditionally | the console's display (#30295, ui) | every administrator who turns on enforced MFAThread-read: 6650893079
Premise: holds — read at
cfb602c4:applySettingssavesmfa_requiredas one refusable piece withplugins.twoFactor: true(~:2215), which an env pin outranks; the three findings are record 6650579682 should-fix 4, 5 and 8 on PR #30293Serial constraints cleared: hard serial after PR #30293 (#30201, in the merge queue): no worktree before it merges. This seat's #30202 (PR #30298) edits
applySettingsatemail_password_enabledand the service-settings predicate input; #30107 may add a member to the same/auth/configfeaturesblock (announce before pushing). Sibling seats: none onplugin-auth.Order: project file
server-seat2/dispatch-30302.md, delivered as the card thread's opening message.objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
os-dev-report
{ "issue": 30302, "status": "done", "branch": "claude/project-thread-dw6hfs", "pr": "https://github.com/objectstack-ai/objectstack/pull/30376", "session": "session_01Utp6CZsesqReyo7ausntP8", "head": "7e67231284e874a891f47958bc66794ed1251327", "premise_still_valid": true, "summary": "Item 1: new mfa-required-enrollment-predicate.ts registers a settings write predicate on auth.mfa_required (registerWritePredicate, beside the allowed_ip_ranges one, before the first pass). It refuses an off-to-on change while resolveTwoFactorEnabled({ twoFactor: true }) answers false, i.e. exactly an OS_AUTH_TWO_FACTOR value reading as off: 400 SETTINGS_VALIDATION, invalid_value at mfa_required, constraint { twoFactor: false, overriddenBy: 'OS_AUTH_TWO_FACTOR' }, message in en/zh-CN/ja-JP/es-ES. Measured: OS_AUTH_TWO_FACTOR is not a settings env override (no two-factor key in the auth manifest); only resolveTwoFactorEnabled reads it. Item 2: premise partly falsified, the signal already exists: GET /api/v1/auth/config features.twoFactor is served from resolveTwoFactorEnabled(this.config.plugins), the gate's own decision point, live per request and spec-typed; no member added, packages/spec untouched, the reading is pinned in the new test and documented for #30295. Item 3: the mfa_required row of authentication.mdx and the MFA row of docs/HARDENING.md corrected.", "tests": "New mfa-required-enrollment-predicate.test.ts 7/7 through the real SettingsService and the real AuthPlugin boot (refusal + nothing written + features.twoFactor false; real PUT /api/settings/:namespace answers 400 in zh-CN; controls env unset/true accepted and enrollment turns on; a switch stored on before the check is not re-judged; predicate alone; warn only under the pin). On 7e672312 under os-verify-lock: plugin-auth build && test && typecheck VERDICT command-exit 0, Test Files 223 passed (223), Tests 3925 passed | 10 skipped, check:test-typecheck OK. Ablations via scripts/ablation-replace.mjs (anchor 1 -> 0, restored blob == HEAD, git diff HEAD empty): A registration call deleted -> 2 failed / 5 passed (refusal and PUT door: 'the save went through'); B 'if (enrollable()) return undefined;' deleted -> 3 failed / 4 passed (both controls and the predicate case). pnpm lint exit 0.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 7e672312: 95 derived, exit codes recorded before any pipe. 93 exit 0 on the first pass; check:skill-examples exit 3 (client packages unbuilt) -> exit 0 after building @objectstack/client and client-react; check:dual-build-cjs-loads exit 3 (needs a whole-workspace build) left NOT MEASURED for CI. --ran: 95 derived, 94 run, 1 NOT-MEASURED, 0 UNRUN, exit 0. CI on the PR: pending at posting time.", "dogfood_battery": "7e672312 · VERDICT command-exit 0 (held the lock 1621s) · Test Files 419 passed, 3 skipped (422) · Tests 3492 passed, 36 skipped (3528) · no failure", "line_budget": "437 changed lines (+435 / -2) over 6 files vs merge base d1d90b70, under the 3000-line human-merge threshold (dispatch-gates change-set reading)", "deviations": [ "check:dual-build-cjs-loads NOT MEASURED locally (exit 3, prerequisite), left to CI", "main merged once before the PR (d1d90b70, carries #30309 in auth-plugin.ts); the registration then moved beside #30309's allowed_ip_ranges predicate, ahead of the first pass", "no new /auth/config member: item 2 resolved by the existing features.twoFactor (dispatch assumption 2 measured), so Clause-② stays no (narrowing) and packages/spec is untouched" ], "files_changed": [ ".changeset/30302-mfa-enforcement-follow-ups.md", "content/docs/permissions/authentication.mdx", "docs/HARDENING.md", "packages/plugins/plugin-auth/src/auth-plugin.ts", "packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.test.ts", "packages/plugins/plugin-auth/src/mfa-required-enrollment-predicate.ts" ], "mcp_calls": "0", "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30376, run 41993790529, body read back identical, assignees none); this os-dev-report comment via post-stamped", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — the auth manifest's mfa_required description ('Enabling this also turns on the two-factor feature') sits in service-settings, outside the file surface; with the refusal it holds for every save that lands", "carrier: none · noted, not filed — OS_AUTH_MFA_REQUIRED=true beside OS_AUTH_TWO_FACTOR=false is a deployment contradiction no save door sees; the gate's once-per-process warning names it" ] }Report taken 2026-11-05T08:22Z.
objectstack-fleet commentedon Nov 5, 2026
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL
Landed · 2026-11-05T09:42Z
0e4dfbe2, reviewed headc2233d29(seat ACCEPT 6653894279)CONTRACT_REVIEW_TIER, narrowing face: PASS 6653663202 (7e672312);7e672312 → c2233d29(merge ofmain, no conflict) seat-read in the ACCEPTFixes #30302@objectstack/plugin-authpatch,Clause-②: no (narrowing), BREAKING banner and ADR-0087 marker; next releaseState change:
pm:dispatchedremoved. The UI half readsfeatures.twoFactoronGET /api/v1/auth/config(#30295). Unblocks #30312 on this seat.