Repository navigation
Commit e1ab32e
fix(runtime)!: the package write doors refuse a query parameter they do not read (#26919)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XJHgKKRa9iGmUbp1paLnPzX)_
Fixes #26857
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc teaches a query parameter these
doors would now refuse: `skills/**` names a package write door only in
`plugin-hooks.md` (a `publish-drafts` route mention, no query string);
`content/docs` teaches `?keepData=true`
(`kernel/contracts/metadata-service.mdx`, the generated
`references/api/package-api.mdx`) and the QA checklist
`?overwrite=true`, both declared names that keep working.
## 维护者速读
- 改了什么:包的全部写门(卸载、安装、PATCH、启停、发布、回退、回滚、领养孤儿、复制,以及 RestServer 的 `POST
/packages/publish`)现在只认自己声明的查询参数,其余一律答 `400
VALIDATION_ERROR`,文案用仓里唯一那句「不认识的查询参数」。
- 为什么改:`DELETE
/packages/:id?keepdata=true`(小写拼错)以前被当成没写,卸载照常删掉了调用方要保留的对象表并答 200;真实
SQLite 探针在 main 上复现了「表没了」。
- 风险与回滚:只拒带未声明参数的请求;`?keepData=true`、`?overwrite=true` 与不带参数的调用照旧,控制台与
SDK 都在此列。回滚即还原 `packages.ts` 的 helper 与各门入口一行、`package-routes.ts` 一处。
- 先看:`packages/runtime/src/domains/packages.ts` 的
`refusePackageWriteDoorQuery` 与三个声明集,和 DELETE 门入口那一行(在权限与 wall
闸之后、注册表与协议之前);其次 `packages/rest/src/package-routes.ts` 的发布路由。
- 两分钟验证:`cd packages/runtime && pnpm exec vitest run
src/domains/packages-write-doors-closed-query.test.ts`,预期 65 passed;其中
`?keepdata=true` 各例答 400,`deletePackage` 未被调用,包仍在注册表里。
- 需要你做的:无,席位落地。
## What changes
- `packages/runtime/src/domains/packages.ts`: #26853's
`refuseDraftDoorQuery` is generalised in place into
`refusePackageWriteDoorQuery(deps, query, declared)`. Same sentence
(`unknownQueryParamRefusalMessage` from `@objectstack/rest`), same `400
VALIDATION_ERROR` through `deps.error`, no new code, no second helper.
Each door passes its measured closed set:
- `DELETE /packages/:id`: `PACKAGE_DELETE_QUERY_PARAMS = ['keepData']`
(the door reads `query.keepData` and nothing else).
- `POST /packages` (install): `PACKAGE_INSTALL_QUERY_PARAMS =
['overwrite']` (every other install option rides the body).
- every other write door: `PACKAGE_WRITE_NO_QUERY_PARAMS = []` (`PATCH
/packages/:id`, `…/enable`, `…/disable`, `POST …/publish`,
`…/publish-drafts`, `…/discard-drafts`, `…/revert`, `…/rollback`,
`…/commits/:commitId/revert`, `…/adopt-orphans`, `…/duplicate`).
Measured from the handlers and the helpers they call
(`requireWritablePackage`, `requireDuplicableSource`,
`refuseWalledCatalogPackageWrite`, `resolveProtocol`,
`installBodyRefusal`): none reads the query. The two draft doors now
route through the generalised form; `DRAFT_DOOR_QUERY_PARAMS` became
`PACKAGE_WRITE_NO_QUERY_PARAMS`.
- Placement on each door: right after `requireManageMetadata` and
`refuseWalledCatalogPackageWrite`, before the registry or the protocol
is read or called (the #23214 / #26853 placement), so a refused call
deletes, installs or changes nothing. On install it also runs ahead of
the body parse. A repeated declared flag stays refused by
`refuseRepeatedWriteFlag` at the place it has today; recognition runs
before it (AGENTS.md route rule 5), so
`?keepData=true&keepData=true&keepdata=true` is answered as the unknown
name.
- `packages/rest/src/package-routes.ts`: `RestServer`'s `POST
/packages/publish` is still served (the registrar's one route), and it
cannot reach the runtime helper. It declares `PACKAGE_PUBLISH_PARAMS =
[]` and answers the same `unknownQueryParamRefusalMessage` sentence
after its capability gate and before the package service is resolved. It
writes it through the module's own `sendError`, because this surface
answers every body through that wrapped envelope;
`refuseUnknownQueryParams` writes the bare nested body the `/meta`
routes use. Same sentence, same code, no third phrasing.
- Read-only `GET` doors are not touched.
- Changeset: `@objectstack/runtime` and `@objectstack/rest` patch,
`fix(runtime)!:` with both declaration lines and the ADR-0087
`not-required (no-migration-prescription)` disposition.
### Caller census (this is a narrowing, so measured before the first
edit, at `e78d7057`)
Every call to these doors in `ui/**`, `packages/client*/**`,
`packages/cli/**`, `skills/**`, `examples/**`, `content/docs/**`,
`docs/**` and the repo's test and dogfood trees that sends a query
parameter:
- `ui/packages/app-shell/src/views/metadata-admin/PackagesPage.tsx`:
`DELETE …?keepData=true` (structure-only delete). Declared, unchanged.
- `docs/qa/platform-checklist/areas/api-backend.json`,
`platform-core.json`: `POST /api/v1/packages?overwrite=true`, and a
doubled `keepData` arity probe. Declared, unchanged (the arity refusal
keeps its sentence).
- `packages/runtime/src/domain-handler-registry.test.ts`: `POST
/packages` with `{ overwrite: 'true' }`. Declared.
- `packages/client`
(`packages.install/uninstall/enable/disable/update/publish/publishDrafts/discardDrafts/revertCommit/rollback/revert/adoptOrphans/duplicate`):
no query on any write door (install sends `overwrite` in the body). The
client fetch wrapper adds no cache-buster.
- Console `PackagesPage`, `PackageFormDialog`, `StudioDesignSurface`,
`packages-io` (`duplicate`), `commitHistory` (`commits/:c/revert`),
`MetadataClient.publishPackageDrafts`: no query.
- `packages/cli`: no call to these doors (its `package publish` goes to
`/api/v1/cloud/packages`).
No caller sends a name a door now refuses: nothing to fix, no blocker.
## Tests
- New
`packages/runtime/src/domains/packages-write-doors-closed-query.test.ts`,
65 cases, through `dispatch()` over a real `SchemaRegistry` with a
recording protocol and metadata service (the `packages-write-flag-arity`
harness):
- `DELETE /packages/:id`: `?keepdata=true`, `?keep_data=true`,
`?KeepData=1`, `?keepData=true&keepdata=true`, `?dryRun=true` each
answer `400 VALIDATION_ERROR` with exactly
`unknownQueryParamRefusalMessage(query, ['keepData'])`; `deletePackage`
is never called; the package is still in the registry. Recognition
outranks arity. Preservation: `keepData=true` and `keepData=1` still
reach `deletePackage({ packageId, keepData: true })`.
- `POST /packages`: `overwrite=true` still overwrites an installed id
(`201`); a misspelled `overWrite` is refused before the install instead
of answering the `409` it read as absent.
- Per door, all eleven: three refusal shapes (an unknown name, the same
name repeated, two unknown names) with the act never performed; the call
without a query performs the act once and answers as today; a caller
without `manage_metadata` still gets `403` first.
- New `packages/rest/src/package-publish-closed-query.test.ts`, 5 cases:
refusal (status, code, the shared sentence, `publish` never called),
preservation (`200`, `publish` once), `403` first.
- The existing `packages-draft-doors-closed-query.test.ts` (10) and
`packages-write-flag-arity.test.ts` (11) pass unchanged.
## Acceptance notes
- `Clause-②:` is the claim's line, copied as the os-dev rule requires.
Measured on the diff: the accept set narrows and no public surface grows
(no export, route or key added), which is the reading the `(narrowing)`
arm describes; the seat may prefer `no (narrowing)`, as the line's own
question (does this widen the accept set or grow the public surface)
answers no. `Check Changeset` reads `none` as `patch` either way.
- `packages/client`'s `packages.uninstall(id)` exposes no keep-data
option, so an SDK caller cannot keep the tables at all. Not in scope
here (no misbehaviour at a door); noted for the seat.
## Verification
All readings at head `5ce4a3c1`, base `e78d7057`. Evidence: the
project's `pm-orders/server/evidence/26857/`.
<details><summary>Red first on base: 40 runtime and 3 rest cases fail,
exactly the refusal cases</summary>
`packages.ts` and `package-routes.ts` restored from `e78d7057` (tree
only), the new tests at head:
- runtime (`packages-write-doors-closed-query` + the draft-door and
arity files): `Test Files 1 failed | 2 passed (3)`, `Tests 40 failed |
46 passed (86)`. First failure: `expected 400, got
{"status":200,"body":{"success":true,"data":{"packageId":"com.acme.app","success":true,"registryRemoved":true,"persisted":{"success":true}}}}`
for `?keepdata=true`. The misspelled install flag: `expected 400, got …
409 RESOURCE_CONFLICT "Package 'com.acme.app' already exists"`.
- rest (`package-publish-closed-query`): `Tests 3 failed | 2 passed
(5)`.
Every preservation and `403` case passed on base, so the refusals are
the only thing the diff moves.
</details>
<details><summary>Real door, real tables: base drops the table on
?keepdata=true, head keeps it</summary>
One-off probe, not committed (`zz-probe-26857.test.ts` in the evidence
folder): the dispatcher over `ObjectStackProtocolImplementation` on
SQLite, a package owning one object `repairs_ticket` with one row, then
`DELETE /packages/com.example.repairs` with each query.
| query | base `e78d7057` | head `5ce4a3c1` |
|---|---|---|
| `keepdata=true` | `200`, table gone | `400 VALIDATION_ERROR` naming
`keepdata`, table and its 1 row kept |
| `keepData=true` (control) | `200`, table and row kept | `200`, table
and row kept |
| none (control) | `200`, table dropped | `200`, table dropped |
</details>
<details><summary>Ablation: the helper forced to answer nothing reds the
refusal cases only</summary>
`node scripts/ablation-replace.mjs` on `packages.ts`, anchor `const
message = unknownQueryParamRefusalMessage(query, declared);` replaced by
a constant `undefined` (anchor 1 to 0, blob `3bb82a0d7c5d` to
`8d620d85fe51`): `Test Files 2 failed (2)`, `Tests 46 failed | 29 passed
(75)` over the new file and the draft-door file (40 + 6 refusal cases
red, every preservation and `403` case green). Restored: blob after
restore `3bb82a0d7c5d` = HEAD, `git diff HEAD` empty. The tests import
the runtime source, so no build sits between.
</details>
<details><summary>Green at head: package suites, typecheck, lint,
test:repo</summary>
- `pnpm --filter @objectstack/runtime test`: exit 0, `Test Files 462
passed (462)`, `Tests 7532 passed | 19 skipped (7551)`.
- `pnpm --filter @objectstack/rest test`: exit 0, `Test Files 329 passed
(329)`, `Tests 6518 passed | 326 skipped (6844)`.
- `pnpm --filter @objectstack/runtime --filter @objectstack/rest
typecheck`: exit 0.
- `pnpm exec eslint packages/runtime packages/rest --no-inline-config`:
exit 0.
- `vitest run --project repo` in `packages/runtime`, `packages/rest`,
`packages/core`: exit 0 each (6, 5 and 3 files; 83, 280 + 18 skipped,
947 tests).
- `node scripts/check-commit-card-trailers.mjs --range
origin/main..HEAD`: exit 0.
</details>
<details><summary>Derived gates: 61 of 61 run, all exit 0</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `5ce4a3c1` derived 61 families; every one
run, exit code captured before any pipe, all exit 0. Two had container
prerequisites on the first pass (exit 3) and were re-run after meeting
them: `check-plugin-teardown-shape --self-test` (positive control
`621a4876` fetched; 48 cases pass) and `check:dual-build-cjs-loads` (7
packages without `dist` built; 112 entries across 69 packages load).
`--ran`: `61 derived famil(ies) accounted for — 61 run, 0 NOT-MEASURED`.
</details>
<details><summary>Full dogfood once at head: 332 of 333 run files green;
the one red never touches a package door and passes alone</summary>
Closure built (`pnpm --filter '@objectstack/dogfood^...' build`, exit
0), then `pnpm --filter @objectstack/dogfood test` under the verify lock
at `5ce4a3c1`: VERDICT command-exit 1 (held 19m00s), `Test Files 1
failed | 332 passed | 3 skipped (336)`, `Tests 2 failed | 2704 passed |
20 skipped (2726)`.
The red file is
`test/packaged-action-disabled-dispatch-live.dogfood.test.ts`
(acceptance[0]: `expected true to be false` on `after.done`;
acceptance[5]: a different `sys_metadata` row id after the cold boot).
It drives the packaged-action disable switch over `/actions`, `/meta`
and MCP and calls no `/packages` route, so this diff does not reach it.
Re-run alone at the same head and build: `Test Files 1 passed (1)`,
`Tests 8 passed (8)`. Read as order or load dependent within the full
battery on a shared box, not as this change's consequence.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5nZMs618JppKUMdDh4)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 8074feb commit e1ab32e
5 files changed
Lines changed: 398 additions & 22 deletions
File tree
- .changeset
- packages
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
370 | 371 | | |
371 | 372 | | |
372 | 373 | | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
373 | 381 | | |
374 | 382 | | |
375 | 383 | | |
| |||
486 | 494 | | |
487 | 495 | | |
488 | 496 | | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
489 | 507 | | |
490 | 508 | | |
491 | 509 | | |
| |||
0 commit comments