Repository navigation
Commit 63de248
fix(objectql, metadata-protocol, cloud-connection)!: a declared .custom action override is keyed and judged the same way at every door, and only its declared base's handler slots beneath it (#30612)
domain:server#3 · os-bill · session_01Utp6CXTiQqG1Y4zdXMck85
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg1KVSeo3u2QVwiYBHqKjeot)_
Fixes #30542
Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: follow-up #30505
## 维护者速读
1. 改了什么:PR #30497 留下的四处收口。安装门按基包的代码原件判断动作覆盖;保存门把动作覆盖的基包按 `OBJECT:NAME`
认,和 `os validate`、安装门一致;只有覆盖声明的那个基包的 handler
才垫在覆盖下面,别的注册(包括不带包名的)照常替换并告警。
2. 为什么改:#30542。四处都是保存门、安装门、读和运行之间的分歧:安装门会误拒一个把收紧放回基包原样的
v2;保存门会放行一个把动作挪到别的对象上的覆盖;另一个基包的 `.custom`
行在真基包注册前绑上后会一直跑,而读已经不认它;不带包名的注册垫到覆盖下面时文档说告警、实际没有。
3. 风险与回滚:BREAKING 只有一条,保存门现在拒绝把动作挪到别的对象上的声明覆盖(403 `NOT_OVERRIDABLE`,规则
`declared_package_ships_no_such_item`),这条形状 `os validate`
和安装门本来就拒。其余三处只改判断依据或日志。单一提供方的正常部署不受影响。回滚即 revert。
4. 先看:`engine-actions.ts` 的
`arrivesAsDeclaredBase`(运行时谁能垫在覆盖下面);`protocol-packaged-base-refusal.ts`
的 `onItsObject`;`marketplace-install-local-plugin.ts` 的 `artifactOf`。
5. 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run
src/plugin-authored-actions.test.ts
src/protocol-flow-custom-override.test.ts`,全绿(24 + 14)。
6. 需要你做的:无,席位落地。
## What changes
Four loose ends PR #30497 left in the declared `.custom` action override
(ADR-0146 D9), from its contract reviews (round 0 ③-2, ③-3; delta ③-7,
③-8). Each was measured on `main` (`db04e5ce5`) first, on the real
engine, protocol and install door; all four reproduced.
**1. Install-door ratchet**
(`packages/cloud-connection/src/marketplace-install-local-plugin.ts`)
- The managed install's override gate now hands the judge the installed
base's code artifact (`registry.getArtifactItem(type, name, base)`, kept
only when it is that base's, at the same `OBJECT:NAME` identity) as
`baseItemOf`, the item the protocol door's `overrideWriteVerdict` judges
against. A registry that answers no artifact leaves the listed item, as
before. A base delivered in the same install is still read off its body.
- Before, it handed the judge the item the protocol's per-package
listing served. Once the pair's override is armed, that listing serves
the override under the base's envelope, so a re-install of the pair was
judged against its own previous version.
**2. `objectName` pinned at the protocol door**
(`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`)
- `overrideWriteVerdict` keeps, for a *declared* action override, only
the shipped artifacts on the override's own `objectName` (as shippers
and as `baseItemOf`). An override that moves the base's action to
another object therefore finds no shipper and is refused
`declared_package_ships_no_such_item`: the rule `os validate` and the
install door already gave it, since both key an action `OBJECT:NAME`
(`overrideWriteItemsOf`). One private helper,
`overrideWriteActionObjectOf`, now spells that object for both.
- Chosen over an `action` row in `ITEM_KEY_DISCRIMINATORS`: that table
also keys the registry's item collections and the protocol's list merge,
so a row there would re-key every action read, far wider than this door.
An undeclared item is judged exactly as before.
**3. Only the declared base's handler slots beneath an override**
(`packages/objectql/src/engine-actions.ts`, `engine.ts`, `plugin.ts`)
- `ObjectQL#registerActionOverride` takes an optional fifth argument,
`base`: the package the override declares (`overrides.package`). The
authored-action resync passes it, and the entry records it.
- `registerAction` under a key such an override holds slots the arriving
handler beneath it only when the registry's code artifact of the action
(`getArtifactItem('action', name)`) is that package's, on this object.
That is the judgement the resync itself binds the override by once the
artifact is registered, and every path registers a package's items
before it binds its handlers (`AppPlugin.start`, the marketplace
rehydrate, the install). Any other registration replaces the override
and is warned like every other handler collision.
- So a row of another base's pair that ships the same `OBJECT:NAME`,
bound on its own declaration before the real base registered, is
replaced by the real base's handler when it arrives: the run now agrees
with the reads, which decline that row for the real base. The next
resync judges the row against the registered artifact and does not bind
it. Chosen over `rehydrate` announcing `metadata:reloaded`: that signal
re-runs every consumer's `kernel:ready` sweep (flows, security,
translations) from inside the install plugin's own `kernel:ready`, which
is neither cheap nor safe at boot.
- An override bound with no `base` keeps taking any package's handler
beneath it.
**4. Log level: warn** (`engine-actions.ts`, `engine.ts` docblock)
- A registration with no package under an armed override is no
package's, so it is not the declared base's: it now takes the
replace-and-warn path, the one `registerAction`'s docblock promises for
an unpackaged registration displacing a package's handler. Before, it
slotted beneath at `debug`. The docblock now names the exception
precisely (the declared base's handler only).
**Declarations.** `Clause-②: yes (narrowing)`, BREAKING: item 2 refuses
at the save and publish doors a declared override the save door admitted
before. Item 1 admits at the install door what the save door already
admitted (a widening arm, outranked by the narrowing).
`Metadata-protocol: none`: what an author can write did not narrow,
because `os validate` (`findOverrideDeclarationRefusals`) and the
managed install already keyed an action OBJECT:NAME on `main` and
refused a moved-object override; the save door caught up with them (the
#30095 precedent). The changeset carries the ADR-0087 disposition
(`not-required (no-migration-prescription)`: no schema key moves;
nothing at rest is rewritten). Items 3 and 4 change which handler runs
and a log level, not what an author can write. `patch` on the three
packages the diff moves.
## Tests
-
`packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts`,
new `describe` "[#30542] the install door judges a declared action
override against the base's code artifact": with the armed v1 served by
the listing and the base's own action in the registry, a v2 back at the
base's gates installs (200, saved), and a v2 that drops a gate the base
carries is still refused 403 `declared_action_widens_access`, nothing
written. The harness's registry gains an optional `getArtifactItem`; the
existing cases pass none and read as before.
- `packages/objectql/src/protocol-flow-custom-override.test.ts`, new
case in the `[#30065]` `describe` (real protocol over a real engine): a
moved-object override is refused at the save door (`code` + `status`),
`overrideWriteVerdict` answers rule
`declared_package_ships_no_such_item`, nothing is stored and every read
still serves the base; control: the same declaration on the base's
object is admitted.
- `packages/objectql/src/plugin-authored-actions.test.ts`, two new cases
on a real engine: the other-base row bound before the base registers
runs until the real base binds, then the real base's body runs, one
collision warning names both owners, `isServedOverrideRowOf` declines
the row for the real base (and admits it for its own), and the next
resync leaves the base bound; an unpackaged registration under an armed
override replaces it with one warning naming both owners.
<details><summary>Measurement on main (db04e5c), then on the
fix</summary>
Throwaway probes (deleted) on the real engine, protocol and install
door, the same harness the #30497 tests use:
| Item | Door | `main` | fix |
|:--|:--|:--|:--|
| 1 | protocol listing of the base after v1 is armed | serves `V1` under
`com.example.showcase` | same (unchanged) |
| 1 | protocol door, v2 back at the base's gates | ADMITTED | ADMITTED |
| 1 | install door gate (`evaluateOverrideWriteGate`), same v2 | refused
`declared_action_widens_access` | no refusal |
| 2 | protocol door, override with `objectName` moved | ADMITTED |
REFUSED 403 `NOT_OVERRIDABLE` |
| 2 | install door, same body | refused
`declared_package_ships_no_such_item` | same |
| 2 | `os validate` (`findOverrideDeclarationRefusals`) | refused
`declared_package_ships_no_such_item` | same |
| 3 | other-base row resynced before the real base binds, then run |
`ran:OTHER BASE` | `ran:BASE BODY` |
| 3 | `isServedOverrideRowOf(row, realBase)` (the reads' predicate) |
false | false |
| 4 | unpackaged `registerAction` under an armed override | slotted
beneath, 0 warnings | replaces, 1 warning |
</details>
<details><summary>Ablations, one per fix</summary>
Each through `scripts/ablation-replace.mjs` (literal anchor, on-disk
count, restore by trap), the pin run under the mutation:
- **Item 1**: `artifactOf(...) ?? held` → `held`: exactly "a v2 that
relaxes the v1 tightening back to the base's own gates installs" fails
(1 failed, 11 passed). A first attempt with the bare replacement `held`
was refused by the tool as a no-op (the count did not move), so it did
not run; the second used a marked replacement.
- **Item 2** (`metadata-protocol`, read from `dist/`): the object
comparison → `!== 'ablate-30542-a2'`, rebuilt, `ablation-dist-preflight`
found the marker in 2 built files; exactly "an override that moves the
action to another object is refused…" fails (1 failed, 13 passed).
Restore leg: rebuilt, `--absent` exit 0, tree clean, suite 14/14. A
first attempt (`true ||`) was folded away by the build (marker absent
from `dist/`), so its suite never ran.
- **Item 3**: the declared-base check → `return true`: exactly "another
base's declared override of the same key is replaced…" fails (1 failed,
23 passed).
- **Item 4**: `packageName === undefined` → slot beneath: exactly "a
registration with no package under an armed override replaces it…" fails
(1 failed, 23 passed).
</details>
## Acceptance notes
- The warning a replaced override logs is the generic collision sentence
("Two owners share one handler key only when both declare action …,
rename one of them"), which fits the same-key misconfiguration the
review names.
- The pair's *undeclared* same-named action on another object is still
refused `undeclared_same_name` at the protocol door while `os validate`
and the install door admit it (they key it `OBJECT:NAME` and find no
collision). That is the pre-existing, stricter direction and outside
this card; it narrows nothing new.
## Verification
All readings on head `76e59ff17` (the fix merged with `origin/main` at
`739c97273`) unless a line says otherwise. Head `accb62409` changes only
the changeset's declaration line and levels (no source), so the readings
below cover its source; on it `check-changeset-no-major.mjs --base
origin/main` exit 0 and `check-adr-0087-registration.mjs --base
origin/main` exit 0.
- Targeted pins after the merge and rebuild: objectql
`plugin-authored-actions` + `protocol-flow-custom-override` 38/38,
cloud-connection `marketplace-install-local-override-gate` 12/12, cli
`override-declarations` 11/11, metadata-protocol
`protocol.packaged-base-refusal` 91/91.
- Full suites on the fix before the merge (`da065aea3`):
metadata-protocol 22447 passed, objectql 11023 passed, cloud-connection
880 passed; `typecheck` clean on all three. The merge brought in no
change to the files this PR touches.
- Dogfood battery (title carries `!`): `pnpm --filter
@objectstack/dogfood test` after the `@objectstack/dogfood^...` build,
lock VERDICT command-exit 0, Test Files 420 passed | 3 skipped (423),
Tests 3496 passed | 36 skipped (3532), no failure.
<details><summary>dispatch-gates on this head: 62 derived, 62 run, 0
NOT-MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` over the PR's change set, each family run
and recorded `COMMAND :: exit N`; `--ran` reconciliation: 62 derived, 62
run, 0 NOT-MEASURED, 0 UNRUN, every exit 0. Also `pnpm --filter
@objectstack/spec check:liveness` exit 0. On the first run (before the
merge) two families did not measure and were re-run green:
`check-plugin-teardown-shape --self-test` needed its pinned fixture
commit in this shallow clone (fetched), and `check:dual-build-cjs-loads`
needed every package built (PREREQUISITE NOT MET until the full build).
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's.
- [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: 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.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CXTiQqG1Y4zdXMck85)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 778aedf commit 63de248
9 files changed
Lines changed: 222 additions & 28 deletions
File tree
- .changeset
- packages
- cloud-connection/src
- metadata-protocol/src
- objectql/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
| 47 | + | |
| 48 | + | |
47 | 49 | | |
48 | 50 | | |
49 | 51 | | |
| |||
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
62 | | - | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
77 | 79 | | |
78 | 80 | | |
79 | 81 | | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
80 | 87 | | |
81 | 88 | | |
82 | 89 | | |
| |||
132 | 139 | | |
133 | 140 | | |
134 | 141 | | |
| 142 | + | |
135 | 143 | | |
136 | 144 | | |
137 | | - | |
| 145 | + | |
138 | 146 | | |
139 | 147 | | |
140 | 148 | | |
| |||
299 | 307 | | |
300 | 308 | | |
301 | 309 | | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
576 | 576 | | |
577 | 577 | | |
578 | 578 | | |
| 579 | + | |
| 580 | + | |
579 | 581 | | |
580 | 582 | | |
581 | 583 | | |
| |||
3691 | 3693 | | |
3692 | 3694 | | |
3693 | 3695 | | |
| 3696 | + | |
| 3697 | + | |
| 3698 | + | |
| 3699 | + | |
| 3700 | + | |
| 3701 | + | |
| 3702 | + | |
| 3703 | + | |
| 3704 | + | |
| 3705 | + | |
| 3706 | + | |
| 3707 | + | |
| 3708 | + | |
| 3709 | + | |
3694 | 3710 | | |
3695 | 3711 | | |
3696 | 3712 | | |
| |||
3703 | 3719 | | |
3704 | 3720 | | |
3705 | 3721 | | |
3706 | | - | |
| 3722 | + | |
| 3723 | + | |
| 3724 | + | |
| 3725 | + | |
3707 | 3726 | | |
3708 | 3727 | | |
3709 | 3728 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
766 | 766 | | |
767 | 767 | | |
768 | 768 | | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
769 | 780 | | |
770 | 781 | | |
771 | 782 | | |
| |||
1112 | 1123 | | |
1113 | 1124 | | |
1114 | 1125 | | |
1115 | | - | |
1116 | | - | |
1117 | | - | |
| 1126 | + | |
| 1127 | + | |
1118 | 1128 | | |
1119 | 1129 | | |
1120 | 1130 | | |
| |||
1171 | 1181 | | |
1172 | 1182 | | |
1173 | 1183 | | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
1174 | 1192 | | |
1175 | 1193 | | |
1176 | 1194 | | |
1177 | 1195 | | |
1178 | | - | |
| 1196 | + | |
1179 | 1197 | | |
1180 | 1198 | | |
1181 | 1199 | | |
| |||
1188 | 1206 | | |
1189 | 1207 | | |
1190 | 1208 | | |
1191 | | - | |
| 1209 | + | |
1192 | 1210 | | |
1193 | 1211 | | |
1194 | 1212 | | |
| |||
0 commit comments