Skip to content

fix(plugin-hono-server): current-user 端点改由「拥有该请求的 kernel」应答 (cloud#927) - #4159

Merged
os-zhuang merged 1 commit into
mainfrom
claude/vercel-startup-permissions-provider-iwtkqm
Jul 30, 2026
Merged

fix(plugin-hono-server): current-user 端点改由「拥有该请求的 kernel」应答 (cloud#927)#4159
os-zhuang merged 1 commit into
mainfrom
claude/vercel-startup-permissions-provider-iwtkqm

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

cloud#927 的框架侧。#4144 补的是"有没有提供者",这条是"哪个 kernel 提供"——两件不同的事,后者在多租户宿主上一直是错的。

问题:注册期的定位器 ≠ 拥有该请求的 kernel

这三条端点从注册时捕获的服务定位器解析答案。单环境宿主上那就是唯一的 kernel,因此正确。多租户宿主上那是路由外壳,而身份不在那里:

  • cloud 的 ArtifactKernelFactoryAuthPlugin 挂在每个环境的 kernel 上(per-project HKDF secret);
  • 宿主 kernel 刻意不挂 —— objectos-stack.ts 原话:"AuthPlugin is intentionally NOT injected on the host";
  • 全仓 registerService('auth', …) 只有 plugin-auth 一处。

于是 ctx.getService('auth') 在宿主上抛错 → resolveCtx 落到 catch → 返回 undefined → 端点走匿名分支:

GET https://os-<id>.objectos.app/api/v1/auth/me/permissions
→ 200 {"authenticated": false}          // 无论调用者是谁、有没有登录

这比报错更糟:objectui 的 MePermissionsProviderauthenticated:false 当作匿名并保持宽松默认(return data.authenticated !== true)—— 匿名/公开面没有可解析的权限是设计使然,锁死每个字段会把公开表单弄坏。所以已登录 tenant 用户的 console,其 FLS / apiOperations 提示是系统性错误的。不是权限绕过(ADR-0057 D10 起服务端才是权威闸门,仍然逐请求强制),但正是 foldWildcardSuperUser / clampManagedObjectWrites 那一整套整形逻辑存在的目的(让客户端提示与服务端实际强制一致),只不过错在上一层。

改法:走宿主已有的 kernel-resolver

框架早就为这件事声明了规范缝:kernel-resolver(ADR-0006 Phase 5),dispatcher 从那时起一直在用。这三条端点不查它,才是缺陷本身。 现在改为逐请求查询,多租户路由从此只有一份策略而不是两份。

四种结果,两条失败路径都是刻意的:

resolver 行为
未注册 不变。单环境宿主、os serve、QA conformance 宿主零差异。
返回一个 kernel 那个 kernel 的 auth/objectql/metadata/security.permissions 应答。这就是修复。
返回 undefined 用注册期定位器 —— 这正是该缝对"无 scope/控制面请求"的契约,那种宿主上默认 kernel 确实是对的答案。
抛错 不给答案:错误自带 status 就用它(cloud 的 KernelWarmingError 是 503 + Retry-After),否则 503 environment_unavailable。退回默认 kernel 会交回一个自信而错误的 {authenticated:false},客户端据此 fail-OPEN —— 严格劣于一个可重试的错误。

逐请求惰性读取,绝不在注册时捕获:宿主可能在 kernel.bootstrap() 之前注册这些路由(为了抢在 /api/v1/auth/* 通配之前,见 cloud#924),而那早于注册 resolver 的插件跑 init()。测试里 resolver 是 registerCurrentUserEndpoints 之后才装进服务表的,正是为了钉住这一点。

三个 handler 用一层包装,让逐请求定位器遮蔽(shadow)注册期的 ctx —— handler 体一行未改,而不是逐个读去穿参数。

影响面 / FROM → TO

CurrentUserEndpointsContext 增加可选 getKernel(): unknown,即该缝需要的 defaultKernelPluginContext 本来就满足,所以HonoServerPlugin 的宿主无需改动;自己手写定位器的宿主应该补上:

 registerCurrentUserEndpoints({
   rawApp: httpServer.getRawApp(),
-  ctx: { getService: (n) => kernel.getService(n) },
+  ctx: { getService: (n) => kernel.getService(n), getKernel: () => kernel },
 });

不补的话,多租户宿主问不到"谁拥有这个请求",就退回旧的(错误的)来源 —— 是静默降级而非中性选择,所以即使当下是单环境也值得补。

已知代价(issue 里点名要求评估的两条)

多租户宿主上每个这三条的请求现在都会走一次 resolveKernel,对 cloud 而言就是 resolveByHostname(冷分片会构建 tenant driver,可能数秒)+ KernelManager.getOrCreate(可能冷建 kernel)。两点判断:

  1. 不是新增开销:console 的其余数据调用本来就全部经过 dispatcher,走的是同一个缝。轮到取权限时环境 kernel 通常已经热。
  2. 冷的时候答案是诚实的:503 + Retry-After 正是同一宿主上 /api/v1/auth/*AuthProxyPlugin)和 dispatcher 已经在给的形状,客户端可以重试;而旧行为是一个不可重试、静默 fail-open 的 200。

顺带记一笔(不在本 PR 范围):objectui 的 MePermissionsProvider 在 fetch 失败时渲染 errorFallback ?? loadingFallback,而 console 没传 errorFallback —— 所以 503 期间 console 停在 LoadingScreen 而不会自动重试。它已经暴露了 retry,接上是 objectui 侧一个独立的小改动。

验证

范围 结果
plugin-hono-server 全量 13 files / 149 passed(新增 12)
turbo run test --affected 73/73 packages
ESLint job 全部门禁(含 check:authz-resolvercheck:wildcard-fallthroughcheck:error-code-casing PASS
packages/spec 八个生成物门禁 PASS
tsc --noEmit clean

新增 12 例:

  • 解析出的环境 kernel 应答(旧行为在这里是 {authenticated:false});
  • 且在宿主自己也有 auth 时仍然优先环境 kernel —— 钉住"这是解析取胜"而不只是"填了个空洞";
  • 三条端点都走同一解析;
  • resolver 收到的是去掉 prefix 的 routePath(cloud 的 resolver 对它施加控制面跳过策略)、第二参是宿主 kernel、headers 是真的 Headers
  • 未注册 resolver/返回 undefined/返回默认 kernel/没有 getKernel —— 四种都保持旧答案;
  • warming 503 带 Retry-After: 4
  • 无 status 的失败是 503,并显式断言不是 {authenticated:false}
  • resolver 抛 4xx 原样透传。

关联


🤖 Generated with Claude Code

https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd


Generated by Claude Code

…ernel that owns the request (cloud#927)

`/auth/me/permissions`, `/auth/me/localization` and `/me/apps` resolved their
answer from the locator captured at REGISTRATION time. On a single-environment
host that is the only kernel, so it is right. On a MULTI-TENANT host it is the
routing shell — and identity is not there: cloud's `ArtifactKernelFactory` mounts
`AuthPlugin` per environment and its host kernel deliberately has none
("AuthPlugin is intentionally NOT injected on the host"). So `getService('auth')`
threw, `resolveCtx` fell to its catch, and every authenticated tenant caller got
`{authenticated:false}`.

Worse than an error: objectui's `MePermissionsProvider` reads
`authenticated:false` as ANONYMOUS and keeps its permissive default
(`return data.authenticated !== true`) because a guest surface has no resolvable
permissions by design. So the console's FLS / `apiOperations` hints were
systematically wrong — not a bypass (the server still enforces per request), but
exactly the client/server divergence `foldWildcardSuperUser` and
`clampManagedObjectWrites` exist to close, one layer up.

These endpoints now consult the host's ADR-0006 `kernel-resolver` seam per
request — the same one the runtime dispatcher has used since Phase 5, so
multi-tenant routing has one strategy rather than two:

- no resolver registered → unchanged (single-environment hosts, `os serve`, the
  QA conformance host);
- a kernel → that kernel's `auth`/`objectql`/`metadata`/`security.permissions`;
- `undefined` → the registration-time locator, the seam's contract for an
  unscoped/control-plane request;
- a throw → NO answer: the thrown status when it carries one (cloud's
  `KernelWarmingError` is 503 + `Retry-After`), else 503
  `environment_unavailable`. Falling back would hand back a confidently-wrong
  `{authenticated:false}` the client fails OPEN on.

Read LAZILY, per request, never captured at registration: a host may register
these routes before `kernel.bootstrap()` to outrank an `/api/v1/auth/*` wildcard,
which is before the plugin registering the resolver has run `init()`. The three
handlers are wrapped so the per-request locator SHADOWS the registration-time
`ctx`, leaving the bodies byte-identical rather than threading it read by read.

`CurrentUserEndpointsContext` gains an optional `getKernel()` — the
`defaultKernel` the seam takes. `PluginContext` already satisfies it, so
plugin-mounting hosts need no change; a hand-rolled locator should add it (the
changeset carries the FROM → TO).

Tests: 12 new (149 total in this package). Env kernel answers and OUTRANKS a host
that has its own auth; all three endpoints resolve; the resolver receives the
prefix-stripped `routePath` + the host kernel + real `Headers`; no-resolver /
`undefined` / default-kernel / no-`getKernel` all keep the old answer; warming
503 carries `Retry-After`; a status-less failure is 503 and explicitly NOT
`{authenticated:false}`; a 4xx passes through. Affected set 73/73 packages; all
lint-job and spec-artifact gates pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gEeLZ4oTeSXG6fKG1r3vd
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 1:05pm

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-hono-server.

6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-hono-server)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-hono-server)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-hono-server)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-hono-server)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 13:18
@os-zhuang
os-zhuang merged commit 545d931 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/vercel-startup-permissions-provider-iwtkqm branch July 30, 2026 13:19
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants