Repository navigation
Commit 1d0600b
fix(runtime,cloud-connection): an install-local package binds its script-action bodies and body hooks; list_actions lists only what run_action can run (#21401)
Fixes #21321
Clause-②: yes (widening)
An app installed with `os package install ./dist/objectstack.json`
(install-local) now runs its `type: 'script'` action bodies and its body
hooks exactly as the same artifact does under `os start --artifact`, on
install, after a reinstall and after a restart. MCP `list_actions` now
lists a script action only when `run_action` can run it. Triage's
rulings on the card are implemented as written: route A, probe A, and
the hook half folded in.
## What changed
- **One binder, `@objectstack/runtime`.** The new module
`packages/runtime/src/app-artifact-handlers.ts` exports
`bindAppArtifactHandlers(ql, bundle, { appId, logger, source })` and
`appArtifactHandlerOwner(appId)`, and the package index re-exports both.
The function binds an artifact's action bodies through
`ql.registerAction` and its hook bodies and bundle functions through
`ql.bindHooks`, all under the owner `app:APPID`. It first removes the
action handlers and hooks that owner bound before. On a first bind this
does nothing. On a reinstall it keeps one handler per action and one
binding per hook, and it unbinds an action or hook the new version
dropped. The hook removal is explicit because `bindHooksToEngine`
unregisters only when it is given a non-empty list.
- **`AppPlugin.start`** calls the binder in place of its two inline
blocks. The order (after `runtime.onEnable`), the log lines and the
failure handling are the same as before.
- **Install-local, `@objectstack/cloud-connection`.** The plugin calls
the binder on `POST /api/v1/marketplace/install-local`, after
`manifest.register` and `syncSchemas` and before translations and seeds.
It calls it again on the `kernel:ready` rehydrate of each ledger entry,
with appId set to the manifest id. The runtime is loaded lazily, like
the plugin's other runtime helpers. A runtime without the export binds
nothing and logs a `warn` that names the consequence. There is no
fallback registration path.
- **Probe for `list_actions`.** `registeredActionHandlerProbe` sits
beside `executeRegisteredAction` in `action-execution.ts`. It reads the
engine's public `listRegisteredActions()` once per listing and walks the
same `actionHandlerObjectKeys` x `resolveActionHandlerKeys` order as the
run door. `list_actions` uses it for the script branch: every action
`invokeBusinessAction` sends to the handler registry, meaning neither a
declarative update nor a flow. Those two branches keep their own checks.
An engine without `listRegisteredActions` lists no script action. No
engine `hasAction` was added.
- Nothing changes in `packages/objectql`, `packages/metadata-protocol`
or `packages/spec`.
## Measured with the real CLI, before and after
The app has one object, one script action with an inline body and
`ai.exposed`, and one `beforeInsert` body hook that appends `stamped` to
`status`. The flow is `os build`, then an empty `os start`
(`OS_CLOUD_URL=off`), then `os package install ./dist/objectstack.json`.
Probes went over REST and over MCP Streamable HTTP with a minted API
key.
| phase | door | before (main f397608) | after (this branch) |
|---|---|---|---|
| after install | insert, hook | 201, `status: null` | 201, `status:
"stamped"` |
| | REST `POST /api/v1/actions/tasks_app_task/complete_task` | 404
`RESOURCE_NOT_FOUND` | 200 `{ok:true}`, row `done` |
| | MCP `run_action` | isError: "No handler registered for action
'complete_task' on 'tasks_app_task'" | `{ok:true, result:{ok:true}}`,
row `done` |
| | MCP `list_actions` | lists `complete_task` (that run_action then
refuses) | lists `complete_task` (that run_action runs) |
| after reinstall | all four | same as after install | same as after
install; hook fires once (`stamped`) |
| after restart (same home) | all four | 404, "No handler registered",
`status: null`; restart log `re-synced runtime-authored actions
{"registered":0,...}` | 200, ok, `stamped`; restart log
`[MarketplaceInstallLocal] Bound declarative actions
{"appId":"com.example.tasksapp","actionCount":2}` |
| control `os start --artifact` | all four | 200, ok, `stamped` | 200,
ok, `stamped` |
## Pins (each measured red on unfixed code first)
- `packages/cli/test/package-install-local-handlers.integration.test.ts`
(integration tier, spawn) runs the real `os start` and `os package
install` through the tsx source entry, across install, reinstall,
restart and the `--artifact` control. Each phase checks four things: the
hook fires once, the REST action runs, MCP `run_action` runs and
`list_actions` lists the action, and a declared AI-exposed `ghost_task`
(a `target` nothing registers) is not listed while `run_action` refuses
it. Red on main: 13 failed, 4 passed (the control's three rows and
harness health).
-
`packages/cloud-connection/src/marketplace-install-local-artifact-handlers.test.ts`
uses the real runtime binder and a recording engine. It covers install,
rehydrate, a reinstall leaving exactly one handler and binding, and a
reinstall of a version without the action and hook unbinding both. Red
on main: 4 of 4.
- `packages/runtime/src/mcp-list-actions-handler-probe.test.ts` covers
listing against run_action on one engine double: an unbound body action,
a target-bound key, the object-less key, the flow control, and an engine
that cannot list its handlers. Red on main: 3 failed, 3 passed (the
controls).
- `packages/runtime/src/app-artifact-handlers.test.ts` runs the binder
on a real `ObjectQL` engine with the QuickJS sandbox: `executeAction`
runs the body, `triggerHooks` runs the hook, re-binding keeps exactly
one of each, a dropped action or hook is unbound, and other owners are
left alone.
## Ablations (fix committed at 2e4e1ff; every leg through
`scripts/ablation-replace.mjs`, restore proven blob == HEAD and `git
status --porcelain` empty; dist legs rebuilt and checked with
`scripts/ablation-dist-preflight.mjs` both ways)
| leg | mutation | red |
|---|---|---|
| A | delete the install-route bind call | cc pin 3 of 4 (install, both
reinstall cases); spawn pin 6 (after-install and after-reinstall hook,
REST, MCP); restart and control stay green |
| B | delete the rehydrate bind call | cc pin: rehydrate; spawn pin 3
(after-restart hook, REST, MCP) |
| C1 | `ql.removeActionsByPackage(owner)` to `void 0` | binder pin and
cc pin (via runtime dist): dropped action still bound |
| C2 | `ql.unregisterHooksByPackage(owner)` to `void 0` | binder pin and
cc pin (via dist): dropped hook still fires |
| D | `packageId: owner` to `packageId: undefined` | binder pin 2
(re-bind runs the hook twice; dropped hook); cc pin 3 (via dist); spawn
pin 1 (after-reinstall hook fires twice) |
| E | remove the probe condition in `list_actions` | probe pin 3 (the
defect and its two siblings); spawn pin 4 (`ghost_task` listed in every
phase) |
| F | AppPlugin call replaced by `void bindAppArtifactHandlers;` | spawn
pin 3 (control hook, REST, MCP) |
A first run of leg F deleted the call outright. That left the import
unused, so the runtime DTS step failed with TS6133 after the JS had
already been emitted. It was re-run with the type-clean replacement in
the table, and that run is the one quoted.
## Tests and gates (at 2e4e1ff; main merged at db0cf22)
- `@objectstack/runtime` `vitest run --project local` (2 shards): 305
files, 4341 passed, 11 skipped.
- `@objectstack/cloud-connection` full: 31 files, 401 passed.
- `@objectstack/cli` `--project unit` (3 shards): 246 files, 3489
passed. Integration project: only the new file was run locally (17
passed). The rest of the integration project is left to CI.
- `typecheck` of runtime, cloud-connection and cli: exit 0, with each
`check:test-typecheck` OK.
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derived 66 gate commands. All 66 were run with their exit
codes recorded and all exited 0. `--ran` reconciled: 66 derived, 66 run,
0 NOT-MEASURED, 0 UNRUN.
- `pnpm lint` (the full `eslint . --no-inline-config`): exit 0.
## Acceptance notes
- `AppPlugin` now clears `app:APPID` before it binds. A first boot is
unchanged. If two `AppPlugin`s on one engine share an app id, the later
one's set now replaces the earlier one's actions as well; before, it
replaced only the hooks.
- Seven existing install-local suites (`bundle`, `conflict`, `id-gate`,
`list-posture`, `offline-degradation`, `posture-gate`, `storage-dir`)
gained a module-top `import '@objectstack/runtime'`. An install or
rehydrate now reaches the runtime's lazy import, and its first load
inside a 5000ms `it` timed out `posture-gate` and `id-gate` (the
clocked-window rule in `scripts/check-test-source-alias.mjs`). The suite
now pays that load during collection. No baseline duration was measured.
- The two `list_actions` engine fixtures in `http-dispatcher.test.ts`
gained `listRegisteredActions()`, listing the keys their `executeAction`
already answers.
- The spawn pin runs in development through the tsx entry and signs in
as the dev-admin seed. Using the production `bin/run.js` entry would add
the file to `check:cli-test-child-env`'s pinned roster of six
built-entry spawners, which needs an edit to that gate.
- The spawn pin does not cover a reinstall that drops an action or hook.
The binder and cc unit pins cover it (legs C1 and C2).
- The `[AppPlugin|MarketplaceInstallLocal] Bound declarative actions`
count is registrations, not distinct handlers. The same action collected
from `actions[]` and `objects[].actions[]` counts 2 for one handler.
This is unchanged and noted only.
- #21322 (flows and permission sets on hot install) is not addressed
here. It can reuse `bindAppArtifactHandlers`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ca0dfb6 commit 1d0600b
19 files changed
Lines changed: 1277 additions & 104 deletions
File tree
- .changeset
- packages
- cli/test
- cloud-connection/src
- runtime/src
- domains
Lines changed: 12 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
Lines changed: 398 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 246 additions & 0 deletions
| 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 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
15 | 20 | | |
16 | 21 | | |
17 | 22 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
17 | 22 | | |
18 | 23 | | |
19 | 24 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
41 | 46 | | |
42 | 47 | | |
43 | 48 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
65 | 70 | | |
66 | 71 | | |
67 | 72 | | |
| |||
Lines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
31 | 36 | | |
32 | 37 | | |
33 | 38 | | |
| |||
0 commit comments