Repository navigation
feat(metadata-protocol,objectql): open the declared override for a package's action (ADR-0146 D9) - #30497
Conversation
…(wip) Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe Co-authored-by: Claude <noreply@anthropic.com>
…s widening refusal (wip) Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe Co-authored-by: Claude <noreply@anthropic.com>
…t every door (wip) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
…al at the CLI and install doors (wip) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
📓 Docs Drift CheckThis PR changes 4 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 55 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b944ff610a5bd822378f0f5ae6920df7201baf64 && git checkout b944ff610a5bd822378f0f5ae6920df7201baf64
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c32242196a19570415e337b27461f4ed44773b5c 6366e96ef9e845e55954dc6a10750ce34d9a6d85 && git checkout -B drift-repro c32242196a19570415e337b27461f4ed44773b5c && git merge --no-ff 6366e96ef9e845e55954dc6a10750ce34d9a6d85
node scripts/docs-audit/affected-docs.mjs --json c32242196a19570415e337b27461f4ed44773b5c
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentAgainst D2/D9 and ADR-0066 D4: option A (refuse widening at the write gate) is the right place for "cannot be widened without saying so" — the refusal names each widened gate, is one verdict shared by save, publish, Implemented-by: VERDICT: FAIL |
…ts beneath the override Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
… rebound Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC Contract review (delta, round 1)Served-tier: Face: unchanged from round 0 — the committed path list still hits no schema, rules or narrowing path ( ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
④ Design judgmentAgainst D9 conditions 1–3 read for Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC 维护者速读(席位终稿)
席位意见:同意落地。三个条件都有测试钉住,重启顺序的缺口已补;剩余四处都不扩权,已立卡 #30542 跟进。 席位核对ACCEPT · head
|
domain:server#3 · os-bill · session_01Utp6CYKQNa91PVtcbE6bGe
Requested by Bill · project thread
Fixes #30065
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #30505
维护者速读
.custom包里用overrides声明覆盖,和 flow 一样走 ADR-0146 D9。原来这类保存一律 403 "not yet available",现在放行。actions、运行门读的getSchema、分层读effective)都按基包的名字返回覆盖;删除覆盖后各处读回基包;开关账本按名字一行管住当前武装的定义。requiredPermissions里的能力、ai.exposed从关到开、去掉ai.requiresConfirmation: true,都会以新规则declared_action_widens_access返回 403NOT_OVERRIDABLE。保存、发布、os validate和安装门共用这一道闸门。packages/spec、skills/**和其他受管面,ui/**只读;skills 文档的补改由席位另开卡。changeset 对四个包都是 minor。What changes
ADR-0146 D9 lists
actionas overridable "once its door is cut". This PR cuts that door, at the grain of the flow override (#28412).Write gate (
packages/metadata-protocol/src/protocol-packaged-base-refusal.ts)actionjoinsCONDITIONS_DELIVERED. A declared override from the base's paired.customis admitted by the save, publish and install doors,os validateandos build. Only the pair declares: a dependent's declaration and the pair's undeclared same-named save stay refused, and their remedies are unchanged.declared_action_widens_access(403 NOT_OVERRIDABLE). An override may add a gate and never remove one (ADR-0066 D4). Refused when the override:requiredPermissionsnames;ai.exposed: trueon an action the base keeps off the agent surface;ai.requiresConfirmation: true.The refusal names each widened gate.
actionOverrideWidening(baseItem, overrideItem)is exported.OverrideWriteClosure.baseItemOflets each door hand the judge the base's own item: the protocol doors (from the shipped artifact), the CLI walk (from the base in the stack) and the managed install (from the base's held items).POST /api/v1/actions/_activation/:object_or_global/:action. With the longer route, the new declared-save sentence for an 88-character action name ran to 520 characters and crossed the REST door's 500-character client-message bound. It is now 487.Reads (
protocol-lock-state.ts)declinesStoredRowserves the pair's declared action row, so the layered read'seffective(and the metadata service's by-name read) answer the override. The other reads already went through the served-override resolver once the type was open. The action keeps its bare registry entry, as a flow does, soSEALED_ROW_SERVED_OVERRIDE_TYPESis unchanged.Script body rebinding (
packages/objectql)ObjectQL#registerActionOverride(objectName, actionName, handler, packageName)binds a handler over the one registered under the key and keeps it beneath (ActionHandlerEntry.beneath). Re-binding the same package's override keeps the same handler beneath.removeActionsByPackage(packageName)removes the package's entries from each key's stack, so removing an override binds the handler beneath again. Removing the base under an armed override keeps the override.resyncAuthoredActionsno longer filters out a stored row that is the declared.customoverride of an artifact-shipped action on the same object (judged by the sameisServedOverrideRowOfthe reads use). Such a row is bound withregisterActionOverride, so one resync rebinds the name in either direction. A row whose base the registry does not hold yet is judged by its own declaration (isServedOverrideRowOf(row, overrides.package)) and bound as an override too.registerActionof another package's handler under a key an override holds takes the place beneath the override (the entry is markedoverride) instead of replacing it. This is what keeps condition 2 after a restart for a marketplace-installed base: its handlers are bound by the install plugin'skernel:readyrehydrate (bindAppArtifactHandlers→registerAction(..., 'app:<base>')), which runs after ObjectQLPlugin'skernel:readyresync bound the override and announces nometadata:reloaded. Before this, the rehydrate replaced the override's handler, so every read served the override whileexecuteActionran the base's body.Why
#30065: a customer should be able to rename, hide or adjust a button a package ships without cloning it. Before this PR the declared override was refused. And merely opening the type (measured with a temporary flip of the set) would have left two gaps. First, the layered read kept serving the base. Second, a script action would have kept running the base's body: the resync filtered the override row out as artifact-shipped, and binding it with
registerActionwould have lost the base's handler on restore. A third problem showed up in the same measurement: an override carryingrequiredPermissions: []was served as-is, so the override could remove ADR-0066 D4's capability gate from the base's button. That is what Acceptance 3 asked about. The seat chose to refuse the widening at the write gate.What an override can still change: every behaviour field (
label,type,target,body,params,visibleand the rest) is replaced whole, so an override can change what runs, including a body that runs system-elevated. It only does so for people who could already run the action: the gates are the base's, or stricter.Boot order: a base's handlers may be bound before the resync (a code-loaded package, in
AppPlugin.start) or after it (a marketplace-installed base, in the install plugin'skernel:readyrehydrate). Both orders end with the override on top and the base's handler beneath it, and a re-bind of the base's handlers while the override is armed does too.Out of scope (follow-ups)
All four are filed as #30542 (the two below, plus the self-declared binding before the base registers and the slot-beneath log level from the delta review).
.customagainst the base's items asgetMetaItems({ type, packageId: base })serves them when the base is not in the same delivery. Once an override is armed, that read serves the override under the base's envelope, so a v2 that relaxes a v1 tightening back to the base's own gates is refused at install although the protocol door admits it. Over-refusal only (fails closed).ITEM_KEY_DISCRIMINATORShas noactionrow), whileos validateand the install door key it<objectName>:<name>. An override may therefore move a base action to another object at the save door. It widens nothing (the handler is not rebound and the base's gates ride the declaration), but the doors disagree.Verification
Tests at the flow override's grain:
packages/objectql/src/protocol-flow-custom-override.test.ts, newdescribe"[Action customization: open the declared override for a package's action, so a customer can rename, hide or adjust a button #30065] a declared .custom action override is the packaged action, at every door". It uses a real protocol over a real ObjectQL engine with a memory driver and covers:effective, both lists, the object read'sactions, the object list,getSchema) serves the override under the base's_packageId, and the run doors read the override's added capability;setActionActive({ name, packageId: BASE, active: false })turns the armed override off, the object read'sdisabledActionslists it, and the switch survives the restore;code+status), nothing stored;packages/objectql/src/plugin-authored-actions.test.ts, newdescribeon a real engine with the base handler registered asapp:<id>. It pins override → restore → run again (the base body runs); a base bound after the override's resync, as a marketplace base's rehydrate binds it (the override runs, then a restore runs the base body); the base's handlers re-bound while the override is armed; re-binding; undeclared and dependent rows never binding, before and after the base is registered, and an other-base row never binding once the base is registered (before that, see the follow-up objectql: a declared .custom action override has four door and boot-order loose ends left by PR #30497 #30542); a body-less override; and the handler stack itself.packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts: admitted narrowing, three widening refusals, a publish of a seeded widening draft refused.packages/cli/src/utils/override-declarations.test.tsandpackages/cloud-connection/src/marketplace-install-local-override-gate.test.ts: the action declaration is admitted, and a widening is refused atos validateand at the managed install (403, nothing written).packages/rest/src/rest-meta-packaged-action-permission-refusal.test.ts: the 88-character name sentence stays under the bound, with the declare remedy.Runs (local): full suites of metadata-protocol (22414 passed) and objectql (10998 passed at the rework head); targeted cli, cloud-connection and rest suites passed;
tsc --noEmitclean on metadata-protocol, objectql and cloud-connection; eslint clean on the changed files;dispatch-gates --ran65 of 65 run, none unmeasured;check:livenessandcheck:platform-checklistgreen.Human Minimum
packages/specholds no runtime logic. (packages/specnot touched.)pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01Utp6CYKQNa91PVtcbE6bGe