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
domain:server#6 · os-project-manager · session_01FEkQ5pfyn4wSGLdTuVZctK
os-dev-report
{
"issue": 30924,
"status": "done",
"branch": "claude/project-thread-v9d4f2",
"pr": "#31249",
"session": "session_01FEkQ5pfyn4wSGLdTuVZctK",
"premise_still_valid": true,
"summary": "PR 2 of 2 (Fixes #30924). A sign-in refused by the ADR-0069 D5 IP allow-list now writes one login_failed row (reason IP_NOT_ALLOWED, rule ip_allow_list, endpoint, attempts, client address and user agent) at both doors: the deployment list in the auth-route middleware (sign-in endpoints only, account named through the new public forwarder AuthManager.resolveRefusedSignInAccountId when the JSON email resolves, subject-less otherwise; the body read, lookup and write run detached so the 403 status, body and timing are unchanged) and an organization list at session creation (names the account and organization; no row on a refused active-organization switch). Rows are bounded per address (1st/10th/100th refusal in a 10-minute window) and globally (1000 per window, one warn line), in process memory per audit sink, so N replicas write up to N times as many. plugin-audit admits a subject-less login_failed (user_id/record_id/actor null); userId is optional on both exported event types (Clause-2 yes, widening). Measured on main 658d84f: the deployment list wrote nothing; the organization list already got a login_failed from the password after-hook (reason only, no rule/org/bound), so signInFailureEventFor now yields IP_NOT_ALLOWED to the door row and one row is written, not two. Docs audit-service.mdx updated. Merged origin/main once (2aeb9e0) because #31194 landed first in plugin-auth; clean merge.",
"tests": "Red first on 658d84f (production sources restored, tests kept): ip-allow-list-refusal-audit.test.ts 8 failed | 3 passed (deployment cases: expected [] to have a length of 1; flood: length 3 got 0; forwarder absent; organization row lacked rule/organizationId/attempts; 12 refusals gave 12 rows), plugin-audit auth-event-audit.test.ts 2 failed | 17 passed. Green: the two new plugin-auth files 17 passed (17). Ablations via scripts/ablation-replace.mjs, each restored (blob == HEAD, git diff HEAD empty): A1 gate call deleted -> 5 red; A2 marks ignored -> 4 red; A3 switch hands creating -> 1 red (negative pin); A4 IP_NOT_ALLOWED suppression deleted -> 2 red (two rows); A5 every path recorded -> 1 red (negative pin); A6 write awaited before the 403 -> 1 red (first attempt was a no-op: unbalanced replacement did not parse, reading discarded, redone as three held mutations); B1 subject-less login_failed refused -> 2 red; B2 any subject-less event admitted -> 2 red. Dogfood dist ablation of A1: rebuild (DTS TS6133 after JS was written), ablation-dist-preflight --absent marker gone from all 14 built files, new dogfood case 1 failed | 6 skipped; restored + rebuilt, marker present in 2 built files, dogfood 7 passed (7). On head 2aeb9e0: plugin-auth full suite 236/237 files, 4199 passed | 1 failed | 10 skipped, the one red is organization-access-policy.test.ts exact-60-minute boundary flake carried by #31175 (re-run alone 19 passed (19) three times); on 148bac5 before the merge 236 files, 4185 passed | 10 skipped. plugin-audit full suite 1203 passed (148bac5), auth-event-audit.test.ts 19 passed (2aeb9e0). Dogfood auth-session-audit-trail.dogfood.test.ts 7 passed (7) on 2aeb9e0. Typecheck src + tests: plugin-auth 0, plugin-audit 0 (2aeb9e0), dogfood 0 (148bac5). eslint --no-inline-config on the 10 changed .ts files exit 0.",
"gates": "Head 2aeb9e0. turbo run build under os-verify-lock: VERDICT command-exit 0, 120 successful, 120 total. dispatch-gates --ran: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN, all 99 exit 0 (codes captured per command before any pipe). Three first answered exit 3 PREREQUISITE and were re-run after it: check-plugin-teardown-shape --self-test (fetched fixture 621a487), check:skill-examples and check:dual-build-cjs-loads (after the full build). CI check-runs on PR #31249: 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": "0 — no MCP GitHub call; reads through gh api REST",
"api_writes": "4 — git push x2 (claude/project-thread-v9d4f2: 148bac5, then merge 2aeb9e0); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #31249, read-back identical, no assignees, no labels); plus this os-dev-report comment (post-stamped)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: public door POST /api/v1/auth/sign-in/email · evidence: on main, a sign-in refused by an organization IP list (SESSION_SCOPING_REFUSAL, 403 IP_NOT_ALLOWED) counts toward the ADR-0069 D2 lockout although the password was right: with lockoutThreshold 3, three correct-password sign-ins from outside the list answer 403 IP_NOT_ALLOWED and the fourth from inside the list answers 403 ACCOUNT_LOCKED (failed_login_count 3). Cause: auth-manager-after-hook.ts reads any returned APIError on /sign-in/email as a failed credential and calls recordSignInOutcome(email, false). Outside this card file surface, so not fixed here; the seat files it · dedupe words: lockout IP_NOT_ALLOWED, organization allowed_ip_ranges lockout, recordSignInOutcome scoping refusal",
"carrier: #31175 · noted, not filed: organization-access-policy.test.ts exact-60-minute absolute-limit case went red once in the full plugin-auth run on 2aeb9e0 and green 3 of 3 alone"
]
}
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/mainonceobjectstack-fleet commentedon Nov 7, 2026
domain:server#6 · os-project-manager · session_01FEkQ5pfyn4wSGLdTuVZctK
os-dev-report
{
"issue": 30924,
"status": "done",
"branch": "claude/project-thread-v9d4f2",
"pr": "#31249",
"session": "session_01FEkQ5pfyn4wSGLdTuVZctK",
"premise_still_valid": true,
"summary": "PR 2 of 2 (Fixes #30924). A sign-in refused by the ADR-0069 D5 IP allow-list now writes one login_failed row (reason IP_NOT_ALLOWED, rule ip_allow_list, endpoint, attempts, client address and user agent) at both doors: the deployment list in the auth-route middleware (sign-in endpoints only, account named through the new public forwarder AuthManager.resolveRefusedSignInAccountId when the JSON email resolves, subject-less otherwise; the body read, lookup and write run detached so the 403 status, body and timing are unchanged) and an organization list at session creation (names the account and organization; no row on a refused active-organization switch). Rows are bounded per address (1st/10th/100th refusal in a 10-minute window) and globally (1000 per window, one warn line), in process memory per audit sink, so N replicas write up to N times as many. plugin-audit admits a subject-less login_failed (user_id/record_id/actor null); userId is optional on both exported event types (Clause-2 yes, widening). Measured on main 658d84f: the deployment list wrote nothing; the organization list already got a login_failed from the password after-hook (reason only, no rule/org/bound), so signInFailureEventFor now yields IP_NOT_ALLOWED to the door row and one row is written, not two. Docs audit-service.mdx updated. Merged origin/main once (2aeb9e0) because #31194 landed first in plugin-auth; clean merge.",
"tests": "Red first on 658d84f (production sources restored, tests kept): ip-allow-list-refusal-audit.test.ts 8 failed | 3 passed (deployment cases: expected [] to have a length of 1; flood: length 3 got 0; forwarder absent; organization row lacked rule/organizationId/attempts; 12 refusals gave 12 rows), plugin-audit auth-event-audit.test.ts 2 failed | 17 passed. Green: the two new plugin-auth files 17 passed (17). Ablations via scripts/ablation-replace.mjs, each restored (blob == HEAD, git diff HEAD empty): A1 gate call deleted -> 5 red; A2 marks ignored -> 4 red; A3 switch hands creating -> 1 red (negative pin); A4 IP_NOT_ALLOWED suppression deleted -> 2 red (two rows); A5 every path recorded -> 1 red (negative pin); A6 write awaited before the 403 -> 1 red (first attempt was a no-op: unbalanced replacement did not parse, reading discarded, redone as three held mutations); B1 subject-less login_failed refused -> 2 red; B2 any subject-less event admitted -> 2 red. Dogfood dist ablation of A1: rebuild (DTS TS6133 after JS was written), ablation-dist-preflight --absent marker gone from all 14 built files, new dogfood case 1 failed | 6 skipped; restored + rebuilt, marker present in 2 built files, dogfood 7 passed (7). On head 2aeb9e0: plugin-auth full suite 236/237 files, 4199 passed | 1 failed | 10 skipped, the one red is organization-access-policy.test.ts exact-60-minute boundary flake carried by #31175 (re-run alone 19 passed (19) three times); on 148bac5 before the merge 236 files, 4185 passed | 10 skipped. plugin-audit full suite 1203 passed (148bac5), auth-event-audit.test.ts 19 passed (2aeb9e0). Dogfood auth-session-audit-trail.dogfood.test.ts 7 passed (7) on 2aeb9e0. Typecheck src + tests: plugin-auth 0, plugin-audit 0 (2aeb9e0), dogfood 0 (148bac5). eslint --no-inline-config on the 10 changed .ts files exit 0.",
"gates": "Head 2aeb9e0. turbo run build under os-verify-lock: VERDICT command-exit 0, 120 successful, 120 total. dispatch-gates --ran: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN, all 99 exit 0 (codes captured per command before any pipe). Three first answered exit 3 PREREQUISITE and were re-run after it: check-plugin-teardown-shape --self-test (fetched fixture 621a487), check:skill-examples and check:dual-build-cjs-loads (after the full build). CI check-runs on PR #31249: 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": "0 — no MCP GitHub call; reads through gh api REST",
"api_writes": "4 — git push x2 (claude/project-thread-v9d4f2: 148bac5, then merge 2aeb9e0); fleet-write relay pr_create x1 (POST /repos/objectstack-ai/objectstack/pulls -> #31249, read-back identical, no assignees, no labels); plus this os-dev-report comment (post-stamped)",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: public door POST /api/v1/auth/sign-in/email · evidence: on main, a sign-in refused by an organization IP list (SESSION_SCOPING_REFUSAL, 403 IP_NOT_ALLOWED) counts toward the ADR-0069 D2 lockout although the password was right: with lockoutThreshold 3, three correct-password sign-ins from outside the list answer 403 IP_NOT_ALLOWED and the fourth from inside the list answers 403 ACCOUNT_LOCKED (failed_login_count 3). Cause: auth-manager-after-hook.ts reads any returned APIError on /sign-in/email as a failed credential and calls recordSignInOutcome(email, false). Outside this card file surface, so not fixed here; the seat files it · dedupe words: lockout IP_NOT_ALLOWED, organization allowed_ip_ranges lockout, recordSignInOutcome scoping refusal",
"carrier: #31175 · noted, not filed: organization-access-policy.test.ts exact-60-minute absolute-limit case went red once in the full plugin-auth run on 2aeb9e0 and green 3 of 3 alone"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 7, 2026
domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ
落地 · 2026-11-07T21:17Z
ea62933c(re-queued once after an unrelated app-todo loop timeout in its first merge group, note 6712846074)Fixes #30924State change:
pm:dispatchedremoved; nothing left on this card. Its out-of-scope lockout finding is #31251 (PR #31262).