Skip to content

fix(runtime,spec): 用 lint 规则守住槽类型化 —— 它第一次运行就查出项目成员资格门形同虚设 (#4127) - #4214

Merged
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb-b4
Jul 30, 2026
Merged

fix(runtime,spec): 用 lint 规则守住槽类型化 —— 它第一次运行就查出项目成员资格门形同虚设 (#4127)#4214
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-storage-upload-typeerror-0exbcb-b4

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Refs #4127#4127 gate 的第四批(前三批 #4168 / #4176 / #4202)。

⚠️ 请优先看第 2 节

本 PR 的主交付物是一条 lint 规则,但它第一次运行就查出一个线上 fail-open —— 项目成员资格门在所有开启项目作用域的部署上从不执行检查。已登录的非成员可以通过。

1. 规则本身

前三批让槽查找返回契约,但没有任何东西守着这个成果。在结果上标一个 any 就能把该调用点的检查静默关掉 —— 没有测试失败,和有检查的代码长得一模一样。这样的点已经存在过三处,是我 grep 找出来的,而 grep 正是这条工作线要取代的那种不可重复的扫描。

规则禁止在 resolveService / getService / getRequestKernelService 的结果上写 : anyas any

没有写过契约的槽protocolmcpkernel-resolverscope-manager按名字在 eslint.config.mjs 里集中豁免,而不是靠就地 disable 注释 —— 因为 pnpm lint 跑的是 eslint . --no-inline-config它会刻意忽略所有 disable 注释。(我最初就是用 disable 注释写的,验证时才发现 14 处在 CI 下全会失败。仓库这个 flag 等于在告诉我正确答案:例外必须集中声明。)

于是效果正是想要的那个:刻意的缺口是一个文件里被评审过的一行,随手的缺口是构建失败,两者在代码里不再长得一样。

2. ⚠️ 规则查出的 fail-open

enforceProjectMembership 读 session 的方式是:

authService?.api?.getSession?.({ headers })   // 没有 getApi() 兜底

这是代码库里三处 .api 读取中唯一没有兜底的resolve-execution-contextdispatcher-plugin 都有,它们的注释还写明了原因:.api 是 legacy 直挂,getApi() 是 lazy-plugin 访问器)。

plugin-auth 注册的是 AuthManagerauth-plugin.ts:288),AuthManager 根本没有 .api 成员 —— getApi() 是唯一入口。

后果链:读取得 undefineduserId 始终未赋值 → 函数在 "anonymous — upstream auth will decide" 那行返回 → sys_environment_member 查询从未执行

成员资格门整体不生效。enforceProjectMembership 默认值 = enableProjectScoping,也就是多环境/云端部署默认开启。匿名调用者仍被别处拦下(#2567/#3963),所以受影响的精确范围是已登录的非成员

而现有测试一直是绿的

那个门的现有测试把 auth 服务 mock 成 { api: { getSession } } —— 线上提供方从不具备的 legacy 形状

这是本工作线发现的第四个「测试为不存在的契约背书」(batch 1 三个 auth.handler mock、batch 3 的 status: 'open')。新增测试用真实的 getApi() 形状,在修复前会失败(已实测验证:还原修复后该测试红,恢复后绿)。

3. 规则同时查出的其他缺口

全是同一形状(实现存在、被调用、没声明),现已全部声明:

契约 新增成员 实现证据
IAuthService apigetApiisAuthGateActiveverifyMcpAccessToken auth-manager.ts 2694 / 3543 / 2837
IMetadataService loadloadDiagnosed metadata-manager.ts 1402 / 1426

getApi 的返回类型只声明有证据的子集 —— getSession({headers}) 及调用点实际读的三个字段 —— 而不是重新声明 better-auth 的完整 handle,那属于那个库。

4. 这个模式的真正根源

返回 any 的查找门面被重复声明了三份。 前三批只类型化了 DomainHandlerDeps,留下:

  • ActionExecutionDeps.resolveServiceaction-execution.ts:62
  • ResolveOptions.getServiceresolve-execution-context.ts:33

仍然说 any 的那一份,就是绕过其余所有份的通道 —— 而上面那些 auth 读取正好都住在那里。三份现已全部类型化。

5. 接口收尾

  • getRequestKernelService 用同样的重载切分。它唯一的调用点和旁边的 resolveService fallback 解析同一个 objectql,此前一个表达式的两条分支类型不同。
  • share-links 的 getEngine 去掉 Promise<any> 返回注解 —— 这是继 : anyas any 之后的第三种抹除句法,而且这条 AST 规则看不见它(注解在外层函数上)。该残留已写进配置注释,需要类型信息的 lint 才能覆盖。
  • getObjectQL 刻意保持 any,并记录了理由:它存在的目的就是够到 ObjectQL 超出 IDataEngine 的表面(registryexecuteAction),那部分没有契约。标成 IDataEngine 会是那个「看起来更舒服的谎」。

📝 更正上一条消息:我曾说 getObjectQL 在 batch 3 之后「是免费的」。那是错的 —— 取证后发现它够到的正是契约未覆盖的部分。

验证

结果
@objectstack/runtime 952 / 67 files
@objectstack/spec 7147 / 275
plugin-auth / rest 579 / 512
pnpm lint clean(含 --no-inline-config
tsc --noEmit spec、runtime、downstream-contract、4 个 examples
9 个 check:* 门禁 全 OK

🤖 Generated with Claude Code

https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw


Generated by Claude Code

… which found the project-membership gate not gating (#4127)

Batch 4 of the #4127 gate. #4168/#4176/#4202 made a slot lookup return the
slot's contract. Nothing protected that: an `any` annotation on the RESULT
switches the checking back off for that call site, silently, with no test
failing and no visual difference from code that has it. Three such sites already
existed and were found by grep — the same unrepeatable sweep this work replaced.

The rule bans `: any` / `as any` on a `resolveService` / `getService` /
`getRequestKernelService` result. Slots with no written contract (`protocol`,
`mcp`, `kernel-resolver`, `scope-manager`) are exempted BY NAME, CENTRALLY, in
eslint.config.mjs — not by inline disables, because `pnpm lint` runs
`--no-inline-config` and ignores those on purpose. The effect is the one worth
having: a deliberate gap is a reviewed line in one file, a careless one is a
build failure, and they stop looking identical in the code.

ITS FIRST RUN FOUND A LIVE FAIL-OPEN. `enforceProjectMembership` read the
session as `authService?.api?.getSession?.(...)` with no `getApi()` fallback —
the only one of the codebase's three `.api` readers without it. `plugin-auth`
registers `AuthManager`, which has NO `.api` member at all. So the read yielded
`undefined`, `userId` stayed unset, and the function returned at its "anonymous
— upstream auth will decide" line BEFORE ever querying `sys_environment_member`.
A signed-in non-member passed the gate, on every deployment with project scoping
on — which is where the flag defaults to true. Anonymous callers were still
denied elsewhere (#2567/#3963), so this was specifically the signed-in
non-member case.

The existing test for that gate mocked auth as `{ api: { getSession } }` — the
legacy shape the shipped provider does not have — so it was green throughout.
That is the FOURTH test in this work line found encoding a contract nobody
implements, after batch 1's three `auth.handler` mocks and batch 3's
`status: 'open'`. The new test uses the `getApi()` shape and fails against the
pre-fix code.

Also found by the rule, all the same #4127 shape (implemented, called,
undeclared) and all now declared: IAuthService gains `api`, `getApi`,
`isAuthGateActive` and `verifyMcpAccessToken`; IMetadataService gains `load` and
`loadDiagnosed`. `getApi`'s return type is the EVIDENCED SUBSET —
`getSession({headers})` and the three fields callers read — not a
re-declaration of better-auth's handle, which belongs to that library.

And the pattern's real root: the lookup facade returning `any` was re-declared
in THREE places. Batches 1-3 typed `DomainHandlerDeps` and left
`ActionExecutionDeps` and resolve-execution-context's `ResolveOptions` still
saying `any` — so the copy that stayed untyped was the way around all the
others, and it is where the auth reads lived. All three are typed now.

Completing the interface: `getRequestKernelService` gets the same overload split
(its one caller resolves the same `objectql` slot the `resolveService` fallback
beside it does, so the two arms of one expression had different types), and
share-links' `getEngine` loses a `Promise<any>` return annotation — a THIRD
erasure syntax after `: any` and `as any`, and one this AST rule cannot see.
That residual is documented in the config.

`getObjectQL` STAYS `any`, deliberately, with the reason recorded: it exists to
reach ObjectQL's surface beyond IDataEngine (`registry`, `executeAction`), which
has no contract. Typing it IDataEngine would be the comfortable-looking lie.

Refs #4127

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
@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 4:14pm

Request Review

@github-actions github-actions Bot added 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 2 package(s): @objectstack/runtime, @objectstack/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

…he contracts actually have (#4127)

Both pages reproduce their interface source, and both had fallen behind. The
auth page stopped at `getCurrentUser` and still called the interface
"intentionally minimal" — it was already missing the two MCP members batch 2
added, which I did not catch when that shipped, and this batch added four more.
The metadata page was missing `load`/`loadDiagnosed`.

Documented with the part a caller can get wrong, not just the signatures:
`api` vs `getApi()` (the shipped provider has no `api` member, so reading only
that one yields undefined everywhere — the fail-open this batch fixed), and
`load` vs `loadDiagnosed` (a null from `load` means either absent or every
loader down, and treating the second as the first turns an outage into an
authorization answer).

Refs #4127

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 16:27
@os-zhuang
os-zhuang merged commit 65a3a84 into main Jul 30, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-storage-upload-typeerror-0exbcb-b4 branch July 30, 2026 16:28
os-zhuang added a commit that referenced this pull request Jul 31, 2026
…4321)

* fix(spec,runtime): catch the getService<any> type-argument form and widen the lookup guard to all packages (#4251)

The #4127/#4214 rule matched ': any' and 'as any' on a lookup result but not
the type-argument form getService<any>(...) — the shape the codebase actually
used — and its files glob stopped at packages/runtime while the composition
roots held the majority of lookups. Both gaps close here:

- eslint.config.mjs: third selector for the type-argument form
  (typeArguments.params.0 = TSAnyKeyword), scope widened to packages/**,
  http.server added to UNCONTRACTED_SLOTS (three providers, no contract).
  The 40 not-yet-swept files are grandfathered in SLOT_LOOKUP_UNSWEPT — a
  visible ratchet enumerated at 180 sites by running the rule with the list
  emptied; batches remove entries, never add.

- The three in-scope runtime src sites are typed. Doing so surfaced the
  batch's first yield: both addDatasource branches (DefaultDatasourcePlugin
  parity branch, DriverPlugin.start) probed a method no metadata service
  implements, so they had never run on any boot — deleted rather than typed
  against a phantom shape. The inert DriverPluginOptions are tracked in
  #4320; registerInMemory('datasource', ...) is the actual visibility path.

- Contracts declare the evidenced members those sites read, both optional:
  IDataEngine.getDefaultDriverName/getDriverByName (ObjectQL's driver
  registry, re-registered as driver.<name> services for os migrate and
  serve storage detection) and IMetadataService.registerInMemory
  (MetadataManager's boot-time seeding primitive, #3827).

- Runtime test lookups typed as IDataEngine; the five http.server test
  lookups ride the slot exemption.

Verified: pnpm lint clean; spec check:generated all 8 up to date; tests
green — spec 7137/277, objectql 1345/85, runtime 954/67, metadata 281/13,
plugin-webhooks 25/3.

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

* docs(kernel): document the evidenced contract members added for #4251

The IDataEngine and IMetadataService contract references enumerate the
interface surface member by member, so the two additions (driver registry
lookups; registerInMemory) were stale-by-omission — flagged by the docs
drift check on #4321. Both interface listings and the per-member sections
now match packages/spec/src/contracts.

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

* feat(runtime,cli)!: retire the inert DriverPluginOptions (#4320)

new DriverPlugin(driver, { datasourceName, registerAsDefault }) never did
what it promised: both options configured the start() datasource block that
probed metadata.addDatasource — a method no metadata service implements —
so they were dead weight on every boot since inception. Routing to a named
auxiliary driver never came from the option: it keys off the DRIVER name
(init registers driver.<name>, ObjectQL's discovery loop adopts it, the
engine's lifecycle resolution looks the name up — engine.ts
LIFECYCLE_DATASOURCE), which is why serve's telemetry split worked all
along despite the option doing nothing.

- DriverPlugin constructor narrowed to (driver, driverName?); the options
  interface (module-local, never exported from the package root) is gone.
- serve.ts drops the options argument; the telemetry wiring comment now
  states the driver-name mechanism explicitly.
- Major changeset for @objectstack/runtime carries the FROM -> TO
  migration; behavior is unchanged by construction (the code the options
  configured never ran).

Verified: full turbo build green (71 packages); runtime 954/67 and cli
628/64 tests pass; pnpm lint clean.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
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