Repository navigation
plugin-auth: MCP clients cannot register over OAuth DCR — /oauth2/register defaults a missing application_type to web and refuses the http://localhost redirect every local MCP client sends #25118
Description
Ruled: 6329352251 · letter A1 · 2026-10-22T00:05Z
Filing gate ① a reproduced defect — reach: measured live on ObjectOS Cloud production, https://huayan.objectos.ai (framework pin 9d13b5fc83, @better-auth/oauth-provider 1.7.3), 2026-10-21, with Claude Code 2.1.159 as the MCP client. Filed at the maintainer's request in a cloud session.
Reader (who acts): objectstack triage → the plugin-auth lane.
What fails
Every MCP client that authorizes through Dynamic Client Registration with a loopback redirect — Claude Code, and by the same shape Cursor, VS Code and Codex CLI — cannot connect to an ObjectStack MCP endpoint over OAuth. Registration is refused before the browser ever opens:
SDK auth failed: web clients require https redirect URIs on non-loopback hosts: http://localhost:63678/callback
The OAuth track is otherwise healthy on the same host: /api/v1/mcp answers 401 with WWW-Authenticate: Bearer … resource_metadata=…, /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server/api/v1/auth both answer 200. Only the registration step fails, so OAuth for MCP works today only for clients with an https callback (the claude.ai web connector). API keys are the sole working path for local agents.
Reproduction (no state written — the request is refused)
curl -sS -X POST https://<env-host>/api/v1/auth/oauth2/register \
-H 'content-type: application/json' \
-d '{"client_name":"probe","redirect_uris":["http://localhost:63678/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}'
# → 400 {"error":"invalid_redirect_uri","error_description":"web clients require https redirect URIs on non-loopback hosts: http://localhost:63678/callback"}This is the body shape an MCP client sends: a public client (token_endpoint_auth_method: none), PKCE, a loopback http redirect, and no application_type.
Cause
@better-auth/oauth-provider 1.7.3 applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") fills a missing application_type with web (the OIDC DCR default), and validateClientRedirectUri refuses any http or loopback redirect for web. The default is not configurable from plugin options. Only the client-metadata-document path defaults to native.
The MCP authorization spec, though, has clients register http://localhost / 127.0.0.1 redirects (RFC 8252 §7.3), and the clients in the field do not send application_type. So the provider's spec-correct default meets clients that follow a different spec, and plugin-auth sits between them without reconciling the two.
Not a duplicate
Dedupe: REST search/issues, closed included —
repo:objectstack-ai/objectstack "web clients require https"→ 5 (OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773, fix(platform-objects): the OAuth register form offers only Web and Native, and its Redirect URLs help states the rule for each type #25011, fix(plugin-auth): a refused OAuth application registration answers the status the refusal declared, not 500 #24853, QA run · ai (8/10) · 5b14b7f3 · 2026-10-19 · 8 PASS / 0 PARTIAL / 0 FAIL / 0 BLOCKED / 2 NOT-RUN #24540, docs(qa): re-point the half-B platform-checklist items that described an older product #24141)repo:objectstack-ai/objectstack "oauth2/register" localhost→ 6 (OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773, fix(plugin-auth): the register form's Application Type reaches the OAuth provider as application_type #24992, feat(cli):objectstack dev --cert/--keyterminates TLS in the dev process, and the canonical origin follows the listener #17725, fix(plugin-auth)!: the OAuth register door refuses an application type the provider does not judge with 400 INVALID_REQUEST, where it registered it as web #25038, fix(hono): mount /auth where the auth service serves, and refuse a prefix it cannot serve under #16380)repo:objectstack-ai/objectstack "application_type" register→ 13 (fix(plugin-auth): the register form's Application Type reaches the OAuth provider as application_type #24992, fix(plugin-auth)!: the OAuth register door refuses an application type the provider does not judge with 400 INVALID_REQUEST, where it registered it as web #25038, OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773, sys_oauth_application.type declaresuseragentbasedandpublic, which no writer produces, and cannot hold theuser-agent-basedornullthe OAuth provider has written #25053, fix(plugin-auth): a refused OAuth application registration answers the status the refusal declared, not 500 #24853 …)
The nearest is #24773 (closed 2026-10-21, fixed by #24992 / #25038 / #25011): the same provider message, but from the Setup form's door POST /api/v1/auth/sys-oauth-application/register, where an administrator now picks Web or Native. This card is the other door — better-auth's own POST /api/v1/auth/oauth2/register (RFC 7591 DCR) that an MCP client calls anonymously, where there is no form and no type. Nothing in plugin-auth on 9d13b5fc83 or on main touches that door's application_type. #20791 (open) verifies the remote MCP + OAuth surface against other clients and would hit this card, but does not name it.
Suggested fix (for the lane to judge)
A before-hook on /oauth2/register in plugin-auth: when the body has no application_type, token_endpoint_auth_method is none, and every redirect_uri is http on exactly localhost, 127.0.0.1 or [::1], register it as native. Every other body keeps the provider's default and its rule. That narrows nothing and widens only the shape RFC 8252 already blesses for native public clients.
Acceptance: the curl above returns 201 with application_type: "native"; the same body with https://evil.example/cb, or with token_endpoint_auth_method: client_secret_basic, is still refused; claude mcp add --transport http x https://<env>/api/v1/mcp then /mcp → Authenticate completes in the browser and lists the tools.
Activity
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 1 · 2026-10-21T22:49Z · p1,
domain:services, ruling A (6327761812, the maintainer's 「A:用提供方 infer (推荐)」)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25118-dcr-infer-nativeWorktree:
objectstack-issue-25118Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)Ruling-ref: 6327761812
File surface (line anchors at
origin/mainabc25a353):packages/plugins/plugin-auth/src/auth-manager.ts: theoauthProvider({ … })options (about:4311–:4380), one option,clientRegistrationDefaultApplicationType: 'infer', and its comment. Nothing else in the file.packages/plugins/plugin-auth/src/;.changeset/25118-*.md(apatch, by changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105's ruling A).auth-plugin.ts, notpackages/spec/**, not the Setup form's door (sys-oauth-application, OAuth app registration rejects http://localhost and http://127.0.0.1 redirect URIs as "non-loopback", with HTTP 500 #24773), notpnpm-workspace.yamlor any lockfile.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")Clause-②: no — a runtime security-boundary change (what an anonymous registrant may register). The dispatch rules class that as the human-floor security class, and the maintainer ruled it (A).
Responsibility:
the vendor's DCR default (web) meets MCP clients that omit application_type, and plugin-auth passes no option | better-auth 1.7.12's clientRegistrationDefaultApplicationType: 'infer', unset today | every local MCP client (Claude Code, Cursor, VS Code, Codex CLI) that registers anonymously over OAuth; measured on productionThread-read: 6327783026
Serial constraints cleared:
03966ceae) is onmain, andplugin-auth/package.jsonpins@better-auth/oauth-provider1.7.12.openandemail_domainpostures, a sign-up for an address that already has an account answers the success shape and mails that address (ruling 1B on #25040); without a live mail transport the explicit refusal stays #25060 (claim6327912966) editsauth-manager.tsin other regions (about:2150,:2767–:2830,:5130,:5495–:5560). The file is shared, so whichever lands second mergesmain.plugin-authtest file, which is disjoint. No other open PR touchesplugin-auth.Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
os-dev-report
{
"issue": 25118,
"status": "needs_decision",
"branch": "claude/issue-25118-dcr-infer-native",
"pr": null,
"session": "session_01UnHcTADjeg3VwxF21QHj7W — the parent's harness-stamped id (subagent run of the dispatching seat)",
"premise_still_valid": true,
"summary": "Stopped at the ruling's first measurement, as ruling 6327761812 directs ('a mismatch is reported as a fork'). No code was written and no PR was opened. The defect is real and the ruled mechanism exists on 1.7.12. With the options AuthManager actually produces, the card's body (public client, loopback http redirect, no application_type) gets 400 'web clients require https redirect URIs on non-loopback hosts'. Adding clientRegistrationDefaultApplicationType: 'infer' turns it into 201 application_type native. GAP 1 CONTRADICTS the decision's assumption. 'infer' classifies by redirect URIs only and never reads token_endpoint_auth_method. So an anonymous CONFIDENTIAL registration that omits application_type and lists a loopback or custom-scheme redirect also becomes native and receives a client_secret: 400 today, 201 with infer (cells R2, R2b, R9). GAP 2 HOLDS: the authorize step without PKCE is refused for every DCR client in every cell. Two facts bound the fork. (F1) The same confidential registration is already accepted anonymously on main today when the body spells application_type native (R2c: 201, client_secret issued), so infer adds no client shape that can be registered; it changes only what an omitted field means. (F2) PKCE stays enforced for all of these clients. Partition-2 assumptions 1 and 2 hold as measured. Assumption 3 holds by code reading and was not measured with DCR off. The fork, with options along the four axes, is in open_questions.",
"tests": "No product test was written (needs_decision). The measurement was a throwaway vitest probe, never committed. It is archived at /tmp/claude-0/-home-user/1c8ca8e4-330d-5501-a9b1-41cf766c6c6c/scratchpad/issue-25118/zz-probe-25118.test.ts and was deleted from the worktree before cleanup. The probe builds a real AuthManager (OS_MCP_SERVER_ENABLED=true) and captures the exact options it hands oauthProvider (allowDynamicClientRegistration true, allowUnauthenticatedClientRegistration true, clientRegistrationRequirePKCE not passed, clientRegistrationDefaultApplicationType not passed). It then boots the REAL better-auth 1.7.12 + @better-auth/oauth-provider 1.7.12 handler (memory adapter, the auth-manager.mcp-oauth-resource.test.ts harness pattern) twice: once with those options, once with the same options plus 'infer'. It drives POST /api/v1/auth/oauth2/register for each cell, then GET /oauth2/authorize?resource=MCP_URL with and without S256 PKCE for each registered loopback client. Command:
bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/zz-probe-25118.test.ts'. Final run:Test Files 1 passed (1),Tests 2 passed (2),os-verify-lock: VERDICT command-exit 0 · held the lock 18s · waited 328s. This was after thepnpm --workspace-concurrency=2 --filter '@objectstack/plugin-auth^...' buildclosure (VERDICT command-exit 0). Readings as today then with infer: R1 public, loopback, no type: 400 then 201 native. R2 client_secret_basic, loopback, no type: 400 then 201 native with a secret. R2b auth method omitted (provider defaults it to client_secret_basic), loopback, no type: 400 then 201 native with a secret. R2c client_secret_basic, loopback, application_type native: 201 with a secret in BOTH modes. R2d public, loopback, native: 201 in both. R3 public, https://evil.example/cb alone, no type: 201 web in both. R3b public, loopback plus https://evil.example/cb, no type: 400 then 201 native. R3c the same pair with application_type native: 201 in both. R4 public, loopback, application_type web: 400 in both. R5 public, http://evil.example/cb, no type: 400 in both. R6 public, cursor:// custom scheme, no type: 400 then 201 native. R7 public, http://127.0.0.1:5000, no type: 400 then 201 native. R8 public, http://localhost.evil.example, no type: 400 in both. R9 client_secret_basic, cursor:// custom scheme, no type: 400 then 201 native with a secret. Authorize, every registered loopback client in both modes: with PKCE the server sends 302 to /_console/oauth/consent. Without PKCE it sends 302 to the redirect with error=invalid_request, 'pkce is required for public clients' (R1, R2d, R3b, R3c) or 'pkce is required for this client' (R2, R2b, R2c). Code reads in the installed dist (node_modules @better-auth/oauth-provider 1.7.12): the option type is at oauth-DXiqbRzK.d.mts:1469 ('web' | 'native' | 'infer', default 'web'), and clientRegistrationRequirePKCE @default true is at :1444-1446. resolveDynamicRegistrationDefaultApplicationType (authorize-D_jy3s-1.mjs:1718-1722) tests redirect_uris with some(), never the auth method. applyOAuthClientRegistrationDefaults (:1688-1692) fills token_endpoint_auth_method with client_secret_basic and fills application_type only when absent. Every redirect is validated by the resolved type (:1857). registerEndpoint (:1792-1796) throws FORBIDDEN 'Client registration is disabled' when allowDynamicClientRegistration is false. isPKCERequired (utils-BfpTo7XY.mjs:912-920): 'none' always requires PKCE; otherwise requirePKCE ?? true. plugin-auth puts no layer in front of /oauth2/register: auth-manager.ts's global before-hook has no branch for that path, and auth-plugin.ts mounts no raw route on it.","gates": "No diff, so no local gate is owed.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave exit 2: 'this branch changes nothing against origin/main (merge base abc25a3) — nothing to derive'; '--repo objectstack-ai/objectstack ... it holds'. No PR, so there are no CI check-runs to cite.git status --porcelainwas empty andgit diff HEADwas empty at abc25a3 before the worktree was removed.","dogfood_battery": "n/a — no PR (stopped at the ruling's fork before any code)",
"mcp_calls": "0 — no MCP GitHub tool was called; reads went through gh api (REST GET issues/25118, issues/25118/comments, issues/25096/comments for the report shape)",
"api_writes": "1 — POST /repos//issues/25118/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Separately, not a REST write: one git push of the empty branch claude/issue-25118-dcr-infer-native at abc25a3 (the write-route probe and the claim's landing marker). No pr_create, no label write, no assignee write.",
"line_budget": "n/a — no diff",
"files_changed": [],
"deviations": [
"Served-stack readings were NOT MEASURED. Reason: the
pnpm dev:crmpreflight (scripts/check-dev-prereqs.mjs) requires build artefacts for all 68 workspace packages, which is a full build that does not fit a foreground window on the shared lock, and the fork stops the card before the acceptance stage those readings serve. The readings instead come from the vendor's REAL handler built from the exact options AuthManager produces, and plugin-auth places no layer in front of /oauth2/register (see tests).","The browser walk in the ruling (claude mcp add --transport http NAME https://ENV-HOST/api/v1/mcp, then /mcp, then Authenticate) was NOT MEASURED. Reason: it needs a deployed environment, and the card stopped at the fork.",
"Partition-2 assumption 3 (the option changes nothing while DCR is off) holds by code reading only (authorize-D_jy3s-1.mjs:1792-1796). It was not measured with DCR off.",
"In the probe's infer leg, the option was injected into the captured options object rather than written into auth-manager.ts, so that no speculative code was written before the fork is decided."
],
"open_questions": [
{
"question": "Fork on ruling A (decision 6318906173, confidence gap 1). Under clientRegistrationDefaultApplicationType: 'infer' the provider classifies by redirect URIs only. An anonymous CONFIDENTIAL registration that omits application_type and lists a loopback or custom-scheme redirect is therefore registered as native and issued a client_secret. Its token_endpoint_auth_method is client_secret_basic, or omitted, which the provider fills as client_secret_basic. Today it gets 400; with infer it gets 201 (R2, R2b, R9). The decision assumed native classification would take effect for public loopback clients only. Bounding facts: (F1) the same confidential registration is ALREADY accepted anonymously on main when the body spells application_type native (R2c: 201 with a secret), so infer widens no set of registrable clients, only the meaning of an omitted field; (F2) PKCE is still required at authorize for every one of these clients ('pkce is required for public clients' is unconditional in the provider; 'pkce is required for this client' follows from clientRegistrationRequirePKCE defaulting to true, which plugin-auth does not override). A related note that is NOT a fork, for the acceptance wording: infer classifies native when ANY redirect qualifies, as the decision states. https://evil.example/cb ALONE stays web (R3: 201 web in both modes). Paired with a loopback URI in one body, it is judged by the native rule and accepted (R3b: 400, then 201), exactly as it is today with application_type native spelled (R3c: 201). Which way should the card go?",
"options": [
"A1 — Land ruling A as written: the one option line clientRegistrationDefaultApplicationType: 'infer' plus a comment citing RFC 8252 §7.3 and the MCP authorization spec. Pins run on the vendor's real handler for the four acceptance cells. One more pin records the confidential cell as it really behaves (201 native with a secret, authorize refused without PKCE), so no reader takes the option as public-only. Customer-visible: local MCP clients (Claude Code, Cursor, VS Code, Codex CLI) connect over OAuth. A confidential body that omits application_type with a loopback redirect is now accepted, as it already is with the field spelled. Work: services, 1 PR (one option line, a comment, one test file, a patch changeset), no precedent needed. Axes. Business need: serves the measured pull (Claude Code on production, a token_endpoint_auth_method none body) and spends nothing on the confidential cell, which no measured client sends. Long-term: one classification rule, owned by the provider and evolving with the spec; RFC 8252 §8.4 treats native apps as public clients except where DCR provisions per-instance secrets, which is exactly the R2 shape. AI error: one declarative option, and the confidential pin keeps its real scope explicit. Startup focus: no new mechanism.",
"A2 — 'infer' plus a plugin-auth before-hook on /oauth2/register that keeps web (so refuses the loopback redirect) when application_type is omitted AND token_endpoint_auth_method is not none. Customer-visible: the same as A1 for public clients; an omitted-type confidential loopback body stays refused. Work: services, 1 PR (about 30-50 lines plus pins), re-verified on every provider bump. Axes. Business need: no measured producer of the cell it refuses. Long-term: a second classification rule beside the provider's, the duplication the decision rejected option B for. AI error: it reads like a boundary, but one field bypasses it (R2c is 201 today and stays 201): a guard that declares more than it enforces. Startup focus: a new hook to maintain.",
"A3 — A1 now. Separately, if the maintainer wants anonymous registration limited to public clients, a SEPARATE card that refuses every unauthenticated registration whose token_endpoint_auth_method is not none, whatever application_type says. Of the three, only this option moves the boundary F1 describes: it narrows a door that is open on main today, independent of infer. The installed provider has no option for it: 1.7.12 offers only allowUnauthenticatedClientRegistration on or off, and documents that confidential clients receive a one-time client_secret. So it would be a plugin-auth hook. Work: a second PR (about 50 lines plus pins) and its own decision, because it narrows. Axes. Business need: no measured producer either way today. Long-term: a real policy, but outside this card's ruling. AI error: a genuine, loud refusal at the door. Startup focus: a new mechanism, which defaults to no unless the maintainer names it."
],
"recommendation": "A1. On all four axes the reading moves no boundary. Every body that infer newly accepts is accepted today with application_type spelled out (F1), and PKCE stays enforced at authorize for all of them (F2). A2 would add a duplicate rule that one field bypasses, which is the declared-but-not-enforced shape. If a public-only anonymous door is wanted, that is A3's separate decision, and it does not need to hold this card, whose measured pull is a public client. Self-check: A1 on the long-term axis alone; the other three axes do not flip it."
}
],
"out_of_scope_findings": [
"carrier: #25118 (the implementing PR under whichever ruling) · noted, not filed: the existing real-provider harness in packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-resource.test.ts (registerDcrClient) registers with an explicit application_type native. That is why the suite stayed green through this defect: the card's body omits the field. The implementing PR's pins should send the card's own body shape (no application_type)."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 21, 2026
[Decision] 裁决 A 的置信缺口 1 实测不符:
infer只看回调地址,不看客户端是否公共domain:servicesseat 2 (seat post #21118) ·os-bill·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-21T23:31Z · Thread-read: 6328861459Release:
session_01UnHcTADjeg3VwxF21QHj7W· Cause: the dev stopped at the fork ruling6327761812names ("a mismatch is reported as a fork"), with no code and no PR · Goes to:needs-user-decision. This write's label stroke clears the assignee. The branchclaude/issue-25118-dcr-infer-nativeis empty atabc25a353.一句话问题: 打开
infer后,本地 AI 客户端(Claude Code 等)能通过 OAuth 连上 MCP;但同一开关也会让「未声明类型、带密钥的机密客户端 + 本机回调」的匿名注册从被拒变成被接受。裁决时假设它只放行公共客户端,实测不是。背景(开发实测
6328861459;席位已对照提供方 1.7.12 发布包源码复核)clientRegistrationDefaultApplicationType: 'infer'后 201,类型为native。resolveDynamicRegistrationDefaultApplicationType只检查回调地址(任一为非 http(s) 协议或精确本机 http 即判 native),从不读token_endpoint_auth_method。席位读的是 npm 发布包@better-auth/oauth-provider@1.7.12的dist/authorize-D_jy3s-1.mjs:1718-1722。所以「机密客户端(client_secret_basic,或省略认证方式时提供方默认填client_secret_basic)+ 本机或自定义协议回调 + 未声明类型」今天 400,开infer后 201 并下发client_secret。clientRegistrationRequirePKCE默认true,plugin-auth 没有覆盖(dist/utils-BfpTo7XY.mjs:912-920)。application_type: native,今天在main上就已经被匿名接受(201、带密钥)。infer不新增任何「能注册的客户端形态」,只改变「没写类型」时的含义。https://evil.example/cb单独出现仍按 web 判(201 web,两种模式相同)。它与一个本机回调写在同一请求里时,按 native 规则校验并被接受,与今天写明native时完全一样。Governing text
None found in the repo for dynamic client registration (decision
6318906173searcheddocs/adr, 0 hits; ruling6327761812records the same). External: the MCP authorization spec; RFC 8252 §7.3 / §8.3; RFC 8252 §8.4 (native apps are public clients, except where dynamic registration issues a per-instance secret).是否改协议: 否。不碰 spec 或元数据契约,只改 plugin-auth 交给 OAuth 提供方的运行时配置。这是匿名注册入口的安全边界,属人工地板,所以呈维护者,席位不代裁。
前提(复核命令)
npm view @better-auth/oauth-provider@1.7.12 dist.tarball后执行grep -n "function resolveDynamicRegistrationDefaultApplicationType" -A4 package/dist/authorize-D_jy3s-1.mjs。阳性对照:同文件grep -n "function isExactLoopbackHttpRedirectUri"必中。git grep -c clientRegistrationDefaultApplicationType origin/main -- packages/plugins/plugin-auth/src,预期 0。阳性对照:git grep -c allowDynamicClientRegistration origin/main -- packages/plugins/plugin-auth/src/auth-manager.ts,预期 1。选项
infer配置加注释;在提供方真实处理器上钉四个验收格,外加一格如实记录机密客户端的真实行为(201 native 带密钥,缺 PKCE 授权被拒),写明它不是「只放公共客户端」infer加一个注册前钩子none时仍按 web 判,即拒绝本机回调native即可绕过(今天就能)none的匿名注册,不论类型怎么写业务含义直译:
四轴(业务立场)
token_endpoint_auth_method: none)。机密客户端那一格今天没有测到任何真实客户端在发。A1 满足拉动,对那一格不额外花钱。推荐:A1
claude mcp add→/mcp→ Authenticate)未实测;读数来自用 AuthManager 实际选项构造的提供方真实处理器,plugin-auth 在/oauth2/register前没有任何拦截层。infer无关,不必拖住这张有真实拉动的卡。裁后执行段
pm:queue,本席立即派发。派发令带开发的实测格作钉子(含机密格的真实行为),并让钉子发送卡面原样请求(不写application_type)。现有auth-manager.mcp-oauth-resource.test.ts写明了native,所以这次缺陷没被它抓到。changeset 为patch。none的匿名注册」的判据与选项,进决策箱另裁;它不阻塞本卡。pm:queue,派发令按 A2 实现钩子,钉子覆盖「写明native仍可注册」这一格,并在 PR 正文写明。os-decision-facets
Prior rulings read: dynamic client registration,oauth2/register,oauth-provider → 0 hits in docs/adr; none; thread: 1 ruling (
6327761812)Related: decision
6318906173; ruling6327761812; the dev report6328861459; #24773 (the Setup form's door, closed); #20791 (pm:awaiting-maintainer, the remote MCP + OAuth verification).Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Ruling: batch #383 item 1 · letter A1 · maintainer 「同意」 2026-10-22T00:05Z
Director seat, summon #47,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). Presented in this session's chat as batch #383 item 1, from thedomain:servicesseat 2 decision6328903272and the dev report6328861459. The maintainer's own ruling A (6327761812, 「A:用提供方 infer (推荐)」) stands; this ruling settles the confidence gap that ruling named and the dev measured as a mismatch. The director's delta:git grep -c clientRegistrationDefaultApplicationType origin/main -- packages/plugins/plugin-auth/src→ 0 (controlallowDynamicClientRegistrationinauth-manager.ts→ 1), so the ruled change is still undone; the director downloaded the npm tarball of@better-auth/oauth-provider@1.7.12and readdist/authorize-D_jy3s-1.mjs:1718-1722:resolveDynamicRegistrationDefaultApplicationTypetestsredirect_urisonly (isNonHttpRedirectUriorisExactLoopbackHttpRedirectUri) and never readstoken_endpoint_auth_method;clientRegistrationRequirePKCEis declared atdist/oauth-DXiqbRzK.d.mts:1446. The seat's and the dev's readings hold. Thread read to its end at this write:6328903272is the newest comment.Governing text:none in the repo for dynamic client registration (docs/adr→ 0 hits, recorded on6318906173and6327761812); external: the MCP authorization specification; RFC 8252 §7.3 / §8.3 (loopback redirects for native apps) and §8.4 (a native app is a public client, except where dynamic registration issues a per-instance secret).What the ruling binds
clientRegistrationDefaultApplicationType: 'infer'to the OAuth provider, with a comment naming this card and the measured boundary. No hook, no second classification rule.httpredirect, noapplication_type) registers 201native;https://evil.example/cbalone staysweb;http://evil.example/cbandhttp://localhost.evil.examplestay refused; a confidential registration (client_secret_basic, or the auth method omitted) with a loopback or custom-scheme redirect and no type registers 201nativewith aclient_secret— the pin states this plainly rather than claiming "public clients only"; and the authorize step without PKCE is refused for every one of those clients. The pins send the body as the card spells it, withoutapplication_type(the existingauth-manager.mcp-oauth-resource.test.tsspellsnative, which is why it did not catch this defect).maintoday when the body spellsapplication_type: native(the dev's R2c), soinferadds no registrable client shape; it changes only what an omitted field means. Any token it issues still needs the user's consent and PKCE. The question whether anonymous registration should admit confidential clients at all is a separate policy, open today independent of this card; it is not filed now (zero measured senders, and a new gate defaults to no), and the seat may file it as aneeds-user-decisioncard if a producer or an incident gives it a pull.application_type: nativebypasses — a declared boundary that is not enforced); A3 (A1 plus a second card now — the second card has no pull today, so it is not opened).6327761812: the environment's Setup → Integrations → Connect an Agent page becomes true; the browser walkclaude mcp add --transport http <name> https://<env-host>/api/v1/mcp→/mcp→ Authenticate completing and listing the tools is part of the acceptance, measured on a deployed environment after the cloud pin moves.Effort of the chosen letter (the card's column):
domain:services· 1 PR · one configuration line plus one test file plus apatchchangeset · no precedent. Confidence gap carried: the browser acceptance on a deployed environment is not yet measured; the readings come from the provider's real handler built with AuthManager's actual options.State:
needs-user-decision→pm:queue.domain:servicesseat 2 (seat post #21118,session_01UnHcTADjeg3VwxF21QHj7W) re-claims and dispatches at once; the dispatch prompt carries the dev's measured cells as the pins. Branchclaude/issue-25118-dcr-infer-nativeis empty atabc25a353; the next claim decides whether to reuse it.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 · 2026-10-22T00:19Z · p1,
domain:services, re-claim under ruling A1 (6329352251, the maintainer's 「同意」), which settles ruling A's (6327761812) confidence gapSession:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-25118-dcr-infer-native(reused; empty atabc25a353)Worktree:
objectstack-issue-25118Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)Ruling-ref: 6329352251
File surface (line anchors at
origin/mainabc25a353):packages/plugins/plugin-auth/src/auth-manager.ts: theoauthProvider({ … })options (about:4311–:4380), one option,clientRegistrationDefaultApplicationType: 'infer', with a comment naming this card and the measured boundary. Nothing else in the file.packages/plugins/plugin-auth/src/;.changeset/25118-*.md(apatch).auth-plugin.ts, notpackages/spec/**, not the Setup form'ssys-oauth-applicationdoor, notpnpm-workspace.yamlor any lockfile.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable"). The dispatch waits for a free slot (batch3, the maintainer's 「并发 3」).Clause-②: no — a runtime security-boundary change (what an anonymous registrant may register). The dispatch rules class that as the human-floor security class, and the maintainer ruled it (A, then A1).
Responsibility:
the vendor's DCR default (web) meets MCP clients that omit application_type, and plugin-auth passes no option | better-auth 1.7.12's clientRegistrationDefaultApplicationType: 'infer', unset today | every local MCP client (Claude Code, Cursor, VS Code, Codex CLI) that registers anonymously over OAuth; measured on productionThread-read: 6329352251
Serial constraints cleared:
openandemail_domainpostures, a sign-up for an address that already has an account answers the success shape and mails that address (ruling 1B on #25040); without a live mail transport the explicit refusal stays #25060 (PR feat(plugin-auth, plugin-email): under open and email_domain, a sign-up for an existing address answers like a new one and mails that address, while mail is delivered #25145) editsauth-manager.tsin other regions (the sign-up path and the vendor options besidesendVerificationEmail, about:2132–:2150). The file is shared, so whichever lands second mergesmain.plugin-authsource (the open PRs' file lists were read this round).Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{
"issue": 25118,
"status": "done",
"branch": "claude/issue-25118-dcr-infer-native",
"pr": "#25156",
"session": "session_01UnHcTADjeg3VwxF21QHj7W — the parent's harness-stamped id (mode:subagent run of the dispatching seat)",
"premise_still_valid": true,
"summary": "Implemented ruling A1 (6329352251) as written. packages/plugins/plugin-auth/src/auth-manager.ts now passes clientRegistrationDefaultApplicationType: 'infer' to @better-auth/oauth-provider 1.7.12, with a comment that names #25118, the ruling, RFC 8252 §7.3/§8.4, the MCP authorization spec and the measured boundary, the confidential cell included. There is no hook and no second classification rule. A new test file, auth-manager.mcp-oauth-dcr-infer.test.ts, boots the REAL better-auth and oauth-provider 1.7.12 handler from the exact options AuthManager produces. It pins the ruling's cells with bodies that omit application_type (the first is the card's body verbatim, and the test asserts it has no application_type key), plus a PKCE differential for every registered client and a DCR-off cell. A patch changeset (.changeset/25118-dcr-infer-native.md, Clause-②: no, Metadata-protocol: none) rides with it. Draft PR #25156 is open with assignee os-bill. Premises re-verified on origin/main 459afea: the pin is 1.7.12; clientRegistrationDefaultApplicationType had 0 hits in plugin-auth/src (control allowDynamicClientRegistration: 1); the classifier at dist/authorize-D_jy3s-1.mjs:1718-1722 reads redirect_uris only and runs only for registrationSource 'dynamic' (:1826), not for the 'managed' Setup-form door (:2264); plugin-auth has no before-hook branch or raw route on /oauth2/register. Every Partition-2 cell reproduced. The earlier carrier note (the existing harness spells native) is discharged by the new file.",
"tests": "Readings at 5a7e1f0, shared box. (1) New file:
bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-manager.mcp-oauth-dcr-infer.test.ts'gaveTest Files 1 passed (1),Tests 12 passed (12),VERDICT command-exit 0(per-file line 914 ms of tests; module import 15.4 s). Cells: card body 201 native; http://127.0.0.1 201 native; cursor:// 201 native; https://evil.example/cb alone 201 web; http://evil.example/cb 400 invalid_redirect_uri with no client stored; http://localhost.evil.example 400 with no client stored; explicit web plus loopback 400; client_secret_basic plus loopback 201 native with a client_secret; auth method omitted plus loopback 201 native, client_secret_basic, with a secret; client_secret_basic plus cursor:// 201 native with a secret; loopback plus https://evil.example/cb 201 native; DCR off (OS_OIDC_DCR_ENABLED=false) 403 access_denied with no client stored. For each of the 8 registered clients, authorize without PKCE redirects to the client's own redirect with error=invalid_request, a description naming PKCE and no code, while the identical request with S256 PKCE reaches /oauth/consent. (2) Ablation, one-time and not committed: commit first, thenos-verify-lock.sh -- node scripts/ablation-replace.mjs --file ABS/auth-manager.ts --anchor \"clientRegistrationDefaultApplicationType: 'infer',\" --delete -- pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2 src/auth-manager.mcp-oauth-dcr-infer.test.ts, under a bashtrap 'git checkout HEAD -- ABS' EXIT INT TERM. On-disk proof: anchor 1 -> 0, blob 9c00cadb -> a4a53154. ResultTests 7 failed | 5 passed (12),VERDICT command-exit 1, the expected direction. Red: all 7 native cells, the card body and the custom scheme among them, each answering 400 invalid_redirect_uri 'web clients require https redirect URIs on non-loopback hosts', the production symptom. Green: the 5 option-independent cells. Restore proven: blob == HEAD 9c00cadb,git diff HEADempty, porcelain empty. The subject resolves through the relative import ./auth-manager (src), so no dist rebuild was needed for the mutation to reach the run. (3)pnpm --filter '@objectstack/plugin-auth^...' build(closure, workspace-concurrency 2): VERDICT command-exit 0. (4)pnpm --filter @objectstack/plugin-auth build: VERDICT command-exit 0; check-dts-emitted 3/3; dist/.d.ts has 0 hits for clientRegistrationDefaultApplicationType while dist/index.mjs has 1 (positive control), so no published type widened. (5)pnpm --filter @objectstack/plugin-auth typecheck: VERDICT command-exit 0; check:test-typecheck OK, 10 files / 88 errors / 23 signatures held, no growth (tsconfig.test.json includes src/**/). (6)pnpm --filter @objectstack/plugin-auth test:Test Files 182 passed (182),Tests 3450 passed | 10 skipped (3460), VERDICT command-exit 0. Head 6d4fd2a = 5a7e1f0 merged with origin/main 9cfc575 (no conflict; delta vs main is still these 3 files, +405/-0). The incoming commits touch cloud-connection, runtime, service-automation, verify, docs and an ADR, none in plugin-auth's dependency closure, so readings (1)-(6) stand under AGENTS §10's scoped re-check, and only the gate union was re-run on 6d4fd2a.","gates": "Local, at HEAD 6d4fd2a:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 70 commands (merge base 9cfc575, 3 paths, no stale-tree warning). All 70 ran with exit codes captured before any pipe, every one exit 0.--ranreconciliation exit 0: 'Run reconciliation — 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN'. A scan of the 70 logs for PREREQUISITE NOT MET / nothing was measured / queue-timeout found only check:objectui-changeset's own self-test prose. Run 1 at 5a7e1f0: 69 exit 0, plus check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (41 packages without dist). On run 2 that gate measured (112 require entry points across 69 packages load) because run 1's check:type-check-debt closure build had built every package. Commands: node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0; node scripts/check-adr-0087-registration.mjs --self-test :: exit 0; node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0; node scripts/check-changeset-no-major.mjs --self-test :: exit 0; node scripts/check-ci-filter-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs :: exit 0; node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0; node scripts/check-comment-mask-adoption.mjs :: exit 0; node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0; node scripts/check-comment-mask-corpus.mjs :: exit 0; node scripts/check-dev-prereqs.mjs --self-test :: exit 0; node scripts/check-dts-emitted.mjs --self-test :: exit 0; node scripts/check-empty-changeset.mjs --base origin/main :: exit 0; node scripts/check-empty-changeset.mjs --self-test :: exit 0; node scripts/check-issue-citations.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs :: exit 0; node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0; node scripts/check-platform-object-tenancy-census.mjs :: exit 0; node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0; node scripts/check-plugin-teardown-shape.mjs :: exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0; node scripts/check-position-name-fold-loaders.mjs :: exit 0; node scripts/check-position-name-fold-loaders.mjs --self-test :: exit 0; node scripts/check-registry-log-declared.mjs :: exit 0; node scripts/check-registry-log-declared.mjs --self-test :: exit 0; node scripts/check-rest-log-spy-declared.mjs :: exit 0; node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0; node scripts/check-system-context-census.mjs :: exit 0; node scripts/check-system-context-census.mjs --self-test :: exit 0; node scripts/check-tenant-audit-census.mjs :: exit 0; node scripts/check-tenant-audit-census.mjs --self-test :: exit 0; node scripts/check-undeclared-dep-imports.mjs :: exit 0; node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0; node scripts/docs-audit/check-affected-docs.mjs :: exit 0; node scripts/docs-audit/check-drift-comment.mjs :: exit 0; node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0; node scripts/release-pending-publish.mjs --self-test :: exit 0; pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0; pnpm check:auth-mount-ledger :: exit 0; pnpm check:changeset-gate-self-tests :: exit 0; pnpm check:closed-ceremony-text :: exit 0; pnpm check:cross-package-test-inputs :: exit 0; pnpm check:dispatcher-error-vocabulary :: exit 0; pnpm check:doc-authoring :: exit 0; pnpm check:driver-memory-census :: exit 0; pnpm check:dts-closure :: exit 0; pnpm check:dual-build-cjs-loads :: exit 0; pnpm check:durability-log-level :: exit 0; pnpm check:engine-double-contract :: exit 0; pnpm check:gitlink-declared :: exit 0; pnpm check:issue-citations :: exit 0; pnpm check:lean-entry-closure :: exit 0; pnpm check:logger-receiver-detach :: exit 0; pnpm check:nul-bytes :: exit 0; pnpm check:objectql-double-limit :: exit 0; pnpm check:objectui-changeset :: exit 0; pnpm check:org-identifier :: exit 0; pnpm check:page-declaration-shape :: exit 0; pnpm check:pm-changeset-deadline-census :: exit 0; pnpm check:published-files :: exit 0; pnpm check:query-options-erasure :: exit 0; pnpm check:refd-timer-probe :: exit 0; pnpm check:slot-lookup :: exit 0; pnpm check:sourcemap-no-sources-content :: exit 0; pnpm check:test-source-alias :: exit 0; pnpm check:tier-file-adoption :: exit 0; pnpm check:type-check-coverage :: exit 0; pnpm check:type-check-debt :: exit 0; pnpm check:watch-hint-literal :: exit 0; pnpm check:where-matcher :: exit 0. CI on PR #25156, single snapshot at open (no polling): 38 check-runs. Completed success: filter, No other open PR may claim the same issue, Check Documentation Links, Part-of PR must not also close its card, No other open PR may claim the same single-writer path, The card this PR closes must claim this branch, Check PR Size. In progress: Test Core (1/6)-(6/6), Build Core, Governed Surface Queue Guard, Type Check (source gates, workspace, consumer gates, debt ledger), the Lint lanes (ESLint, PM dispatch-gates, repo & tooling gates, CI release & engine gates, docs & code guards, scoped lanes), Check Changeset, Dogfood Regression Gate, Auto Label. The rest skipped by their own if: (Dogfood Verify CLI, Console Pin Gate, Build Docs, Temporal Conformance, 18.x upgrade harness, Packed-tarball smoke, the dogfood matrix template). CI convergence is the PM's to read.","dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm (Clause-②: no)","mcp_calls": "0 — no MCP GitHub tool was called. Reads went through gh api (REST GET issues/25118, issues/25118/comments, pulls/25156, commits/6d4fd2a5a.../check-runs).",
"api_writes": "3 REST writes in 2 relay strokes, each stroke one POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write run as objectstack-fleet[bot]. Stroke 1 (scripts/pm/fleet-write/dispatch.mjs, run 39709534127, success): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/25156/assignees [os-bill]; read-back 8734 bytes sent, 8734 stored, identical. Stroke 2 (scripts/pm/post-stamped.mjs): POST /repos//issues/25118/comments, this os-dev-report. No label write and no card assignee write. Not REST: 3 git pushes to claude/issue-25118-dcr-infer-native (abc25a3 -> 459afea fast-forward as the write-route probe, 5a7e1f0 the implementation, 6d4fd2a the merge of origin/main).",
"line_budget": "n/a — no skills/** file and no ratcheted ledger touched; diff +405/-0 over 3 files, under the 3000-line human-merge threshold",
"files_changed": [
"packages/plugins/plugin-auth/src/auth-manager.ts (+41: the one option line and its comment, inside the oauthProvider({ … }) options only)",
"packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-dcr-infer.test.ts (new, +345)",
".changeset/25118-dcr-infer-native.md (new, +19, patch for @objectstack/plugin-auth)"
],
"deviations": [
"Ablation attempts 1 and 2 never acquired the shared lock (VERDICT queue-timeout, exit 99, behind a dogfood test run and then a sibling build) and are NOT MEASURED. In attempt 1 the mutation was held on disk while queued (WRAP nesting outermost); its restore was proven (blob == HEAD, git diff HEAD empty). Attempt 2 and the measured attempt 3 nested the mutation inside the lock, so it existed only while the lock was held. No gate or build ran while any mutation was on disk.",
"Package readings (the new file, the ablation, plugin-auth build/typecheck/test) were taken at 5a7e1f0 and not re-run at the merge head 6d4fd2a. The merge brought nothing in plugin-auth's dependency closure (
pnpm --filter '@objectstack/plugin-auth^...' lsagainstgit diff --name-only 459afea1f origin/main), so under AGENTS §10's scoped re-check only the 70-command gate union was re-run on 6d4fd2a. CI re-runs everything on the head.","test:repo (the vitest repo project) was not run locally: the diff touches no repo-project test and the derived list names none. CI Test Core runs it.",
"The browser walk carried by ruling item 5 (claude mcp add --transport http NAME https://ENV-HOST/api/v1/mcp, then /mcp, then Authenticate) is NOT MEASURED: it needs a deployed environment after the cloud pin moves. The PR body records it as owed.",
"No served-stack (pnpm dev) reading was taken. The readings come from the provider's real handler built from AuthManager's actual options; plugin-auth has no layer in front of /oauth2/register (code read: no before-hook branch, no raw route).",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and an emoji PR footer. Following AGENTS.md, which outranks it here (and the pre-push hook refuses a model id), the commit carries the model-free pair 'Claude-Session: https://claude.ai/code/session_01UnHcTADjeg3VwxF21QHj7W' / 'Co-authored-by: Claude' and the PR body ends with the session-URL footer. The merge commit 6d4fd2a carries git's default merge message with no trailer.",
"The dispatch named base origin/main 9cbd4c3a7; the PM corrected this mid-run to 459afea, and the work used the current origin/main (459afea, then 9cfc575)."
],
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (PR #25156 Acceptance notes only): the comment above rawApp.post(.../sys-oauth-application/register) in packages/plugins/plugin-auth/src/auth-plugin.ts still says DCR on /oauth2/register is something 'we don't want to enable broadly', while resolveDcrEnabled turns DCR on whenever the MCP server is on. Stale comment, no behaviour, outside this card's file surface.",
"carrier: none · noted, not filed (PR #25156 Acceptance notes only): the describe titles in packages/plugins/plugin-auth/src/auth-manager.mcp-oauth-resource.test.ts say 'installed 1.7.2' while the installed provider is 1.7.12. Naming only."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
ACCEPT → PR #25156 comment 6331289535 ·
domain:servicesseat 2 ·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T02:35Z · ruling A1 (6329352251) item by item · ready and auto-merge set through the relay; the landing note follows the merge. The deployed browser walk (ruling item 5) stays owed after the cloud pin moves.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 2 ·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T03:02Z6b44efa7b4origin/main6b44efa7b, subject "fix(plugin-auth): register an MCP client that omits application_type with a loopback or custom-scheme redirect as native"completedbyFixes. The only other card closed in that window, #24916, closed by its own PR (domain:cli)State:
pm:dispatchedis removed in this act. Ruling A1 (6329352251) is carried out. Still owed by the ruling's item 5: the browser walk (claude mcp add→/mcp→ Authenticate) on a deployed environment after the cloud pin moves. That makes the Setup → Integrations → Connect an Agent page true.Generated by Claude Code