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
plugin-auth writes no audit row when policy refuses a sign-in (IP allow-list 403, MFA_REQUIRED hold), so no enforcement report can be built #30924
Filing gate: (b) a contract gap with a named consumer, measured by the cloud#3668 dev (report 6684026104 on objectstack-ai/cloud#3668, Enterprise P2-8). The maintainer told the cloud seat 「cloud 多了几个企业版的任务,优先处理」 and ruled platform gaps go to the platform (「平台的问题应该在平台处理」, objectstack#30765). Consumer: the Enterprise enforcement report (objectstack-ai/cloud#3668), which is pm:blocked on this card.
维护者速读
因为 IP 不在白名单、或者没开双重验证而被拦下的登录,平台现在什么记录都不写:IP 拦截只在进程里打一行告警,双重验证拦截只是一个会话门槛。企业版的「谁、从哪里、因为哪条规则被拦」报表因此无数据可读。要做的是每次策略拦截写一条审计记录。
Measured (objectstack 2ffdb649)
ADR-0069 D5 IP refusal: the auth-route middleware returns 403 (packages/plugins/plugin-auth/src/auth-plugin.ts:3262) before better-auth resolves an account. It writes only a once-per-process warn line (:3138).
ADR-0069 D3 MFA hold:auth-manager-password-session-policy.ts:248 pushes an AuthGate and returns. No row is written.
login_failed carries a refusal code only for the password and lockout refusals of /sign-in/email (auth-session-audit.ts:261).
Expected
One sys_audit_log row per policy refusal: the account where it can be resolved, the client address, the rule (ip_allow_list | mfa_required, extensible), the organization where known, and the time.
Follow the incumbent shape: a login_failed row with context.reason (auth-session-audit.ts:261), unless the lane judges a dedicated action better. Say which.
Rate-limit or aggregate so a scanner hammering the IP gate cannot flood the table, and state the rule.
Acceptance
Tests: an out-of-range sign-in writes one row with the rule and address; an MFA-held sign-in writes one row; a flood is bounded.
Serial: after objectstack#30922 (both edit auth-manager-password-session-policy.ts).
This answers domain:server#3's ask (6684268695). plugin-auth writes no audit row when policy refuses a sign-in (the IP allow-list 403, the MFA_REQUIRED hold), so the Enterprise enforcement report (objectstack-ai/cloud#3668) has nothing to read. p1 kept on the maintainer's 「cloud 多了几个企业版的任务,优先处理」. The audit row must not carry the refused credential.
Claim: PM loop round 1 · 2026-11-06T12:43Z · seat 3's pick in the live server split, after the first-touch audit 6684900750
Session: session_016WV4cytQjG9X1CrCgytUM5
Account: os-elon-musk (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/project-thread-o1vsm1
Worktree: the card thread's own checkout of claude/project-thread-o1vsm1
Domain: domain:server
Seat: domain:server#3
File surface: packages/plugins/plugin-auth/src/ (auth-session-audit.ts, auth-manager-database-hooks.ts (session.create.after), auth-plugin.ts (the IP gate's sign-in refusal and the refusal counter store in init), + tests); packages/plugins/plugin-audit/src/auth-event-audit.ts (a row with no resolvable account, seat-approved addendum); one pin file under packages/qa/dogfood/test/; root .changeset/ files. ⛔ auth-manager.ts, auth-manager-password-session-policy.ts, settings manifests, spec, ui, the cloud repo (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default, effort: xhigh
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — the policy doors (ADR-0069 D3 / D5) never wrote an audit row; the consumer is objectstack-ai/cloud#3668
Thread-read: 6684900750
Premise: holds, measured on origin/main0e882086: an out-of-range /sign-in/email answers 403 IP_NOT_ALLOWED and hands the audit sink nothing (real or unknown email); an MFA-held sign-in is not a refusal on main (200, a session and a login row; the hold is computed per session read), so its hold rides that login row
Serial constraints cleared: PR1 none. PR2 waits on #30922 (claimed by repo:cloud#3, whose claim declares plugin-auth's IP allow-list middleware)
Series of two: PR1 Refs #30924 — the MFA hold recorded on the held sign-in's own login row (context.reason: 'MFA_REQUIRED', context.rule: 'mfa_required'), Clause-②: no. PR2 Fixes #30924 — one login_failed row per /sign-in/* IP refusal (context.reason: 'IP_NOT_ALLOWED', context.rule: 'ip_allow_list', client address, the account where it resolves), bounded per address per 10-minute window (rows at the 1st / 10th / 100th refusal carrying context.attempts) plus a global per-window cap; userId optional on the exported audit event type → Clause-②: yes (widening) on PR2 alone, contract review owed there. No row carries the refused credential.
{
"issue": 30924,
"status": "done",
"branch": "claude/project-thread-o1vsm1",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30950",
"session": "session_016WV4cwTfQ1ifwQY2EUnSUt",
"premise_still_valid": true,
"summary": "PR 1 of the two-PR series in the seat's Claim (Refs, not Fixes; the card stays open). A sign-in held by ADR-0069 D3 enforced MFA now records the hold on its own login row (context.reason MFA_REQUIRED, context.rule mfa_required), computed once in session.create.after through the same resolveUserAuthGates the session gate asks; impersonation sessions take only account-scope postures; a failing posture read records no hold. Premise partly re-shaped by measurement on 0e882086: the MFA hold is a minted session (200 + login row), so it is not a login_failed; the IP allow-list half (login_failed, subject-less widening, flood bound) is PR 2, designed against #30922's branch and built only after #30922 merges, as ruled.",
"tests": "plugin-auth auth-session-audit.test.ts + auth-account-event-audit.test.ts: Tests 45 passed (45). Unit ablation via scripts/ablation-replace.mjs (withSessionPolicyHold(login, hold) -> login in sessionCreateAfter): Tests 4 failed | 41 passed, controls green, restored. Dogfood auth-session-audit-trail.dogfood.test.ts: Tests 6 passed (6); same ablation rebuilt, ablation-dist-preflight --absent proved the marker left dist, the #30924 case failed (1 failed | 5 skipped), restored + rebuilt, marker back, git status --porcelain empty. plugin-auth full suite: 228 files, 4108 passed | 10 skipped. plugin-audit auth-event-audit.test.ts: 17 passed (17). plugin-auth typecheck (src + tests) exit 0; eslint --no-inline-config on 7 changed files exit 0.",
"gates": "Head 60164041. Whole-tree turbo build under os-verify-lock: VERDICT command-exit 0, 119/119 successful. dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, all 67 exit 0 (exit codes captured per command before any pipe). CI check-runs on PR #30950: pending at report time; the seat reads them before ACCEPT.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "8 — all reads: mcp__github__issue_read x6, mcp__github__list_pull_requests x2; no MCP write",
"api_writes": "3 — git push x2 (claude/project-thread-o1vsm1); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #30950, read-back identical); plus this os-dev-report comment",
"open_questions": [
{
"question": "PR 2: AuthManager.resolveSignInAccountId / findSignInAccount are private, so the deployment IP gate in auth-plugin.ts cannot name the account without touching auth-manager.ts.",
"options": [
"A: a second small auth-manager.ts addendum, one public forwarder used only for the audit row",
"B: the deployment-gate IP row is always subject-less"
],
"recommendation": "A, because the ruling says the row names the account where it resolves"
},
{
"question": "PR 2: #30922 also refuses a switch of active organization (session.update.before). Is that a sign-in?",
"options": [
"A: no row there; only session creation writes login_failed",
"B: a row there too"
],
"recommendation": "A, because a switch is not a sign-in attempt, the same reasoning that excludes get-session and sign-out"
},
{
"question": "PR 2: the global per-window cap on IP refusal rows.",
"options": [
"A: 1000 rows per 10 minutes, one operator warn line per window once dropping",
"B: another number"
],
"recommendation": "A"
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted in PR body Acceptance notes: the per-organization MFA requirement is seen through the lazily refreshed org-MFA cache, so a sign-in in the boot window before the first refresh records no per-org hold, exactly as the gate does not hold that read"
]
}
PR 2 design notes (not yet built): the three open questions above.
Progress · 2026-11-06T14:42Z · PR 1 of 2 landed: PR #30950 merged through the queue as 256a4b7251 (Refs #30924; the card stays open and dispatched).
Reading
Value
Landed
ruling 2B: a sign-in held by the ADR-0069 D3 enforced-MFA requirement records the hold on its own login row (context.reason: "MFA_REQUIRED", context.rule: "mfa_required"), computed once at session creation; no login_failed row for a held sign-in; no credential on any row
Reviews
seat ACCEPT 6686831728 on 60164041; not governed, 426 lines; Clause-②: no
Next
PR2 (Fixes #30924): the IP allow-list refusal rows (rulings 1A, 3, 4, 5), the account-resolver forwarder, the SESSION_SCOPING_REFUSAL row, and the audit-service.mdx edit (reason/rule on a held login row + the userId bullet). It opens after #30922's PR merges; Clause-②: yes (widening), contract review owed
PR1 landed (#30950, 256a4b7251). PR2 (Fixes #30924) waits on #30922's PR, and seat 3 is winding down (maintainer order), so the card goes back to pm:blocked with no assignee. The next server seat claims it once #30922's PR merges.
Claim: PM loop round 1 · 2026-11-07T17:16Z · seat 6's pick in the A/B server split (B = the plugin-auth cluster)
Session: session_01FEkQ5p3T5YWhLjurajudnQ
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-v9d4f2
Worktree: the card thread's own checkout of claude/project-thread-v9d4f2
Domain: domain:server
Seat: domain:server#6
File surface: packages/plugins/plugin-auth/src/ (auth-plugin.ts sign-in IP gate region, auth-session-audit.ts, auth-manager.ts one public account-resolver forwarder, the SESSION_SCOPING_REFUSAL site in organization-access-policy.ts / auth-manager-database-hooks.ts, + tests); packages/plugins/plugin-audit/src/auth-event-audit.ts (subject-less row); content/docs/kernel/runtime-services/audit-service.mdx; one dogfood pin under packages/qa/dogfood/test/; root .changeset/. ⛔ packages/spec/**, ui/**, settings manifests (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default, effort: xhigh (p1)
Clause-②: yes (widening) (userId optional on the exported audit event type; TIER_DEFAULT review owed, seat runs it)
Metadata-protocol: none
Responsibility: n/a — not a defect card (contract gap with a named consumer, objectstack-ai/cloud#3668)
Thread-read: 6709725868
Premise: holds for PR 2: PR 1 landed (#30950, 256a4b7251); #30922 closed (PR #31013, b5696b49), unblock 6709725868; the dev re-measures both IP doors red-first
Serial constraints cleared: PR 1 landed; #30922 landed. Same-seat parallel #31030 (auth-plugin.ts admin-route / in-process door region, disjoint function) and #31175 (test file); marchtian #30965 / PR #31194 (SSO refusal codes, register-sso-provider.ts region) — disjoint; second to land merges origin/main once
Activity
objectstack-fleet commentedon Nov 6, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 kept ·
domain:server· 2026-11-06T12:32Z⛔ Not a claim.
domain:server#3's ask (6684268695). plugin-auth writes no audit row when policy refuses a sign-in (the IP allow-list 403, theMFA_REQUIREDhold), so the Enterprise enforcement report (objectstack-ai/cloud#3668) has nothing to read. p1 kept on the maintainer's 「cloud 多了几个企业版的任务,优先处理」. The audit row must not carry the refused credential.Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5
Claim: PM loop round 1 · 2026-11-06T12:43Z · seat 3's pick in the live server split, after the first-touch audit 6684900750
Session:
session_016WV4cytQjG9X1CrCgytUM5Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-o1vsm1Worktree: the card thread's own checkout of
claude/project-thread-o1vsm1Domain:
domain:serverSeat:
domain:server#3File surface:
packages/plugins/plugin-auth/src/(auth-session-audit.ts,auth-manager-database-hooks.ts(session.create.after),auth-plugin.ts(the IP gate's sign-in refusal and the refusal counter store ininit), + tests);packages/plugins/plugin-audit/src/auth-event-audit.ts(a row with no resolvable account, seat-approved addendum); one pin file underpackages/qa/dogfood/test/; root.changeset/files. ⛔auth-manager.ts,auth-manager-password-session-policy.ts, settings manifests, spec, ui, the cloud repo (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default,effort: xhighClause-②: no
Metadata-protocol: none
Responsibility: n/a — the policy doors (ADR-0069 D3 / D5) never wrote an audit row; the consumer is objectstack-ai/cloud#3668
Thread-read: 6684900750
Premise: holds, measured on
origin/main0e882086: an out-of-range/sign-in/emailanswers403 IP_NOT_ALLOWEDand hands the audit sink nothing (real or unknown email); an MFA-held sign-in is not a refusal onmain(200, a session and aloginrow; the hold is computed per session read), so its hold rides thatloginrowSerial constraints cleared: PR1 none. PR2 waits on #30922 (claimed by repo:cloud#3, whose claim declares plugin-auth's IP allow-list middleware)
Series of two: PR1
Refs #30924— the MFA hold recorded on the held sign-in's ownloginrow (context.reason: 'MFA_REQUIRED',context.rule: 'mfa_required'),Clause-②: no. PR2Fixes #30924— onelogin_failedrow per/sign-in/*IP refusal (context.reason: 'IP_NOT_ALLOWED',context.rule: 'ip_allow_list', client address, the account where it resolves), bounded per address per 10-minute window (rows at the 1st / 10th / 100th refusal carryingcontext.attempts) plus a global per-window cap;userIdoptional on the exported audit event type →Clause-②: yes (widening)on PR2 alone, contract review owed there. No row carries the refused credential.objectstack-fleet commentedon Nov 6, 2026
domain:server#3 · os-elon-musk · session_016WV4cwTfQ1ifwQY2EUnSUt
os-dev-report
{ "issue": 30924, "status": "done", "branch": "claude/project-thread-o1vsm1", "pr": "https://github.com/objectstack-ai/objectstack/pull/30950", "session": "session_016WV4cwTfQ1ifwQY2EUnSUt", "premise_still_valid": true, "summary": "PR 1 of the two-PR series in the seat's Claim (Refs, not Fixes; the card stays open). A sign-in held by ADR-0069 D3 enforced MFA now records the hold on its own login row (context.reason MFA_REQUIRED, context.rule mfa_required), computed once in session.create.after through the same resolveUserAuthGates the session gate asks; impersonation sessions take only account-scope postures; a failing posture read records no hold. Premise partly re-shaped by measurement on 0e882086: the MFA hold is a minted session (200 + login row), so it is not a login_failed; the IP allow-list half (login_failed, subject-less widening, flood bound) is PR 2, designed against #30922's branch and built only after #30922 merges, as ruled.", "tests": "plugin-auth auth-session-audit.test.ts + auth-account-event-audit.test.ts: Tests 45 passed (45). Unit ablation via scripts/ablation-replace.mjs (withSessionPolicyHold(login, hold) -> login in sessionCreateAfter): Tests 4 failed | 41 passed, controls green, restored. Dogfood auth-session-audit-trail.dogfood.test.ts: Tests 6 passed (6); same ablation rebuilt, ablation-dist-preflight --absent proved the marker left dist, the #30924 case failed (1 failed | 5 skipped), restored + rebuilt, marker back, git status --porcelain empty. plugin-auth full suite: 228 files, 4108 passed | 10 skipped. plugin-audit auth-event-audit.test.ts: 17 passed (17). plugin-auth typecheck (src + tests) exit 0; eslint --no-inline-config on 7 changed files exit 0.", "gates": "Head 60164041. Whole-tree turbo build under os-verify-lock: VERDICT command-exit 0, 119/119 successful. dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, all 67 exit 0 (exit codes captured per command before any pipe). CI check-runs on PR #30950: pending at report time; the seat reads them before ACCEPT.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "8 — all reads: mcp__github__issue_read x6, mcp__github__list_pull_requests x2; no MCP write", "api_writes": "3 — git push x2 (claude/project-thread-o1vsm1); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #30950, read-back identical); plus this os-dev-report comment", "open_questions": [ { "question": "PR 2: AuthManager.resolveSignInAccountId / findSignInAccount are private, so the deployment IP gate in auth-plugin.ts cannot name the account without touching auth-manager.ts.", "options": [ "A: a second small auth-manager.ts addendum, one public forwarder used only for the audit row", "B: the deployment-gate IP row is always subject-less" ], "recommendation": "A, because the ruling says the row names the account where it resolves" }, { "question": "PR 2: #30922 also refuses a switch of active organization (session.update.before). Is that a sign-in?", "options": [ "A: no row there; only session creation writes login_failed", "B: a row there too" ], "recommendation": "A, because a switch is not a sign-in attempt, the same reasoning that excludes get-session and sign-out" }, { "question": "PR 2: the global per-window cap on IP refusal rows.", "options": [ "A: 1000 rows per 10 minutes, one operator warn line per window once dropping", "B: another number" ], "recommendation": "A" } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted in PR body Acceptance notes: the per-organization MFA requirement is seen through the lazily refreshed org-MFA cache, so a sign-in in the boot window before the first refresh records no per-org hold, exactly as the gate does not hold that read" ] }PR 2 design notes (not yet built): the three open questions above.
Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5
Progress · 2026-11-06T14:42Z · PR 1 of 2 landed: PR #30950 merged through the queue as
256a4b7251(Refs #30924; the card stays open and dispatched).loginrow (context.reason: "MFA_REQUIRED",context.rule: "mfa_required"), computed once at session creation; nologin_failedrow for a held sign-in; no credential on any row60164041; not governed, 426 lines;Clause-②: noFixes #30924): the IP allow-list refusal rows (rulings 1A, 3, 4, 5), the account-resolver forwarder, theSESSION_SCOPING_REFUSALrow, and theaudit-service.mdxedit (reason/ruleon a heldloginrow + theuserIdbullet). It opens after #30922's PR merges;Clause-②: yes (widening), contract review owedobjectstack-fleet commentedon Nov 6, 2026
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5
Parked · 2026-11-06T15:20Z · Blocked-by: #30922
PR1 landed (#30950,
256a4b7251). PR2 (Fixes #30924) waits on #30922's PR, and seat 3 is winding down (maintainer order), so the card goes back topm:blockedwith no assignee. The next server seat claims it once #30922's PR merges.Hand-off for that claim:
pr2-design.md(project files,server-lane-seat3/2026-11-06/30924/) and this card's ruling comments. Forks ruled: 1A (rows even with no resolvable account, via the plugin-audit addendum), 3 (reason= machine code +rule), 4 (IP rows for/sign-in/*only), 5 (per-address 10-minute window, rows at the 1st / 10th / 100th refusal, a global cap of 1000 per window plus one warn line), plus an account-resolver forwarder, no row on the org switch, and a row at Access policy per organization: allowed IP ranges, session idle/absolute/concurrent limits and an MFA requirement of off / administrators / everyone, with the deployment value as default and floor #30922'sSESSION_SCOPING_REFUSAL.content/docs/kernel/runtime-services/audit-service.mdxgetsreason/ruleon a heldloginrow and theuserIdbullet edit.Clause-②: yes (widening)(userIdoptional on the exported audit event type), and a contract review is owed. Branch fresh frommainafter Access policy per organization: allowed IP ranges, session idle/absolute/concurrent limits and an MFA requirement of off / administrators / everyone, with the deployment value as default and floor #30922 lands (merge commit, no rebase).objectstack-fleet commentedon Nov 7, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblock ·
pm:blocked→pm:queue· 2026-11-07T16:19Z⛔ Not a claim.
Blocked-by:target is closed, re-read now: Access policy per organization: allowed IP ranges, session idle/absolute/concurrent limits and an MFA requirement of off / administrators / everyone, with the deployment value as default and floor #30922 (closed completed 2026-11-07T13:45Z).Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ
Claim: PM loop round 1 · 2026-11-07T17:16Z · seat 6's pick in the A/B server split (B = the plugin-auth cluster)
Session:
session_01FEkQ5p3T5YWhLjurajudnQAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-v9d4f2Worktree: the card thread's own checkout of
claude/project-thread-v9d4f2Domain:
domain:serverSeat:
domain:server#6File surface:
packages/plugins/plugin-auth/src/(auth-plugin.tssign-in IP gate region,auth-session-audit.ts,auth-manager.tsone public account-resolver forwarder, theSESSION_SCOPING_REFUSALsite inorganization-access-policy.ts/auth-manager-database-hooks.ts, + tests);packages/plugins/plugin-audit/src/auth-event-audit.ts(subject-less row);content/docs/kernel/runtime-services/audit-service.mdx; one dogfood pin underpackages/qa/dogfood/test/; root.changeset/. ⛔packages/spec/**,ui/**, settings manifests (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default,effort: xhigh(p1)Clause-②: yes (widening) (
userIdoptional on the exported audit event type;TIER_DEFAULTreview owed, seat runs it)Metadata-protocol: none
Responsibility: n/a — not a defect card (contract gap with a named consumer, objectstack-ai/cloud#3668)
Thread-read: 6709725868
Premise: holds for PR 2: PR 1 landed (#30950,
256a4b7251); #30922 closed (PR #31013,b5696b49), unblock 6709725868; the dev re-measures both IP doors red-firstSerial constraints cleared: PR 1 landed; #30922 landed. Same-seat parallel #31030 (
auth-plugin.tsadmin-route / in-processdoorregion, disjoint function) and #31175 (test file); marchtian #30965 / PR #31194 (SSO refusal codes,register-sso-provider.tsregion) — disjoint; second to land mergesorigin/mainonce