Skip to content

feat(plugin-auth)!: an organization administrator reads their judged client address; the console's retired MFA notices are gone - #31258

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-em08na
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-em08na

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #31030

Clause-②: yes (narrowing) — the published @object-ui/i18n packs (all eleven) drop console.organizationMfa.needsTwoFactor and .nowRequired, which had no reader outside the removed notices. Both keys were added by the still-pending .changeset/30295-org-mfa-notice-and-enroll-refusal.md and never released, so no published version of the packs carries them. The widening half: a new served read, GET /api/v1/auth/client-address.
Metadata-protocol: none
Skills-docs: none — skills/** teaches no access-policy key, client-address read or console notice, and content/docs/** teaches no per-organization access policy; its allowed_ip_ranges section in permissions/authentication.mdx is the deployment's own list, whose platform-admin read it names correctly.

维护者速读

  • 改了什么:组织管理员(在当前组织有 setup.write)可通过新接口 GET /api/v1/auth/client-address 看到服务器判定的自己的 IP;auth 插件自挂的路由在进程内读会话时也会按组织的 IP 白名单判定;控制台删掉了组织表单上针对已退役 require_mfa 列的两条提示及其文案。
  • 为什么改:组织管理员保存「允许的 IP 范围」前,原本只能从保存被拒的报错里得知自己的地址(旧接口只对平台管理员开放);另外 auth 插件自挂路由读会话时不判组织 IP 名单,与 better-auth 自身路由的判定不一致。
  • 风险与回滚:新接口只回答调用者自己的地址,查询参数一律拒收;中间件只给 /api/v1/auth/* 下的会话读带上地址,不在名单内的会话会在这些路由上被结束(ip_not_allowed)。回滚即还原本 PR。
  • 先看:packages/plugins/plugin-auth/src/auth-plugin.ts(新路由与地址中间件)、packages/plugins/plugin-auth/src/auth-manager-plugin-list.ts(会话读的 door 参数)、packages/plugins/plugin-auth/src/client-address-read.ts(准入判定)。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/org-admin-client-address-read.dogfood.test.ts,预期 4 条全绿:组织管理员 200、普通成员 403 PERMISSION_DENIED、匿名 401、组织管理员读平台接口 403。
  • 需要你做的:无,席位落地。

What changes

S7: an organization administrator reads their judged address. GET /api/v1/auth/client-address (new, mounted by auth-plugin.ts ahead of the catch-all, beside the platform-admin GET /api/v1/auth/admin/client-address) answers the same { clientAddress, trustProxy }: the address the access_policy.allowed_ip_ranges save check judges. The caller is resolved through core's resolveRequestExecutionContext with the settings door's parameter set, and admitted by judgeOrganizationAddressReader (client-address-read.ts): no caller is 401 UNAUTHENTICATED; a held session is 403 with its posture's code (judgeHeldSession); a caller without setup.write in their current organization is 403 PERMISSION_DENIED; an unreachable permission store is 503 SERVICE_UNAVAILABLE. It answers the caller's own address only: the query set is closed and empty (400 VALIDATION_ERROR for any parameter), and the answer is Cache-Control: no-store. The route is a new row in auth-route-ledger.ts (objectstack-mount, server-only) and in the ledger conformance pin.

D3: an objectstack mount's in-process session read judges the organization's IP list. A middleware on ${basePath}/* establishes the request's address with AuthManager.resolveClientAddress (the D5 gate's rule: server.trustProxy decides whether forwarded headers count) and runs the rest of the request in a better-auth request state carrying it (runAtMountDoor, request-client-address.ts). customSession passes that address as the door to enforceSessionControls when no ctx.request is present (readMountDoor), so gateAdmin, the authorized-AI-assistant routes, session revoke, add member and the other routes that call auth.api.getSession in process end a session outside the organization's Allowed IP ranges (revoke_reason: ip_not_allowed), as better-auth's own routes already did. Outside a mount no door is open, so data requests outside the auth mount are judged by neither list, as before. Measured that the mount has an established address (peer, or forwarded under trustProxy), so the seat's ruling (pass the address) applies.

D2: no heartbeat before the switch judgement, through the real route. A new pin drives POST /organization/set-active on a real better-auth instance: a session idle 30 minutes switching into an organization with a 10-minute idle limit is answered 401 UNAUTHENTICATED, its row ends with idle_timeout, stays scoped to the old organization, and last_activity_at is unchanged; the control (idle 2 minutes) answers 200.

D1: sys_session.revoke_reason's description names ip_not_allowed (sys-session.object.ts); the six translation bundles carry it (en regenerated by check-i18n-bundles.mjs --write, the other five hand-edited the same way).

S6: the organization form's notices are removed. access_policy.mfa_required has no reader a form notice would serve: its save refusal already names two-factor itself, and sys_organization.require_mfa is read-only and off the form, so nothing on the form turns the requirement on. Per the seat ruling, organizationMfaNotices.ts, its test, the AppContent.tsx call (the findOne of the stored column and the description spread on the save toast) and the two console.organizationMfa.* keys in all eleven packs are removed; the German quote-pairing census moves by one span.

New test files: packages/plugins/plugin-auth/src/organization-access-policy-doors.test.ts (D3 on a real better-auth instance, D2 through the route; a new file because #31175 is in flight on organization-access-policy.test.ts) and packages/qa/dogfood/test/org-admin-client-address-read.dogfood.test.ts (S7 end to end on a booted stack).

Why

The card's acceptance: "An org admin reads their judged address; a plain member is refused." / "No dead notice code remains." D1, D2 and D3 are the should-fixes carried from the #31013 contract reviews.

Tests

  • allowed-ip-ranges.pin.test.ts: the organization read answers what the platform read and the save's judged address answer across three transports (peer, forwarded under trustProxy, forged header without it); the platform route refuses a non-platform-admin 403 PERMISSION_DENIED; anonymous 401 UNAUTHENTICATED, plain member 403 PERMISSION_DENIED, held 403 MFA_REQUIRED, each refused before the address is read; store outage 503 SERVICE_UNAVAILABLE; three query parameters each 400 VALIDATION_ERROR; the mount middleware's door (peer, forwarded, forged, unestablished, and none outside a mount).
  • organization-access-policy-doors.test.ts: D3 (outside the list ends the session ip_not_allowed and the next read is null; an unestablished address is refused under a set list; no door, no IP judgement) and D2 (above).
  • Every refusal case asserts code and status.

Acceptance notes

Verification

Final head 78ac3b00 (the branch merged with origin/main once, after #31194 changed plugin-auth). Every heavy run went through scripts/pm/os-verify-lock.sh. Head 4b3757cb moves it by changeset prose only (the two removed keys were never released); on it check-changeset-no-major --base origin/main and check-adr-0087-registration --base origin/main exit 0.

Red first and ablations
  • Red first (the new tests against the base sources): 12 failed.
  • readMountDoor in customSession replaced by undefined: 2 failed (expected undefined to be 'ip_not_allowed'), restored, git diff HEAD empty.
  • The mount middleware made to call bare next(): 3 failed (door undefined), restored.
  • The setup.write check disabled: the plain-member case red (the address was read), restored.
  • A heartbeat written before the switch judgement: the set-active case red (200, not 401), restored. A first attempt at this ablation was a no-op (its replacement contained the anchor, so the anchor count stayed 1); re-run with a replacement that does not contain it, which landed and went red.
Suites, type-checks, lint

On 78ac3b00:

  • pnpm --filter @objectstack/plugin-auth test: exit 0, Test Files 236 passed (236), Tests 4199 passed, 10 skipped.
  • From ui/, pnpm exec vitest run packages/i18n/src/__tests__/ packages/app-shell/src/console/__tests__/AppContent.recordSaveOneToast-27999.test.tsx: exit 0, Test Files 99 passed (99), Tests 1756 passed, 13 skipped.

On 0a6d22c6 (before the ledger-pin fix and the merge; neither touches these packages' sources):

  • pnpm --filter @objectstack/plugin-auth typecheck (with tsconfig.examples.json and check:test-typecheck): exit 0.
  • pnpm --filter @objectstack/platform-objects test: exit 0, Test Files 119 passed (119), Tests 1701 passed. typecheck: exit 0.
  • pnpm --filter @object-ui/i18n run type-check and pnpm --filter @object-ui/app-shell run type-check (after pnpm --filter '@object-ui/app-shell^...' build): exit 0 each.
  • Scoped lint: eslint packages/plugins/plugin-auth/src packages/platform-objects/src plus the new dogfood file --no-inline-config: exit 0, no output. From ui/, eslint on the edited AppContent.tsx, its test, the locale packs and the de-quote test: exit 0, 0 errors, 50 warnings, all on AppContent.tsx lines this PR does not edit.
  • The full plugin-auth run on 0a6d22c6 had two reds: the route-ledger conformance pin (fixed in 4fd8b6be by naming the new route) and the boundary flake named under Acceptance notes; both files pass on 78ac3b00.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 78ac3b00: 86 commands, each run with stdin from /dev/null and its exit captured before any pipe: 86 exit 0. --ran with each line recorded as COMMAND :: exit CODE: "86 derived famil(ies) accounted for — 86 run, 0 NOT-MEASURED (a DERIVED zero — all 86 recorded an exit code and none of them is 3)". The lock's verdict: VERDICT command-exit 0.

On the earlier head (78 commands), four reds were fixed before this run: check:engine-double-contract (the new test's doubles, ledger rewritten with --write), check:test-source-alias (a dynamic @objectstack/core import in a test body made static; the edited AppContent test warms @objectstack/lint at module top), check-plugin-teardown-shape --self-test (exit 3 until the pinned fixture commit was fetched) and check:dual-build-cjs-loads (exit 3 until the closure was built).

Dogfood battery (final head)

78ac3b00 · after pnpm exec turbo run build --concurrency=2 --filter='@objectstack/dogfood^...' (exit 0) · pnpm --filter @objectstack/dogfood test under the lock: VERDICT command-exit 0 · Test Files 436 passed, 3 skipped (439) · Tests 3613 passed, 41 skipped (3654) · no failures. The new file alone: Test Files 1 passed, Tests 4 passed.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (Except the maintainer digest, which os-dev.md writes in Chinese.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…ress their access-policy list is judged by

GET /api/v1/auth/client-address answers the caller's own judged address
(the same answer as the platform route) to a caller holding setup.write in
their current organization, the access_policy manifest's writePermission.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
…es the organization's IP list

The auth mount middleware carries the address the D5 gate established to the
session reads a raw route makes in process, so customSession judges the
organization's allowed IP ranges there as on the better-auth-served door.
Pins the switch idle rule through the real /organization/set-active route.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
… require_mfa column

The column is read-only and off the editable whitelist, so neither notice
could fire. The access policy's MFA level is a settings key with no record
form to serve. Also names ip_not_allowed in sys_session.revoke_reason's
published vocabulary.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
…on vocabulary; changeset

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
…eclare Skills-docs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
…he organization client-address read

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, touching 28 documentable anchor(s).

31 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ea62933c2239b4c35b675f6af7d6812e01173edc.

⛔ 14 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: PERMISSION_DENIED (literal, 51 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 226 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ea62933c2239b4c35b675f6af7d6812e01173edc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 63de9fdf9081a78e12897c8ff4ec3533af5c34c6 — the merge of head fe4a965322d1a2b138f739200961c800daab4f94 into base ea62933c2239b4c35b675f6af7d6812e01173edc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 63de9fdf9081a78e12897c8ff4ec3533af5c34c6 && git checkout 63de9fdf9081a78e12897c8ff4ec3533af5c34c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ea62933c2239b4c35b675f6af7d6812e01173edc fe4a965322d1a2b138f739200961c800daab4f94 && git checkout -B drift-repro ea62933c2239b4c35b675f6af7d6812e01173edc && git merge --no-ff fe4a965322d1a2b138f739200961c800daab4f94

node scripts/docs-audit/affected-docs.mjs --json ea62933c2239b4c35b675f6af7d6812e01173edc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ea62933c2239b4c35b675f6af7d6812e01173edc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 78ac3b0025fb25151a800b4b159ffa2eac467d2e
Local-runs: none
Scope: packages/platform-objects/src/apps/translations/ar.objects.generated.ts, packages/platform-objects/src/apps/translations/en.objects.generated.ts, packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts, packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts, packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts, packages/platform-objects/src/apps/translations/zh-Hant.objects.generated.ts, packages/platform-objects/src/identity/sys-session.object.ts, packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts, packages/plugins/plugin-auth/src/auth-manager-plugin-list.ts, packages/plugins/plugin-auth/src/auth-plugin.ts, packages/plugins/plugin-auth/src/auth-route-ledger.conformance.test.ts, packages/plugins/plugin-auth/src/auth-route-ledger.ts, packages/plugins/plugin-auth/src/client-address-read.ts, packages/plugins/plugin-auth/src/organization-access-policy-doors.test.ts, packages/plugins/plugin-auth/src/request-client-address.ts, packages/qa/dogfood/test/org-admin-client-address-read.dogfood.test.ts, scripts/engine-double-contract.pinned.json, ui/packages/app-shell/src/console/AppContent.tsx, ui/packages/app-shell/src/console/tests/AppContent.recordSaveOneToast-27999.test.tsx, ui/packages/app-shell/src/console/tests/organizationMfaNotices-30295.test.tsx, ui/packages/app-shell/src/console/organizationMfaNotices.ts, ui/packages/i18n/src/tests/de-quote-pairing-3876.test.ts, ui/packages/i18n/src/locales/ar.ts, ui/packages/i18n/src/locales/de.ts, ui/packages/i18n/src/locales/en.ts, ui/packages/i18n/src/locales/es.ts, ui/packages/i18n/src/locales/fr.ts, ui/packages/i18n/src/locales/ja.ts, ui/packages/i18n/src/locales/ko.ts, ui/packages/i18n/src/locales/pt-BR.ts, ui/packages/i18n/src/locales/ru.ts, ui/packages/i18n/src/locales/zh-Hans.ts, ui/packages/i18n/src/locales/zh-Hant.ts + packages/core/src/security/request-execution-context.ts, packages/core/src/security/resolve-authz-context.ts, packages/core/src/security/auth-gate.ts, packages/core/src/security/admission-tenancy-posture.ts, packages/services/service-settings/src/settings-service-plugin.ts, packages/services/service-settings/src/settings-routes.ts, packages/services/service-settings/src/manifests/access-policy.manifest.ts, packages/plugins/plugin-auth/src/platform-admin-gate.ts, packages/plugins/plugin-auth/src/auth-manager-password-session-policy.ts, packages/plugins/plugin-auth/src/auth-manager-request-routing.ts, packages/plugins/plugin-auth/src/organization-access-policy.ts, packages/platform-objects/src/identity/sys-organization.object.ts, packages/spec/src/security/capabilities.ts, docs/adr/0069-enterprise-authentication-hardening.md, ui/packages/i18n/src/index.ts, .changeset/30295-org-mfa-notice-and-enroll-refusal.md

The recogniser returns no hits for this file list (no schema, no governed path); the face is the changeset's Clause-②: yes (narrowing) line, so the floor is every path of the diff outside the changeset, listed above. .changeset/31030-org-admin-client-address.md and scripts/engine-double-contract.pinned.json (regenerated with --write) are read, not reviewed. Check-runs on the head as read: 10 success (Auto Label, Check Changeset, Check PR Size, Dogfood Regression Gate, Flag docs affected, Governed Surface Queue Guard, PR-body guards, Plan Test Core shards, Spec property liveness, filter), 8 skipped (opt-in and schedule gates), 22 still in progress (Build Core, Build UI Deps, the seven Lint jobs, Live E2E UI, Test Core 1-8, the four Type Check jobs). None failed at read time; landing still waits on the running set per the Tier S rule.

① Derived judgments

  • New served read GET /api/v1/auth/client-address (auth-plugin.ts, mounted ahead of the catch-all, beside GET /admin/client-address): a public-surface widening. Right. The route row is in AUTH_ROUTE_LEDGER (objectstack-mount, server-only, tracker id in the comment and not in the note) and in the conformance pin's objectstack-mount list; the ledger note names the gate, the closed query set and no-store correctly against the code.
  • Accept set of that read: the query set is closed and empty (runClientAddressRead refuses any parameter 400 VALIDATION_ERROR through unknownQueryParamRefusalMessage, before trustsForwardedHeaders is asked); no body. Right, and pinned (?userId, ?organizationId, ?address).
  • Its gate (judgeOrganizationAddressReader, client-address-read.ts): no principal 401 UNAUTHENTICATED; a held posture 403 with the posture's code; no setup.write in systemPermissions 403 PERMISSION_DENIED. Read against the cited code: resolveOrganizationSettingsCaller passes the settings door's exact parameter set (settings-service-plugin.ts:493-508: fail-closed, authGate: 'resolved', api-key-and-session, sessionApi optional, session request, localization/accessToken withheld), differing only in the tenancy resolver (the plugin's own TenancyService instead of the kernel lookup; both satisfy TenancyServiceResolver, and null reads as no posture). judgeHeldSession({ authGate }) reaches normalizeAuthGate, which reads .authGate.code against the closed vocabulary, so the held arm works on the resolved AuthGate. setup.write is declared scope: 'org' in capabilities.ts and lands in systemPermissions; the settings door's union with permissions is defensive, not a gap. Right.
  • GET /api/v1/auth/client-address on a store outage: AuthzStoreUnavailableError is relayed 503 SERVICE_UNAVAILABLE in the shared seam's words. Right. An ApiKeyLifecycleRefusalError (a revoked, expired or inactive key, which resolveRequestExecutionContext propagates and credentials: 'api-key-and-session' reaches) falls to the generic 500 INTERNAL_ERROR arm and an error log line, where the settings door relays err.status/err.code (settings-routes.ts:175). Fail-closed, so not a disclosure, but a wrong status class: should-fix S1 below.
  • D3, customSession door (auth-manager-plugin-list.ts): ctx?.request ? { address: readRequestClientAddress() } : await readMountDoor(). A better-auth-served read keeps the request door; an in-process read at an objectstack mount takes the mount door; a programmatic read with no door open still judges limits only. Right. enforceSessionControls judges the list only when door && organizationId && allowedIpRanges.length > 0, ends the session ip_not_allowed, and judgeSessionScopingAddress admits a platform admin regardless. The { address: undefined } door is refused under a set list, which is D5's own rule for an unestablishable address.
  • Mount middleware (auth-plugin.ts rawApp.use(basePath/*)): registered after the D5 gate and the agent-forward middleware and before every rawApp.get, so it wraps every raw route and the catch-all. The catch-all's handleRequest still opens its own fresh runWithRequestState(new WeakMap(clientAddressStateEntries(address))) (auth-manager-request-routing.ts:190), so the sign-in address stamp and the HTTP door's slot are undisturbed by the outer door. The in-process auth.api.getSession reusing the open store is not something I could read from better-auth's source (this clone carries no node_modules); it is pinned on a real better-auth instance by organization-access-policy-doors.test.ts (red-first 12, ablation 2 red), which is the right pin for a vendor-internal dependency.
  • D1: sys_session.revoke_reason description names ip_not_allowed; six *.objects.generated.ts bundles carry the same insertion (en regenerated, five hand-edited identically). Right; the generated files are not reviewed beyond that read.
  • D2: POST /organization/set-active pinned on the real route: 401, idle_timeout, scope unchanged, last_activity_at unchanged; control 200. Right.
  • S6 narrowing: console.organizationMfa.needsTwoFactor and .nowRequired leave all eleven packs; organizationMfaNotices.ts, its test, the AppContent.tsx call, the findOne of the stored column and the save-toast description spread are gone; the de-quote census moves 110 to 109. git grep organizationMfa on the head finds no remaining reader outside the two changesets and the census comments. organizationMfaNotices.ts was not exported from @object-ui/app-shell's index, so app-shell's change is internal. Right.

② Semver level

  • .changeset/31030-org-admin-client-address.md: @objectstack/plugin-auth patch (a new route, Metadata-protocol: none, so patch per the kind rule), @objectstack/platform-objects patch (a description string), @object-ui/i18n patch with the BREAKING banner (a non-protocol narrowing ships patch and keeps its banner), @object-ui/app-shell patch (an internal module removed). Right for what the diff publishes.
  • Clause-②: yes (narrowing) and Metadata-protocol: none on both the PR body and the changeset, line-initial; the title carries !; the ADR-0087 marker not-required (runtime-interface-only ui/packages/i18n/src/locales/en.ts#TranslationKeys) is present and Check Changeset (the job that runs check-adr-0087-registration.mjs --base) is green; the migration table maps both removed keys to their replacement (none, with the reason). The seat's claim amendment 6710714517 already settled widening to narrowing; the declaration matches the diff against main.
  • One accuracy point: .changeset/30295-org-mfa-notice-and-enroll-refusal.md, which ADDED these two keys, is still pending on this head and @object-ui/i18n's CHANGELOG (18.3.0) does not carry them, so no released pack ever had them. The body's sentence "the published packs drop" over-states a consumer-visible break; the declaration is conservative relative to main and follows the 30206 precedent, so it stands. Should-fix S2 (wording only).

③ Boundary flags

④ Design judgment

The narrowing against the published spec on main. sys_organization.require_mfa is declared readonly: true, labelled retired, its description sends the author to Settings → Access policy, and the D2 guard strips a user-context write (sys-organization.object.ts:413-432); ADR-0069 as amended 2026-11-06 puts the requirement in access_policy.mfa_required with three levels, whose save refusal (organization-mfa-enrollment-guard.ts, ORGANIZATION_MFA_REFUSAL_COPY) already names two-factor itself. A form notice over a column no form can write serves no reader, so removing it makes the spec's own retirement real at the consumer; it takes no declared capability away. The card's Expected S6 allowed "removed, or re-pointed", the seat ruled removal, and the migration table says why neither key has a replacement. The removal breaks no compile (t()'s key is a string; TranslationKeys is typeof en, re-exported from @object-ui/i18n and @object-ui/react, and no workspace consumer names the two keys). Holds.

The new read, tried as an attacker. Who may call: any principal a session or API key admits whose systemPermissions, resolved for the session's VETTED active organization, hold setup.write. grantAppliesInTenant applies a grant row only in the organization it names, a caller with no active organization gets no stored grant at all, and an organization claim the membership no longer backs is dropped and re-resolved (resolve-authz-context.ts:797-815), so a member cannot borrow another organization's setup.write by claiming it; the only way in is to BE an administrator of the organization the session is scoped to, which is the caller the save check admits. A platform administrator is admitted only through setup.write, never through the admin anchor (the dogfood pin shows the converse: an organization administrator is still refused at /admin/client-address). What it discloses: the caller's own connection's judged address, and trustProxy. The address was already disclosed to this caller by the save refusal's constraint.clientAddress; trustProxy was platform-admin-only before and is now readable by every organization administrator, but it is a boolean the same caller can already infer by comparing the save refusal's address against a forwarded header, and it is needed to read the answer at all behind a proxy; acceptable, and judged. Can it answer for another organization or user: there is no parameter of any kind, the query set is closed before the address is read (pinned on three names), and the answer is a transport fact of THIS connection, so there is nothing organization-scoped to redirect; the organization enters only through the gate. Cache-Control: no-store keeps one connection's answer off a shared cache. The route sits under both the D5 deployment gate and the new mount door, so a caller already outside a list in force is refused or ended before the read; a caller whose session is held is refused with the posture's code before any address is read (pinned, trustsForwardedHeaders not called). The 500 arm returns a generic message, which is tighter than the platform route's err.message. No must-fix.

D3 design, tried against the spec: TL;DR 6 and D5 say the organization's list is judged on login/session at the auth mount with the one trust rule, and an unestablishable address is refused while any range is set. The mount door carries resolveClientAddress under the same trustProxy rule the D5 gate just applied, opens { address: undefined } when none is established, and leaves no door outside the auth mount, so data requests are judged by neither list as before. The one risk I looked for, the outer door shadowing or being shadowed by handleRequest's own store, does not arise: handleRequest opens a fresh store with the address entries, and customSession there has ctx.request. The design holds as implemented; the dependency on better-auth reusing an open store for in-process endpoint calls is pinned on the real instance, which is the right place for it.

Findings:

  • S1 (should-fix): GET /api/v1/auth/client-address answers an ApiKeyLifecycleRefusalError with 500 INTERNAL_ERROR and an error log line; relay it as the settings door does (err.status, err.code, err.message) beside the 503 arm, and pin one case (a revoked key).
  • S2 (should-fix, wording): the changeset's narrowing sentence says the published packs drop the two keys; they were added by the still-pending .changeset/30295-...md and never released, so say so, or the CHANGELOG will read as a consumer-visible break in the same version that adds them.
  • S3 (should-fix, outside the diff): file the follow-up card for the console's Access policy page to read the new route; the dev's seam has carrier: none.

Implemented-by: claude/project-thread-em08na
Reviewed-by: session_01FEkQ5p3T5YWhLjurajudnQ

VERDICT: PASS

Seat disposition of the should-fixes: S2 (changeset wording, prose only) goes to the dev now; S1 (the revoked-key status on the new read, a code change that would re-open this narrowing review and the dogfood battery) and S3 (the console Access policy page reading the new route) are filed as follow-up cards before ACCEPT.

…set and were never released

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

席位核对 · ACCEPT · 2026-11-07T20:51Z · head 4b3757cb7147497b0427bee7ae7532fe83385187

Seat domain:server#6 reviewed the diff of its own dispatch (claim 6710570984 and amendment 6710714517 on #31030, card thread session_01FEkQ5mEgSFeDT7pQA12if9, report 6712418683). Read against GitHub, not the report.

Regen-provenance: 6712531628 · 78ac3b0025fb25151a800b4b159ffa2eac467d2e → 4b3757cb7147497b0427bee7ae7532fe83385187 · git diff --name-only → .changeset/31030-org-admin-client-address.md

  1. Shape: 34 files, +834 / -468. New served read GET /api/v1/auth/client-address for an organization administrator (setup.write in the session's vetted organization; closed empty query set; held posture refused before any address is read; Cache-Control: no-store), in the route ledger and its conformance pin. D3: the customSession door judges an organization's IP list for in-process reads at the auth mount (mount middleware), D1 revoke_reason names ip_not_allowed, D2 pinned on the real set-active route. S6: the console's retired organization-MFA notices and their two @object-ui/i18n keys removed (no remaining reader). Title carries !. Body says Fixes #31030; no other closing keyword.
  2. Premise: on origin/main an organization administrator could not read the address the server judges before saving an IP list (only the platform-admin GET /admin/client-address), per the card; the dev re-checked D1–D3 against 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).
  3. Gates / check-runs: 68 runs on 4b3757cb, 58 success, 10 skipped, 0 red, including Test Core 1–8/8, Test UI 1–16/16, Type Check · workspace and UI, Lint, Live E2E UI. mergeable_state clean. The aggregate red on 78ac3b00 is that superseded head's cancelled lanes. Dev: 86/86 derived gates exit 0; the full dogfood battery (! / narrowing) ran on 78ac3b00, VERDICT exit 0 (436 files passed, 3 skipped, 3613 tests); the hop to 4b3757cb is changeset prose only, so the battery's tree is unchanged; check-changeset-no-major and check-adr-0087-registration exit 0 on the new head.
  4. Contract review: Clause-②: yes (narrowing) reviewed at CONTRACT_REVIEW_TIER: PASS 6712531628 on 78ac3b00, no must-fix. Should-fix S2 (the two removed keys were never released) is the one prose commit above, read sentence by sentence: true of the tree (.changeset/30295-… still pending, ui/packages/i18n/CHANGELOG.md names neither). S1 (a revoked API key answers 500 on the new read) is filed as plugin-auth: GET /api/v1/auth/client-address answers a revoked or expired API key with 500 INTERNAL_ERROR instead of its own refusal #31259; S3 (the console Access policy page reads the route) as console: Settings → Access policy does not show the administrator the client address the server judges (GET /api/v1/auth/client-address) #31260. PR body rewritten with the same sentence.
  5. Out of scope / Skills-docs: the 60-minute boundary flake was plugin-auth test: organization-access-policy.test.ts sets a session exactly 60 minutes old against a one-hour limit and expects admitted, so it goes red whenever a millisecond passes (main red on 56baf78e) #31175 (merged). Metadata-protocol: none; Skills-docs: none as the body says (no skill or doc teaches the per-organization policy; noted as a docs gap, not a condition here). Nothing else filed.
  6. Landing: check-governed-merges --pr 31258: not governed, 1302 changed lines. No packages/spec/src path. Changeset: patch for plugin-auth, platform-objects, @object-ui/app-shell, and @object-ui/i18n with its BREAKING banner and ADR-0087 not-required (runtime-interface-only …) marker, holds. Landing on the maintainer's standing words 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T16:59Z). Relay pr_ready, then automerge_enable.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mEgSFeDT7pQA12if9
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

席位核对 · ACCEPT · 2026-11-07T21:49Z · head fe4a965322d1a2b138f739200961c800daab4f94

Seat domain:server#6 re-reads its own dispatch after the merge queue removed the PR on a conflict with PR #31249 in packages/plugins/plugin-auth/src/auth-plugin.ts. This supersedes ACCEPT 6712868712 on 4b3757cb.

Regen-provenance: 6712531628 · 4b3757cb7147497b0427bee7ae7532fe83385187 → fe4a965322d1a2b138f739200961c800daab4f94 · git diff --name-only → main's carry-over only (the PR's own net diff is identical on both heads)

  1. The hop: one merge commit fe4a9653 (Merge origin/main, no rebase) whose second parent is ea62933c (feat(plugin-auth, plugin-audit): a sign-in the IP allow-list refuses writes one login_failed row, bounded per address #31249). Measured on the committed trees: the net diff merge-base..4b3757cb (base fe97d85b) and the net diff merge-base..fe4a9653 (base ea62933c) list the same 34 paths and the same added and removed lines, byte for byte. So the conflict resolution changed no line of this PR's own diff, and everything else the hop brought is main's. Under the head-move exception (main's carry-over is outside the review surface) the fable CONTRACT_REVIEW_TIER record 6712531628 (PASS, Clause-②: yes (narrowing)) still governs.
  2. Gates / check-runs: 64 runs on fe4a9653, 56 success, 8 skipped, 0 red. mergeable_state clean, and the head merges cleanly with current main 07559ce5 (git merge-tree no conflict). The dogfood battery ran on 78ac3b00; the PR's own tree has not moved since, and CI's Dogfood Regression Gate reads green on this head with main merged in.
  3. Unchanged since 6712868712: shape (34 files, +834 / -468, ! in the title, Fixes #31030), PR body, changeset with its BREAKING banner and ADR-0087 marker, S1 filed as plugin-auth: GET /api/v1/auth/client-address answers a revoked or expired API key with 500 INTERNAL_ERROR instead of its own refusal #31259 and S3 as console: Settings → Access policy does not show the administrator the client address the server judges (GET /api/v1/auth/client-address) #31260. check-governed-merges --pr 31258: not governed, 1302 changed lines. No packages/spec/src path.
  4. Landing: on the maintainer's standing words 「按照 pm-dispatch 规则复核过的都授权」 (2026-10-25T11:35Z) and 「你应该使用 pm-dispatch 技能审核和落地pr」 (2026-11-07T16:59Z). The PR is already out of draft; relay automerge_enable.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42417800944 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (2/8) — 失败步骤: Run this shard's tests

    @objectstack/scenarios:test:  FAIL  test/runner/external-driver.test.ts > the persona keys, end to end over the external driver — the record, the scorecard and the gallery never carry one > a full crm
      ↳ 失败原因: @objectstack/scenarios:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 15 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Queue triage · 2026-11-07T22:18Z

The merge queue removed this PR on a red Test Core (2/8) in its merge group (run 42417800944, job 128540015300): @objectstack/scenarios test/runner/external-driver.test.ts › "a full crm" timed out at 5000 ms. Not this PR's: the diff touches no packages/scenarios path (plugin-auth, platform-objects and console only), the failure is a timeout, and the same file red-lined two other PRs' groups in the same speculative stack (#31247, #31274), anchored on #31285. Re-queued once, unchanged; a second red on the same test is treated as real and taken up on #31285.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Queue triage · 2026-11-07T23:17Z · held, not re-queued

Third removal (merge group run 42422774934, base 59d9815a, which carries the scenarios fix PR #31284): Test Core (8/8), examples/app-todo/test/loop-body-iteration.test.ts › "ESCALATION: the run reaches notify_owner and the notice renders the real day count" timed out at 5015 ms. Anchor #31294 lists two independent hits on that file in 24 hours, #31249 and this PR, both plugin-auth sign-in / session changes from this lane, and the two earlier removals of this PR were timeouts too. That pattern is enough to stop treating it as load: this PR adds an organization IP-list judgment to in-process session reads at the auth mount (D3), which every authenticated request in an example boot passes through. Held out of the queue until the dev measures the two timed cases (loop-body-iteration.test.ts and packages/qa/scenarios/test/runner/external-driver.test.ts) on main and on this head merged with main, several runs each, and reads the per-request cost of the D3 read. Re-queued only on that measurement; a real slowdown is fixed in this PR.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Queue triage · 2026-11-07T23:31Z · hold lifted

The measurement the hold asked for is 6714482295 on #31030: this head merged with main 59d9815a versus main, five runs per side under the lock, every run exit 0. app-todo ESCALATION median 341 ms vs 339 ms; scenarios "a full crm run" median 2876 ms vs 2907 ms. Neither file reaches the D3 read (the scenarios file imports no plugin-auth; app-todo touches it only through the rate-limit-storage subpath this PR leaves alone), and the read runs only for a session whose organization has a non-empty Allowed IP ranges list. A case that takes about 0.34 s locally and timed out at 5015 ms in the queue is shard load, as anchors #31285 and #31294 record. Head unchanged (fe4a9653); re-queued.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 7, 2026
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 42425740739 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (8/8) — 失败步骤: Run this shard's tests

    @objectstack/example-todo:test:  FAIL  test/loop-body-iteration.test.ts > #19206 — the loop body runs and binds `currentTask` > ESCALATION: the run reaches `notify_owner` and the notice renders the re
      ↳ 失败原因: @objectstack/example-todo:test: Error: Test timed out in 5000ms.
    @objectstack/example-todo:test:  FAIL  test/loop-body-iteration.test.ts > #19206 — the loop body runs and binds `currentTask` > EVERY item is processed, and each one carries its own values
      ↳ 失败原因: @objectstack/example-todo:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 4 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 19 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Queue triage · 2026-11-07T23:55Z · held until the app-todo budget fix lands

Fourth removal (merge group run 42425740739, Test Core (8/8)): examples/app-todo/test/loop-body-iteration.test.ts, now two cases ("ESCALATION …" and "EVERY item is processed …") at vitest's 5 s default, file total 12026 ms. The measurement 6714482295 stands (this head is not slower; the case runs in about 0.34 s locally), and this PR's own new tests in that shard are modest (allowed-ip-ranges.pin 4.7 s, organization-access-policy-doors 3.6 s, beside 88.8 s and 26.8 s plugin-auth posture files already on main). The case is being fixed where it lives: anchor #31294, fix card #31298 (p1), claimed by domain:server#2 (6714705088). Re-queueing before that lands only spends another full queue build, so this PR stays out of the queue, head fe4a9653 unchanged, and the seat re-queues it once #31298's fix is on main.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-project-manager · session_01FEkQ5p3T5YWhLjurajudnQ

Queue triage · 2026-11-08T02:06Z · hold lifted

The hold waited on #31298. It closed on PR #31312, merged cba7d8ae on main 2026-11-08T01:31:00Z: examples/app-todo/test/loop-body-iteration.test.ts now boots one kernel per file under a measured 30 s budget, so the 5 s per-case timeout that ejected this PR twice can no longer be paid by the boot. The merge group builds on main with that fix. Head unchanged (fe4a9653), mergeable_state clean, ACCEPT and Regen-provenance 6712531628 still read this head; re-queued.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit f5e23e2 Nov 8, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-em08na branch November 8, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant