Repository navigation
Commit 8eca18f
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} -->
Part of #30218 · follows #30011 (PR #30013) and #30192 (PR #30195)
_Requested by **Sam** · [project
thread](https://claude.ai/code/project/chan_01GsBogozeZG5sdpAARrN7HP?thread=cmsg_01GsBogozeZG5sdpAARrN7HPYLaC4gZBht1y9TL9mMjrFz)_
The card carries no `Claim:` naming this branch, so this body declares
`Part of`; the owning seat closes the card by hand once this lands.
Nothing of the card's framework half is left open here: what remains is
cloud's pin round and its hosted reading (see *Acceptance notes*).
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:`objectQLProvider`(数据引擎)改成按请求所属环境取内核里的引擎,和 #30013、#30195
的其它接口同一种做法;四个用到它的地方(`POST /batch`、对象状态、已发布读取、meta
读取门的「已停用包」判断)都改为传入按请求解析出的环境 id。
-
为什么改:托管运行时上这四处一直用宿主内核的引擎。批量写会在宿主引擎上开事务、用宿主引擎做更新和删除,而新建走环境自己的协议,一个请求用了两个内核。
- 风险与回滚:只动 `packages/rest`。单内核部署(没有
`kernel-manager`)仍读插件自己的内核,测试钉住不变。回滚即撤销本 PR。
- 先看:`packages/rest/src/rest-api-plugin.ts`(`environmentScopedProvider`
加了自身内核一腿)、`packages/rest/src/batch-routes.ts`(事务入口)、`packages/rest/src/rest-environment-scoped-providers.test.ts`(最后两个
describe)。
- 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run
src/rest-environment-scoped-providers.test.ts
src/rest-api-plugin-slot-lookups.test.ts`,预期 121 个全绿。
- 需要你做的:无,席位落地。
## What changes
**The card's fix.** `objectQLProvider` in
`packages/rest/src/rest-api-plugin.ts` is now
`environmentScopedProvider('objectql', objectQLOnOwnKernel)`, typed
`IObjectQLEngine`, with the same legs and the same rule as the seams
#30013 and #30195 scoped: a request for environment A is answered by A's
engine or by nothing, never by the host's.
- `environmentScopedProvider` takes an optional own-kernel leg. The
environment leg is unchanged: acquire the environment's kernel through
`kernelManager.getOrCreate`, read the slot with `getServiceAsync`, read
a branded "never registered" as `undefined`, and re-raise anything else.
Only the last leg is swapped.
- `objectQLOnOwnKernel` is the old provider body, unchanged: the
factory-aware `kernel.getServiceAsync('objectql')` that absorbs only the
branded rejection, and the `LiteKernel` sync leg. A single kernel, the
reserved `platform` id and a request in no environment still take it.
**Every consumer asks for the request's environment.** Each passes the
id `RestServer.resolveRequestEnvironmentId` answers (the scoped param,
else the host's `kernel-resolver` for a hostname-routed request). The
call sits inside the `wiredEngineOrLoud` thunk, so an unwired provider
still costs nothing and a rejection still answers 503:
- `batch-routes.ts`, the transactional `POST /batch`. `BatchRouteHost`
gains `resolveRequestEnvironmentId`.
- `meta-object-state-routes.ts`, `GET /meta/object/:name/state/:field`.
`MetaObjectStateRouteHost` gains it.
- `meta-item-history-routes.ts`, the disabled-owner check of `GET
/meta/:type/:name/published`. `MetaItemPublishedRouteHost` gains it.
- `rest-server.ts`, the meta read gate's `isItemOfDisabledPackage`.
`computeExecCtx` reaches the provider only when no environment kernel
was bound, and it already passed the resolved id, so it is unchanged.
Changeset: `patch` for `@objectstack/rest`.
`scripts/engine-double-contract.pinned.json` gains the two rows its
`--write` records for the harness's new engine double.
## Why
On cloud's hosted runtime one `RestApiPlugin` sits on the host kernel in
front of every environment kernel. `objectQLProvider` read the host
kernel's `objectql` whatever id it was handed, and the unscoped mount
handed it none. `computeExecCtx` already read the environment kernel's
engine, so on a hosted request the execution context and the door's
engine came from different kernels.
For `POST /batch` that meant three things:
- the transaction opened on the host's engine;
- update and delete ops went through the host's engine;
- create ops went to the environment's protocol, joined to the host's
transaction.
The ablation readings below show exactly that.
## Tests
The new cases live in #30013's harness
(`rest-environment-scoped-providers.test.ts`), as two new top-level
`describe`s, per the shared-harness rule.
The harness uses real `ObjectKernel`s:
- A host with the real plugin, `kernel-manager` and `kernel-resolver`
sits in front of the environment kernels. The host now carries an engine
of its own.
- Every engine records which kernel's engine made each call, and which
transaction a write joined.
- Each registry knows one object (`acct_ENV`) and disables one package
(`pkg_ENV`). A door judged by another kernel's registry therefore
answers differently on the wire too.
- The double's `update` and `delete` open with
`assertEngineUpdateDispatch` / `assertEngineDeleteDispatch`.
**Hosted, scoped and hostname mounts, env A and env B:**
- `POST /batch` (one create, one update, one delete) answers 200. The
calls are exactly these:
1. the transaction opens on the target's engine;
2. the create goes through the target's protocol, inside the target's
transaction;
3. update and delete run on the target's engine, inside that
transaction.
No call of any kind reaches the other environment's engine or the
host's.
- `GET /meta/object/:name/state/:field` answers 200 for `acct_ENV`,
judged by the target's registry only.
- `GET /meta/:type/:name/published` asks the target's registry whether
the owner package is disabled, and answers its 404. Any other registry
would have served the document.
- The meta read gate's `isItemOfDisabledPackage` asks the target's
engine only.
**Hosted, both mounts:** an environment whose kernel carries no engine
answers `POST /batch` with 501 `NOT_IMPLEMENTED`, and the host's engine
is not asked.
**Single kernel (no `kernel-manager`), both mounts:** the same four
doors are answered by the one kernel's engine, unchanged.
**`rest-api-plugin-slot-lookups.test.ts`:**
- The scope table marks `objectql` environment-scoped.
- It is asked of `env_1`'s kernel for `env_1`, and of the plugin's own
kernel for no id and for `platform`.
- With no `kernel-manager`, every environment-scoped provider reads the
plugin's own kernel for every id.
## Acceptance notes
- The four doors now each resolve the request's environment once more.
#30013's review noted this pattern, and #30195 also followed it. The
hostname resolver is cached, and `resolveExecCtx` is memoised per
request.
- `isItemOfDisabledPackage` still hands the raw route id to
`p.getMetaItem`. `p` is the protocol already resolved for the request,
which is where its environment comes from, so this is unchanged here.
- **Not proven here:** cloud's pin round, and a hosted reading of `POST
/api/v1/batch` on its local stack (the card's done-when, on cloud's
side).
## Verification
Readings on `objectstack-ai/objectstack`, branch
`claude/project-thread-k1cdp9`, branched from `f2a9007e`, in a cloud
container. The head is `8bb0c277`. The two commits after `54c86871` add
only the changeset (`a5cde52d`) and the ledger JSON (`8bb0c277`), with
no source.
| | the two harness files (121 cases) |
|---|---|
| **this branch at `8bb0c277`** | **121 green** (also 121 green at
`54c86871`) |
| **ablation 1**: `rest-api-plugin.ts`, `rest-server.ts`,
`batch-routes.ts`, `meta-object-state-routes.ts`,
`meta-item-history-routes.ts` restored to `f2a9007e`, tests kept | **20
red**: all 18 new hosted cases, plus two slot-table cases. The 8
single-kernel cases stayed green. |
| **ablation 2**: only the four consumer sites reverted to the raw route
id (`() => this.objectQLProvider!(environmentId)`), provider kept | **9
red**: every hosted case on the hostname mount. The scoped mount stayed
green, because there the raw param is the environment id. |
What ablation 1 showed for the batch:
- the transaction opened on `host`'s engine;
- the create went through env A's protocol, inside `host`'s transaction;
- update and delete ran on `host`'s engine;
- the engine-less environment answered 500 instead of 501.
The harness imports the plugin and server from `src/` by relative path,
so neither ablation goes through a built `dist/`. Ablations ran on the
`54c86871` tree. Before each run I checked the mutation on disk by
marker counts, and after it I restored from `HEAD` (`git diff HEAD`
empty).
<details><summary>Package suite, typecheck, lint (at the a5cde52
tree)</summary>
- `pnpm --filter @objectstack/rest test`, under `os-verify-lock`:
- Test Files 386 passed (386);
- Tests 7298 passed | 332 skipped (7630);
- exit 0.
- `pnpm --filter @objectstack/rest typecheck`: exit 0. That covers
`tsc`, plus `check:test-typecheck` OK with 0 files / 0 errors held in
`test-typecheck-debt.json`.
- `eslint --no-inline-config` on the seven changed source/test files:
exit 0.
</details>
<details><summary>Gates (dispatch-gates --ran at 8bb0c27)</summary>
`node scripts/pm/dispatch-gates.mjs --ran ran.list` gave: **68 derived,
66 run, 2 NOT-MEASURED, 0 UNRUN**, exit 0. All 66 that ran exited 0.
The 2 NOT-MEASURED both exited 3, PREREQUISITE NOT MET in this
container. CI runs both.
- `node scripts/check-plugin-teardown-shape.mjs --self-test`: the
shallow clone cannot read its positive-control commit. The diff-reading
`node scripts/check-plugin-teardown-shape.mjs` exited 0.
- `pnpm check:dual-build-cjs-loads`: no full workspace build here.
`pnpm check:engine-double-contract` first exited 1, because the
harness's new pinned doubles were not yet in the ledger. Its `--write`
recorded the two rows in `8bb0c277`, and after that it exited 0.
16 families beyond the derivation also ran green. Among them:
- `check:service-providers`
- `check:startup-registry-verdict`
- `check:rest-log-declared`
- `check:route-ledger-census`
- `check:changeset-fixed`
- `check:tenant-chokepoint`
- `check:facade-delegation-only`
- `check:filter-alias-parity`
- `check:commit-card-trailers`
Not run locally (CI's own): the path-scheduled CI jobs and type-check
lanes that dispatch-gates lists as having no local invocation.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (`eslint
--no-inline-config` on the changed files.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The `## 维护者速读`
section is the required maintainer brief, in Chinese.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QUs3BrY9GFj1N39DmTsqEp
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 31260ca commit 8eca18f
9 files changed
Lines changed: 347 additions & 20 deletions
File tree
- .changeset
- packages/rest/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
233 | 234 | | |
234 | 235 | | |
235 | 236 | | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
236 | 245 | | |
237 | 246 | | |
238 | | - | |
| 247 | + | |
239 | 248 | | |
240 | 249 | | |
241 | 250 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | | - | |
133 | | - | |
134 | | - | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| |||
1092 | 1092 | | |
1093 | 1093 | | |
1094 | 1094 | | |
| 1095 | + | |
1095 | 1096 | | |
1096 | 1097 | | |
1097 | 1098 | | |
| |||
1265 | 1266 | | |
1266 | 1267 | | |
1267 | 1268 | | |
| 1269 | + | |
| 1270 | + | |
1268 | 1271 | | |
1269 | 1272 | | |
1270 | | - | |
| 1273 | + | |
1271 | 1274 | | |
1272 | 1275 | | |
1273 | 1276 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
55 | | - | |
56 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
186 | 187 | | |
187 | 188 | | |
188 | 189 | | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
189 | 194 | | |
190 | 195 | | |
191 | | - | |
| 196 | + | |
192 | 197 | | |
193 | 198 | | |
194 | 199 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | | - | |
| 105 | + | |
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
133 | | - | |
| 133 | + | |
| 134 | + | |
134 | 135 | | |
135 | 136 | | |
136 | 137 | | |
| |||
420 | 421 | | |
421 | 422 | | |
422 | 423 | | |
423 | | - | |
| 424 | + | |
| 425 | + | |
424 | 426 | | |
425 | | - | |
| 427 | + | |
426 | 428 | | |
427 | 429 | | |
428 | 430 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
344 | 344 | | |
345 | 345 | | |
346 | 346 | | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
347 | 359 | | |
348 | 360 | | |
349 | 361 | | |
| |||
380 | 392 | | |
381 | 393 | | |
382 | 394 | | |
383 | | - | |
| 395 | + | |
384 | 396 | | |
385 | 397 | | |
386 | 398 | | |
| |||
396 | 408 | | |
397 | 409 | | |
398 | 410 | | |
399 | | - | |
400 | | - | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
401 | 414 | | |
402 | 415 | | |
403 | 416 | | |
| |||
428 | 441 | | |
429 | 442 | | |
430 | 443 | | |
431 | | - | |
| 444 | + | |
432 | 445 | | |
433 | 446 | | |
434 | | - | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
435 | 454 | | |
436 | 455 | | |
437 | 456 | | |
| |||
442 | 461 | | |
443 | 462 | | |
444 | 463 | | |
| 464 | + | |
445 | 465 | | |
446 | 466 | | |
447 | 467 | | |
448 | 468 | | |
449 | 469 | | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
450 | 478 | | |
451 | 479 | | |
452 | 480 | | |
| |||
0 commit comments