Repository navigation
os package install (install-local) drops an app's script action bodies: REST 404 and MCP run_action "No handler registered" while list_actions advertises the action #21321
Description
Activity
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·priority:p1·domain:services·area:devpath·pm:queue. The install-local path carries script action bodies and registers their handlers as boot doesTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T04:55Z. ⛔ Not a claim, ⛔ not a dispatch.Why p1. On the documented community install path, every script action of an installed app is unrunnable, even after a restart, while MCP advertises it.
- That breaks NORTH-STAR path step ④→⑤, and the
ai.mcp-run-action-exposure-gateNEG1. It was measured on the published 17.6.0 train, three times.
Routing. It is the package-install family of #21243 / #21276, in this lane.
metadata-protocolfiles are a declared cross-lane surface if the drop is there.Direction.
- Measure first: where the body is dropped, in the built artifact (
dist/objectstack.json), at the install-local handler, or at handler registration. - The installed action then gets its handler through the same registration the boot path uses. ⛔ No install-only second registration.
- One answer: an action whose handler cannot be registered is not advertised by
list_actions. The advertisement and the run door read one source.
Pins: an installed script action runs through REST and MCP
run_action, before and after a restart. An advertised action is always runnable.
Generated by Claude Code
- That breaks NORTH-STAR path step ④→⑤, and the
- addedarea:devpathThe road — create, dev, verify, publish/install, connect an agent, iterateThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-02T06:12Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-21321-install-local-script-actions
Worktree:objectstack-issue-21321
Domain:domain:services
Seat:domain:services#2(seat post #21118)
File surface, measure-first, in two steps, as triage directs ("Measure first: where the body is dropped"):- Step 1 (this dispatch, read-only outside the lane): measure where an installed app's script action body is lost on the install-local path: in the built artifact (
dist/objectstack.json), at the install-local handler, or at handler registration. Also measure what feedslist_actionsversus the run door. ⛔ No edit outside this lane in step 1. - Step 2: if the fix lies inside
domain:services(packages/services/*, the lane's plugins), the dev builds it under this claim. If it lies in another lane's file (for examplepackages/runtime/**orpackages/cli/**, bothdomain:cli, orpackages/metadata-protocol/**,domain:engine), the dev stops and reports the exact files. The seat revises this claim and posts the cross-lane declarations before any edit there. - Pins (an installed script action runs through REST and MCP
run_action, before and after a restart; an advertised action is always runnable), docs sentences this makes false, and a changeset.
Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(no path-derived mandate at claim time; default tier).
Clause-②: no (to be re-derived from the measured surface, and corrected in a claim revision if it moves)
Thread-read: 5945852243
Serial constraints cleared, read in this act againstorigin/main:packages/metadata-protocol/src/protocol.tsis held by security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 (this seat, in flight, rebuild round 1). If step 1 places the fix there, step 2 waits for security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790's PR. No open PR touches the install-local path that this seat could see at claim time; step 1 re-reads that once the files are known.
Selection:priority:p1(triage5945852243), under the maintainer's 「优先 p1 的卡片」 and 「并发保持3」. It takes the slot freed by plugin-security: an org member reading a colleague's sys_user row is served the Admin-group fields (last_login_ip/at, failed_login_count, locked_until, ban_*, phone_number) — and through sys_activity, their sign-in history #21237's patch report.
Ruling quoted to the dev from triage
5945852243(overturnable by the maintainer): "The installed action then gets its handler through the same registration the boot path uses. ⛔ No install-only second registration. One answer: an action whose handler cannot be registered is not advertised bylist_actions. The advertisement and the run door read one source." Pins: "an installed script action runs through REST and MCPrun_action, before and after a restart. An advertised action is always runnable."
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
- Step 1 (this dispatch, read-only outside the lane): measure where an installed app's script action body is lost on the install-local path: in the built artifact (
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{"issue": 21321, "pr": null, "status": "blocked",
"branch": "claude/issue-21321-install-local-script-actions",
"session": "session_01DiCSbmJrkzNhuEAier4VoJ (the PM session that dispatched this subagent run; subagent = parent id)",
"premise_still_valid": true,
"summary": "Step 1 measured, then stopped: the fix lies wholly outside domain:services, so no code was written and no PR opened. The card reproduces on main 04f0cc4: a minimal app with one type script action carrying an inline body, built withos buildand installed withos package install ./dist/objectstack.json(install-local) into an emptyos startruntime, answers REST 404 RESOURCE_NOT_FOUND and MCP run_action "No handler registered for action 'complete_task' on 'tasks_app_task'" while MCP list_actions advertises it, before and after a restart; the same artifact underos start --artifactanswers 200 and run_action ok:true (control). The body is not dropped in the artifact or by the install-local handler (it is in dist/objectstack.json, in the cached ledger entry, and in the SchemaRegistry); it is dropped at HANDLER REGISTRATION: the boot path registers body handlers in AppPlugin.start (packages/runtime/src/app-plugin.ts L1145-1193, owner app:APPID), which an installed package never reaches; MarketplaceInstallLocalPlugin.applySideEffects replicates only translations and seeds; and ObjectQLPlugin.resyncAuthoredActionsNow never sees the action (restart log: registered 0, authoredRows 0, artifactSkipped 0). Every file the fix needs is domain:cli (packages/runtime, packages/cloud-connection) on the recommended route; no packages/objectql, packages/metadata-protocol or packages/spec edit is needed, so the #20790 hold on protocol.ts does not apply. A same-family sibling was measured: an installed package's body hook never runs either (out_of_scope_findings[0]).",
"measured_files": {
"drop_point_boot_path": "packages/runtime/src/app-plugin.ts — AppPlugin.init L430 registers the bundle through themanifestservice; AppPlugin.start L1145-1193 is the ONLY binder of artifact action bodies: collectBundleActions (L2263) + actionBodyRunnerFactory(new QuickJSScriptRunner(), {ql, logger, appId}) + ql.registerAction(objectKey, action.name, handler, app:APPID) at L1171. The sibling hook binder is L1090-1143 (ql.bindHooks with packageId app:APPID, functions, bodyRunner).",
"drop_point_install_path": "packages/cloud-connection/src/marketplace-install-local-plugin.ts — handleInstall (L637) step 3await manifestService.register(manifest)(L894), step 5applySideEffects(ctx, manifest, {seedNow: true, c})(L952); rehydrate (L280) on kernel:ready repeats register (L306) andapplySideEffects(ctx, entry.manifest, {seedNow: false})(L318). applySideEffects (L1414) loads translations and merges seed datasets ONLY: no action handler, no hook, no job, no function binding.",
"registry_side": "packages/objectql/src/plugin.ts — themanifestservice register (L423-505) calls ql.registerApp and bridges OBJECTS only into the metadata service (bridgeManifestObjectsToMetadataService); resyncAuthoredActionsNow (L2749) reads metadataService.loadMany(action) plus sys_metadata rows, filters isArtifactShippedAction (L2320, whose comment assumes AppPlugin registered artifact actions), and binds through the default runtime-authored runner under owner metadata-service. The installed action is in neither source, so it is never seen (measured).",
"advertisement": "packages/runtime/src/domains/mcp.ts listActions (L686-718) reads DECLARATIONS (actionExec.collectActionDeclarations: meta.listObjects() object actions + meta.loadMany(action)) and admits a script action on BODY/TARGET PRESENCE via isHeadlessInvokableAction (packages/runtime/src/action-execution.ts L585: type script → Boolean(action.target || action.body)). It never consults a handler.",
"run_doors": "MCP run_action: invokeBusinessAction (action-execution.ts L2094) → executeRegisteredAction (L2505) → ql.executeAction; REST POST /api/v1/actions/OBJECT/NAME: packages/runtime/src/domains/actions.ts L818-831 → the same executeRegisteredAction → 404 on a miss. ql.executeAction reads the engine actions Map (packages/objectql/src/engine.ts L4528), filled ONLY by registerAction (L4519). Two sources today: declaration presence vs handler Map.",
"lanes": "packages/runtime and packages/cloud-connection are domain:cli (pm-dispatch SKILL.md lane table); packages/objectql is domain:engine. No domain:services file is on the path: install-local writes no sys_packages row and no sys_metadata row (measured: both empty after install), so service-package and the metadata-protocol package doors are not involved."
},
"tests": "No code changed, so no unit test, ablation or gate ran on a diff. Measurement, all on main 04f0cc4 in worktree objectstack-issue-21321: (1) build:bash scripts/pm/os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm exec turbo run build --filter=@objectstack/cli... --concurrency=2'→Tasks: 59 successful, 59 total,VERDICT command-exit 0. (2) app: a scratch app (object tasks_app_task, sharingModel public_read_write; standalone action complete_task, type script, locations [record_header], inline body language js with capabilities [api.write], ai.exposed true) built with the worktree CLIos build→Build complete,Artifact: dist/objectstack.json (2.8 KB); the artifact carries the body at actions[0].body (source 138 chars) and at objects[0].actions[0].body. (3) install:OS_CLOUD_URL=off os start -p 41321 --home H --auth-secret S(empty boot), sign-up 200,os package install ./dist/objectstack.json --runtime http://localhost:41321 --email E --password P→Package installed into the running kernel; the ledger entry .objectstack/installed-packages/com.example.tasksapp.json keeps both body copies; sqlite reads after install:sys_packages: [], sys_metadata rows for the package: []. (4) doors after install: POST /api/v1/data/tasks_app_task → 201; POST /api/v1/actions/tasks_app_task/complete_task → 404 {"code":"RESOURCE_NOT_FOUND","message":"Action 'complete_task' on object 'tasks_app_task' not found"}; MCP over Streamable HTTP with an x-api-key minted by POST /api/v1/keys (201): list_actions → complete_task listed, totalCount 1; run_action → isError true, "No handler registered for action 'complete_task' on 'tasks_app_task'"; the record still reads status null. (5) restart, same home and cwd, --log-level debug:[MarketplaceInstallLocal] rehydrated com.example.tasksapp@0.1.0, then[ObjectQLPlugin] re-synced runtime-authored actions {\"registered\":0,\"authoredRows\":0,\"artifactSkipped\":0,\"skippedNoHandler\":0}; REST 404 and the list_actions / run_action pair repeat byte-for-byte. (6) control:os start --artifact dist/objectstack.jsonon a fresh home:[AppPlugin] Bound declarative actions {\"appId\":\"com.example.tasksapp\",\"actionCount\":2}; REST → 200 {"success":true,"data":{"ok":true,...}} and the record reads status done, done true; MCP run_action → ok true. (7) sibling hook probe: same app plus hook tasks_app_stamp_status (beforeInsert, body stamps status) → after install-local, POST /api/v1/data/tasks_app_task answers status null; underos start --artifactit answers status stamped. (8) gates:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands→ "this branch changes nothing against origin/main (merge base 04f0cc4) - nothing to derive" (exit 2); for the seat, the same tool over the 5 route-A paths in open_questions[0] and [1] derives 50 gate commands, and --tier reports no path-derived mandate. Every server I started was stopped by its recorded PID; ports 41321-41324 read free afterwards.",
"mcp_calls": "0 — no MCP GitHub call of any kind",
"api_writes": "1 — POST repos//issues/21321/comments (this os-dev-report, through scripts/pm/post-stamped.mjs and the fleet-write relay). Not REST: onegit push -u origin claude/issue-21321-install-local-script-actionsof the empty branch (the write-route probe). No pr_create and no label-write: step 1 stopped, so there is no PR.",
"open_questions": [
{
"question": "Step 2 is out of this lane: which route gives the installed action its handler through "the same registration the boot path uses"? Every option edits domain:cli files (option B also domain:engine), so the seat must revise the claim and post the cross-lane declarations before any edit.",
"options": [
"A — Extract AppPlugin.start's action-binding loop (app-plugin.ts L1145-1193) into ONE exported @objectstack/runtime function (exported in packages/runtime/src/index.ts beside collectBundleActions at L61). AppPlugin.start calls it unchanged; MarketplaceInstallLocalPlugin.applySideEffects (marketplace-install-local-plugin.ts L1414, reached by the install hot path L952 and the kernel:ready rehydrate L318) calls it with the installed manifest. Files: packages/runtime/src/app-plugin.ts, packages/runtime/src/index.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, plus tests. Lane: domain:cli only. Design point: the function tears down the app:APPID action set before binding, so a reinstall of a newer version drops actions it removed (a no-op on the boot path). Axes: business need - the measured failure is exactly this door, the documented community and air-gapped install path (NORTH-STAR step 4 to 5); long-term - one binder with two callers, under the app:APPID owner that isArtifactShippedAction already assumes, which makes that comment true for installed packages, but it leaves AppPlugin.start rather than manifest.register as the binding seam; AI errors - declaration and handler come from one function, so declared means runnable on install, with no consumer tolerance; startup scope - smallest diff, one lane, but a new public runtime export likely makes Clause-2 "yes (widening)" with a minor changeset for @objectstack/runtime (the claim says "no" and would need revising).",
"B — Bind body handlers inside ObjectQLPlugin's manifest service register (packages/objectql/src/plugin.ts L423-505, domain:engine) through a runner the runtime injects, and delete AppPlugin.start's loop (packages/runtime, domain:cli), so every manifest.register caller (AppPlugin.init L430, install-local L894 and L306) is bound at one seam. Axes: business need - the same measured door, plus any future register caller (no other caller measured broken); long-term - the deeper unification, at the cost of carrying sandbox-runner wiring into objectql, which is deliberately sandbox-free, and of changing the boot path of every app; AI errors - strongest, no register path can skip binding; startup scope - two lanes and a larger blast radius, every boot app re-proven.",
"C — Let ObjectQLPlugin.resyncAuthoredActions bind artifact actions that lack a handler through the engine's default runtime-authored runner. Rejected on the ruling: it is an install-only second registration (owner metadata-service, runner appId runtime-authored, silently disabled by OS_DISABLE_AUTHORED_ACTIONS=1), and the measured resync never even sees the installed action (artifactSkipped 0)."
],
"recommendation": "A, because it is the ruling's "same registration" with the smallest surface, in one lane, and it closes exactly the measured door. B only if the maintainer wants the register seam to own binding for every caller; C is excluded by the ruling."
},
{
"question": ""One answer": how should list_actions read the handler source the run doors read? Today the advertisement admits a script action on body presence (isHeadlessInvokableAction, action-execution.ts L585) while both doors dispatch through executeRegisteredAction (L2505) to the engine handler Map.",
"options": [
"A — Add one probe beside executeRegisteredAction that walks the SAME rotation (actionHandlerObjectKeys(objectName) x resolveActionHandlerKeys(action)) over ql.listRegisteredActions() (engine.ts L4547, already public), and gate list_actions' script arm on it (packages/runtime/src/domains/mcp.ts L686-718); the declarative-update and flow arms keep their own predicates because they do not dispatch through the Map. Files: packages/runtime/src/action-execution.ts, packages/runtime/src/domains/mcp.ts. domain:cli only, no objectql edit. Axes: business need - ai.mcp-run-action-exposure-gate NEG1 is the measured failure; long-term - one addressing algorithm shared with the run door (ADR-0110 D2); AI errors - the advertisement can no longer outrun the handler, whatever install path is used; startup scope - one lane, no new engine API.",
"B — Add hasAction(object, key) to the engine (packages/objectql/src/engine.ts, domain:engine) and probe it. Axes: same effect, but a second engine member duplicating listRegisteredActions, in a second lane."
],
"recommendation": "A, because listRegisteredActions already exposes the Map and sharing the rotation with executeRegisteredAction is what makes the two doors read one source."
},
{
"question": "A same-family drop was measured: an install-local package's body hook never runs (out_of_scope_findings[0]). Fold it into this card's claim revision, or route it to the package-install family closer card?",
"options": [
"A — Fold into the #21321 revision: the route-A extraction covers AppPlugin.start's ql.bindHooks block (app-plugin.ts L1090-1143) the same way, at the same call site in applySideEffects, with the same pin shape. Widens this card's file surface by no new file.",
"B — Leave it to the family closer card, so #21321 stays exactly the action half."
],
"recommendation": "A, if the seat's revision can carry it: one defect class, one fix point, one lane; not decided here because it widens this card's scope."
}
],
"out_of_scope_findings": [
"class: a · reach: measured at a public door on main 04f0cc4 — afteros package install ./dist/objectstack.json(install-local) of an app whose hook tasks_app_stamp_status (beforeInsert, inline body sets status) is in the artifact, POST /api/v1/data/tasks_app_task answers 201 with status null (the body never ran); the same artifact underos start --artifactanswers status stamped (control) · evidence: applySideEffects (marketplace-install-local-plugin.ts L1414) replicates translations and seeds only, and AppPlugin.start's ql.bindHooks(hooks, {packageId: app:APPID, functions, bodyRunner}) (app-plugin.ts L1090-1143) has no install-path caller · same family as this card, so it is named for folding (open_questions[2]), not filed as a single-point card · dedupe words: install-local hook body not bound · os package install hooks do not fire · installed package beforeInsert hook · MarketplaceInstallLocal applySideEffects bindHooks",
"NOT MEASURED (source read only): defineStack jobs scheduling and functions registration for an install-local package. AppPlugin.start schedules jobs on kernel:ready and hands functions to bindHooks; install-local replicates neither · carrier: the step-2 PR of this card, which edits the same applySideEffects · noted, not filed",
"NOT MEASURED: whether the POST /api/v1/packages door (protocol.installPackage) gives an installed script action a handler; a grep for registerAction / getDefaultActionRunner / resyncAuthoredActions over packages/metadata-protocol/src, packages/services/service-package/src and packages/cloud-connection/src (non-test) has 0 hits; the door was not driven · carrier: 承接者:无 · noted, not filed",
"observation, not a finding: the [action-audit] line "REST action ... body executes TRUSTED" (and its MCP twin) is printed before dispatch, so it is logged on every request above that then answered 404 / "No handler registered" · carrier: 承接者:无 · noted, not filed"
],
"deviations": [
"My first background build command set a shell variable inside the backgrounded subshell, so its PID file was written to /build-cli.pid at the filesystem root; the harness safety check refused the removal, so that 5-byte file is still there and is safe to delete by hand.",
"The dispatch asks for the PR number on the FIRST line of the final message; the os-dev contract says the final message is this JSON and nothing else. JSON-only was kept, and the first JSON line carries "pr": null so both read."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionspm:retriage: the fork and the lane question the step-1 report left ·domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-02T06:33ZBasis: the dev's step-1 report
5946762655(statusblocked, no code, no PR; measured onmain04f0cc499). The card stays claimed (pm:dispatched,os-bill), andpm:retriageis added beside it.Measured:
- The card reproduces exactly. After
os package install(install-local), REST answers 404 and MCPrun_action"No handler registered", whilelist_actionsadvertises the action, before and after a restart. The same artifact underos start --artifactanswers 200 (the control). - The body is not dropped. It is in
dist/objectstack.json, in the install ledger entry and in the registry. - It is never bound. The only binder of artifact action bodies is
AppPlugin.start(packages/runtime/src/app-plugin.ts:1145-1193, ownerapp:APPID). The install-local plugin (packages/cloud-connection/src/marketplace-install-local-plugin.ts) replicates only translations and seeds (applySideEffects,:1414), on both the install path (:952) and the rehydrate (:318). - The advertisement and the run doors read two sources.
list_actionsadmits a script action on body presence (isHeadlessInvokableAction,packages/runtime/src/action-execution.ts:585), while REST andrun_actiondispatch through the engine's handler map (executeRegisteredAction,:2505). - Same family, also measured: an installed package's body hook never runs either (
AppPlugin.start'sbindHooksblock,:1090-1143, has no install caller). - Lanes: every file on the path is
domain:cli(packages/runtime,packages/cloud-connection). Option B below also touchesdomain:engine(packages/objectql). Nodomain:servicesfile is involved: install-local writes nosys_packagesorsys_metadatarow.metadata-protocolis not on the path, so security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790's hold onprotocol.tsdoes not apply.
Asked of triage (one answer, all three parts):
- Lane. Triage routed this card here as the package-install family, expecting a
metadata-protocolcross at most. The measured fix is whollydomain:cli. Does this seat carry it as a declared cross-lane surface (the cross-domain exception path), or is the card re-routed todomain:cli? A re-route needs the seat's release first: the claim is withdrawn, the assignee andpm:dispatchedare removed, and the step-1 measurement stays on the card. - The binding route (the dev's fork):
- A. Extract
AppPlugin.start's binding loop into ONE exported@objectstack/runtimefunction.AppPlugin.startand the install-local plugin'sapplySideEffects(install and rehydrate) both call it, under the sameapp:APPIDowner, tearing down that owner's prior set on reinstall. One lane (cli). A new public export likely makes itClause-②: yes (widening)(minor), with a contract review owed. - B. Bind inside
ObjectQLPlugin'smanifestserviceregister(domain:engine) through a runner the runtime injects, and deleteAppPlugin.start's loop. Every register caller is then bound at one seam. Two lanes, sandbox wiring carried intoobjectql, and every app's boot path re-proven. - C. Rejected by the ruling: binding unhandled artifact actions in
resyncAuthoredActionsis an install-only second registration, and the measured resync never sees the action. - Dev's recommendation: A. Seat's reading: A, for the smallest surface in one lane and the ruling's "same registration the boot path uses".
- A. Extract
- "One answer" (
list_actionsreads what the run doors read): A, a probe besideexecuteRegisteredActionthat walks the same key rotation over the engine's publiclistRegisteredActions(), gatinglist_actions' script arm (domain:clionly). B would add an enginehasAction(domain:engine). Dev and seat recommend A. - Fold the hook half? The same defect class has the same fix point in route A. The dev recommends folding it into this card. The alternative is the family's closer card. Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 (a hot install leaves record-change flows and permission sets unbound) is the same family and likely the same
applySideEffectsseam.
State:
pm:dispatched+pm:retriage. No build until triage answers.
Generated by Claude Code · https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
- The card reproduces exactly. After
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsTriage:
pm:retriageanswer. Re-route todomain:cli; route A; probe A; the hook half is folded inTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-02T06:52Z. ⛔ Not a claim, ⛔ not a dispatch.This answers
5946787053. The step-1 measurement corrects my grade: install-local never reachesmetadata-protocolorservice-package, so this is not the package family's lane.- Lane: re-route to
domain:cli. Every file on the measured path isdomain:cli(packages/runtime,packages/cloud-connection). That is the whole fix, not a spillover, so it is not a cross-lane exception. Re-routing is limited to an undispatched card, so this seat releases the card with destinationpm:queue·domain:cli, as its ask describes. The step-1 measurement stays here for the cli claim. Triage reads the release at its next round. - Binding route A. One exported
@objectstack/runtimefunction holds the binding loop.AppPlugin.startand the install-local plugin's install and rehydrate paths all call it, under the sameapp:APPIDowner, tearing down that owner's prior set on reinstall.Clause-②: yes (widening),minor, with a contract review. ⛔ Not B (two lanes, every boot path re-proven). ⛔ Not C (an install-only second registration). - One answer, probe A:
list_actions' script arm is gated by a probe besideexecuteRegisteredAction, over the engine's publiclistRegisteredActions(). One source for advertising and running. ⛔ No enginehasAction. - Fold the hook half: yes. It is the same defect class at the same fix point (
bindHooksin route A's function), with a pin for an installed body hook firing.
- Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 (flows and permission sets on hot install) sits on the same
applySideEffectsseam. It is re-routed todomain:cliin this act and blocked on this card, so it reuses the one binding function.
priority:p1is unchanged.
Generated by Claude Code
- Lane: re-route to
- removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 2, 2026 6 remaining items
- added and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Oct 2, 2026 objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p1, to triage's rulings5946982209(route A, probe A, hook half folded in) on the step-1 measurement5946762655. Landed asFixes #21321.
Session:session_01VvcEokUG1tvVxkceYfR5XB
Account:huangyiirene
Branch:claude/issue-21321-install-local-script-actions(the empty branch the released claim left; reused)
Worktree:objectstack-issue-21321
Domain:domain:cli
Seat:domain:cli#1
File surface, derived atorigin/mainf39760864c:- Route A, as built:
- One exported
@objectstack/runtimefunction holds the artifact binding loop: action bodies throughregisterAction, and body hooks throughbindHooks, under the ownerapp:APPID. AppPlugin.startcalls it, and so do both paths of the install-local plugin, install and rehydrate.- On reinstall, it tears down that owner's prior set first.
- ⛔ Not route B (two lanes). ⛔ Not route C (an install-only second registration).
- One exported
- Probe A, as built:
list_actions' script arm is gated by a probe besideexecuteRegisteredAction, over the engine's publiclistRegisteredActions()(packages/objectql/src/engine.ts:4547, which exists atf39760864c). One source serves both advertising and running. ⛔ No enginehasAction, and nopackages/objectqledit. - Expected files:
packages/runtime/src/app-plugin.ts;- a new
packages/runtime/src/module for the binding function, plus its export from the package index; packages/runtime/src/action-execution.ts;packages/runtime/src/domains/mcp.ts;packages/cloud-connection/src/marketplace-install-local-plugin.ts;- pins beside each.
- Pins, each measured red before the fix, on a real
os startplusos package install(install-local):- an installed script action answers on REST and MCP
run_action, before and after a restart; list_actionsadvertises only an action that runs;- an installed body hook fires;
- a reinstall leaves exactly one handler;
- the
os start --artifactcontrol still dispatches.
- an installed script action answers on REST and MCP
- Changeset:
minor, per triage, for@objectstack/runtimeand@objectstack/cloud-connectionas the real diff takes them. - Not in this card: Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 (flows and permission sets on hot install) stays
pm:blockedon this card. It reuses the binding function after this lands. - Stop clause: stop if the fix needs an edit in
packages/objectql,packages/metadata-protocolorpackages/spec, or a second registration path.
(stop on a breach outside these; explain in the report)
Container & model:L,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable; the default).
Clause-②: yes (widening).@objectstack/runtimegains an exported function. Triage ruledminor, with a contract review. The dev re-reads this against the real diff and states the measured arm on the PR's line 2.
Thread-read: 5947715549
Serial constraints cleared:Of the 9 open PRs (all file lists read in this act), none touches app-plugin.ts, action-execution.ts, domains/mcp.ts, the runtime index or packages/cloud-connection source; Version Packages #21352 touches only cloud-connection's CHANGELOG and package.json (release-owned, never merged by a seat). In-flight claims: #21324 / #21347 (this seat) own packages/verify and packages/cli verify; #21207 (needs-user-decision) names packages/runtime/src/domains/meta.ts, which is not on this surface. #21322 is blocked on this card by triage.
- Route A, as built:
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21321,
"status": "done",
"branch": "claude/issue-21321-install-local-script-actions",
"pr": "#21401",
"session": "session_01VvcEokUG1tvVxkceYfR5XB (subagent run; the dispatching PM session's id, per the claim 5948440047)",
"premise_still_valid": true,
"summary": "Route A, probe A and the hook half are implemented as triage ruled in 5946982209. The new runtime export bindAppArtifactHandlers(ql, bundle, {appId, logger, source}) is the one binder of an artifact's action bodies (registerAction) and its hook bodies and bundle functions (bindHooks), under the owner app:APPID. It first removes that owner's previous action handlers and hooks. AppPlugin.start calls it in place of its two inline blocks, and the install-local plugin calls it on the install route and on the kernel:ready rehydrate. MCP list_actions lists a script action only when registeredActionHandlerProbe, which sits beside executeRegisteredAction and reads the engine's public listRegisteredActions() in the run door's key order, finds a handler. Measured with the real CLI (os build, empty os start, os package install): before, after install, reinstall and restart, REST answered 404, run_action answered 'No handler registered', list_actions still listed the action and the hook never ran; after the fix every phase answers 200, run_action returns ok, and the hook fires once. The os start --artifact control is unchanged. No objectql, metadata-protocol or spec edit was needed and there is no second registration path.",
"binding_function": {
"signature": "bindAppArtifactHandlers(ql: IObjectQLEngine, bundle: unknown, options: { appId: string; logger: Logger; source?: string }): { owner: string; hooks: number; functions: number; actions: number }, plus appArtifactHandlerOwner(appId) returning 'app:' + appId. Both are exported from the @objectstack/runtime index, with the types AppArtifactHandlerBinding and AppArtifactHandlerBindingOptions. Module: packages/runtime/src/app-artifact-handlers.ts.",
"callers": [
"packages/runtime/src/app-plugin.ts AppPlugin.start: replaces the two inline blocks, after runtime.onEnable, source 'AppPlugin', same log lines",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts handleInstall step 4c: after manifest.register and syncSchemas, before applySideEffects, appId = manifestId",
"packages/cloud-connection/src/marketplace-install-local-plugin.ts rehydrate: per ledger entry after register and syncSchemas, appId = entry.manifestId. Both install-local calls go through a private bindArtifactHandlers, which loads @objectstack/runtime lazily; a runtime without the export binds nothing and logs a warn (no fallback path)"
],
"teardown": "Before it binds, the function calls ql.removeActionsByPackage(owner) and ql.unregisterHooksByPackage(owner). 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 given a non-empty list. Functions are not removed: they are code, never a JSON artifact's content, and registerFunction replaces by name. The function never throws; a failure is logged at error per half, as the block it replaced did."
},
"probe": "registeredActionHandlerProbe(deps, ql) sits beside executeRegisteredAction in packages/runtime/src/action-execution.ts. It takes one snapshot of ql.listRegisteredActions(), the engine's public enumeration (engine.ts listRegisteredActions, unchanged), and returns a check of objectName and candidate keys that walks actionHandlerObjectKeys x resolveActionHandlerKeys, the order the run door uses. packages/runtime/src/domains/mcp.ts listActions applies it to the script branch only: every action invokeBusinessAction sends to the handler registry, meaning not isDeclarativeUpdateAction and not type flow. The engine comes from deps.getObjectQL(context, envId), the same call the run door makes. An engine without listRegisteredActions lists no script action. No engine hasAction was added.",
"hook_half": "Folded in, as ruled. Body hooks are bound by the same function and owner through ql.bindHooks with the QuickJS hookBodyRunnerFactory. The pins cover an installed body hook firing (spawn pin, all four phases), firing exactly once after a reinstall (an appending hook reads 'stamped', not 'stampedstamped'), and a reinstall of a version without the hook unbinding it.",
"repro": {
"app": "Scratch app: object tasks_app_task; action complete_task (type script, inline body with api.write, ai.exposed); hook tasks_app_stamp_status (beforeInsert body that appends 'stamped' to status). Built with the worktree CLI os build, giving dist/objectstack.json (3.2 KB). Runtime: OS_CLOUD_URL=off os start -p PORT --home H --auth-secret S --no-ui from an empty cwd, first-user sign-up, then os package install ./dist/objectstack.json --runtime ... --email --password, then probes over REST and over MCP Streamable HTTP with a key from POST /api/v1/keys.",
"before_main_f39760864c": "After install: insert 201 with status null; REST action 404 RESOURCE_NOT_FOUND "Action 'complete_task' on object 'tasks_app_task' not found"; MCP list_actions lists [complete_task]; MCP run_action isError "No handler registered for action 'complete_task' on 'tasks_app_task'"; row unchanged. After reinstall: the same. After restart: the same; the log shows rehydrated com.example.tasksapp@0.1.0 and re-synced runtime-authored actions {registered:0,authoredRows:0,artifactSkipped:0}. Control (os start --artifact): [AppPlugin] Bound declarative actions {actionCount:2}, insert gives status 'stamped', REST 200 {ok:true} with the row done, run_action ok:true.",
"after_this_branch": "After install: insert 201 with status 'stamped'; REST 200 {ok:true,id} with the row status done and done true; list_actions lists [complete_task]; run_action {ok:true,action:complete_task,result:{ok:true}} with the row done. After reinstall: the same, and the hook fires once. After restart: the same; the log shows [MarketplaceInstallLocal] Bound declarative hooks {hookCount:1} and Bound declarative actions {actionCount:2}. The control is unchanged."
},
"tests": "All at HEAD 2e4e1ff (main merged at db0cf22) through scripts/pm/os-verify-lock.sh, verdict line command-exit 0 unless noted. runtime: vitest run --project local in 2 shards gave 153 files (2017 passed, 4 skipped) and 152 files (2324 passed, 7 skipped), so 305 files and 4341 passed. cloud-connection full run: 31 files, 401 passed. cli: --project unit in 3 shards, 82+82+82 files, 1322+1072+1095 passed. cli --project integration: only the new file, 17 passed in 69s; the rest of the integration project is declared to CI. typecheck of runtime, cloud-connection and cli: exit 0, check:test-typecheck OK for runtime (27 files / 190 errors / 68 signatures, unchanged ledger) and for cli. Red before the fix: the spawn pin (built-entry shape at that time) had 13 failed and 4 passed (control rows and harness health); the cc pin 4 of 4 failed; the probe pin 3 failed and 3 passed (controls). The spawn pin was later switched to the tsx entry and its ability to go red was shown again by ablation legs A, B, D, E and F. Full pnpm lint (eslint . --no-inline-config) at 2e4e1ff: exit 0. Narrowed lint was also measured: ESLint's own isPathIgnored and calculateConfigForFile report 18 of 18 changed source files linted, 0 ignored, typeAware=false for every file; --format json lists 18 files with 0 errors and 0 warnings.",
"ablations": [
"Every leg ran on the committed fix at 2e4e1ff through scripts/ablation-replace.mjs with --hold and a trap restore. Each restore was proven by 'ok restored: blob == HEAD' and an empty git status --porcelain. Each dist leg rebuilt the package with turbo, ran ablation-dist-preflight (--absent on the mutated build, present again after the restore rebuild), and then rebuilt dist from HEAD.",
"A, deleting the install-route bind call (cloud-connection): cc pin went red on install and both reinstall cases (3 of 4); spawn pin went red on 6 (after install and after reinstall: hook, REST, MCP), with restart and control green as predicted.",
"B, deleting the rehydrate bind call: cc pin went red on rehydrate (1 of 4); spawn pin went red on 3 (after restart: hook, REST, MCP).",
"C1, ql.removeActionsByPackage(owner) replaced by void 0 (runtime): binder pin red on the dropped action still bound; cc pin through the runtime dist red on the dropped-version case. No spawn leg, because the spawn pin does not cover a dropped action (stated in the PR).",
"C2, ql.unregisterHooksByPackage(owner) replaced by void 0: binder pin red with 'a hook the new version dropped must stop firing: expected stamped'; cc pin through dist red on the same case.",
"D, packageId: owner replaced by packageId: undefined: binder pin red on 2 (re-bind gives 'stampedstamped', dropped hook); cc pin through dist red on 3; spawn pin red on 1 (after reinstall, the hook fires twice).",
"E, removing the probe condition in list_actions: probe pin red on 3 (the defect, the stray key, the non-enumerating engine); spawn pin red on 4 (ghost_task listed in all four phases).",
"F, the AppPlugin call replaced by 'void bindAppArtifactHandlers;': spawn pin red on 3 (control hook, REST, MCP). An earlier F run deleted the call outright; the runtime DTS step then failed with TS6133 on the unused import after the JS was emitted. It was re-run as F2 with the type-clean replacement and a 30/30 build, and that is the run quoted."
],
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, with no paths, at 2e4e1ff derived 66 commands. Each was run one by one with its exit code written to a file before any pipe, and all 66 exited 0. --ran with an ':: exit N' annotation on every line gave 'Run reconciliation — 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' and 'a DERIVED zero — all 66 recorded an exit code and none of them is 3'. Verdict lines: check-changeset-no-major 'This diff introduces no major bump'; check-adr-0087-registration 'adds no declared-breaking changeset'; check:test-source-alias OK; check:cli-test-child-env '135 spawn call(s) declare their child's env, all 6 spawn(s) of the built CLI'; check:dual-build-cjs-loads '105 published require entry point(s) across 66 package(s) load', run after a full turbo build (its first run printed PREREQUISITE NOT MET exit 3 on a partial build and was re-run); check:engine-double-contract OK; check:nul-bytes OK; check:slot-lookup holds; check:cross-package-test-inputs OK; check:published-files; check:doc-authoring. The derivation printed a STALE TREE warning: origin/main moved to 3b4efa7 after the merge, and the only derivation input that changed is scripts/sdui-manifest.record.json. The command list is identical (66 = 66).",
"checks_after_push": "Read once after the PR opened, for head 2e4e1ff: 31 check-runs, 9 completed/success, 3 completed/skipped, 19 in_progress, 0 failing. Status: in_progress. Not waited on.",
"files_changed": [
"A .changeset/21321-install-local-binds-artifact-handlers.md (runtime minor, cloud-connection patch, Clause-②: yes (widening))",
"A packages/runtime/src/app-artifact-handlers.ts (the binder)",
"A packages/runtime/src/app-artifact-handlers.test.ts (pin on a real ObjectQL engine)",
"M packages/runtime/src/app-plugin.ts (calls the binder; the two inline blocks removed)",
"M packages/runtime/src/index.ts (exports the binder and the owner helper)",
"M packages/runtime/src/action-execution.ts (registeredActionHandlerProbe)",
"M packages/runtime/src/domains/mcp.ts (list_actions script branch uses the probe)",
"A packages/runtime/src/mcp-list-actions-handler-probe.test.ts (pin)",
"M packages/runtime/src/http-dispatcher.test.ts (two engine fixtures gain listRegisteredActions; forced by the probe)",
"M packages/cloud-connection/src/marketplace-install-local-plugin.ts (bindArtifactHandlers on install and rehydrate)",
"A packages/cloud-connection/src/marketplace-install-local-artifact-handlers.test.ts (pin)",
"M 7 install-local suites in packages/cloud-connection/src (bundle, conflict, id-gate, list-posture, offline-degradation, posture-gate, storage-dir): module-top import '@objectstack/runtime' (clocked-window rule)",
"A packages/cli/test/package-install-local-handlers.integration.test.ts (spawn pin: real os start and os package install)"
],
"deviations": [
"File surface: no source file outside the claim's surface changed. Three test-only additions sit beyond its literal list. (1) The spawn pin in packages/cli/test, because the claim's Pins clause requires a real os start plus os package install, which only a CLI integration test can host. (2) Seven existing install-local suites got a module-top runtime import: every install and rehydrate now reaches the plugin's lazy runtime import, its first load inside a 5000ms it timed out posture-gate and id-gate (measured twice), and the repo's remedy is a module-top load. (3) Two engine fixtures in http-dispatcher.test.ts gained listRegisteredActions, because the probe reads it. The objectql, metadata-protocol and spec stop conditions were not reached.",
"Changeset grade: @objectstack/runtime is minor (it gains the export, so Clause-② is yes (widening)). @objectstack/cloud-connection is patch (a fix with no new public surface), as I read 'by the real diff'. Both packages are in the fixed release group, so they ship at the same version either way. check-changeset-no-major passes.",
"The spawn pin runs os start through the tsx source entry in development and signs in as the dev-admin seed (admin@objectos.ai). The production bin/run.js entry would add the file to check:cli-test-child-env's self-test roster of exactly six built-entry spawners, which needs an edit to that gate script; I did not make that edit.",
"Main was merged once (at db0cf22) before the PR. It has since moved 10 commits (3b4efa7), including objectql and spec changes that touch none of this diff's files. I did not merge again; CI and the merge queue validate the merge.",
"PR footer: the AGENTS.md session-URL footer form was used. The harness attribution reminder's emoji and claude.com form was not, under its own rule that user instructions take precedence.",
"Ablation leg F was run twice. The first build failed in its DTS step (unused import), so F2, with a clean build, is the leg quoted."
],
"mcp_calls": "0 — no MCP GitHub tool was called. All reads were REST or curl with GH_TOKEN, plus git.",
"api_writes": "3 REST writes, all through the fleet-write relay as objectstack-fleet[bot], each relay call being one POST /repos/objectstack-ai/objectstack/dispatches: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls, draft #21401, body read back byte-identical 9718/9718; (2) label-write --assign huangyiirene, POST /repos//issues/21401/assignees, read back as assignee huangyiirene, with labels untouched apart from labeler-bot labels; (3) this os-dev-report comment, POST /repos//issues/21321/comments, via post-stamped. Not REST: several git push runs of claude/issue-21321-install-local-script-actions (the first a fast-forward of the empty claimed branch to f397608; the final head is 2e4e1ff).",
"open_questions": [],
"out_of_scope_findings": [
"NOT MEASURED, source read only: an install-local package's defineStack jobs are never scheduled, because AppPlugin.start schedules jobs on kernel:ready and install-local has no equivalent. The prior dev noted this too · carrier: the PR for #21322 (flows and permission sets on hot install, which edits the same install-local side-effect path and is ruled to reuse bindAppArtifactHandlers) · noted, not filed",
"observation, not a finding: the 'Bound declarative actions {actionCount: N}' line counts registrations, not distinct handlers. One action collected from both actions[] and objects[].actions[] reads 2 for a single Map entry (measured: actionCount 2 for one action, both before and after the fix) · carrier: 承接者:无 · noted, not filed (PR Acceptance notes)",
"observation, not a finding: two AppPlugins sharing one app id on one engine now replace each other's app:APPID action set, where before only hooks were replaced. No caller of that shape was found (the owner literal has exactly two writers, both this binder's) · carrier: 承接者:无 · noted in the PR Acceptance notes"
],
"cleanup": "80 node_modules directories were removed from the worktree before this comment was posted. The worktree itself (../objectstack-issue-21321) is removed with git worktree remove, without --force, immediately after posting, because it holds the posting script. Before that: git status --porcelain is empty and the local head equals origin claude/issue-21321-install-local-script-actions at 2e4e1ff, so nothing unpushed exists. Every server and test child I started was stopped by its own process group; no process naming the worktree remains."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsACCEPT: PR #21401 at
2e4e1ff4(an installed package's script actions and body hooks are bound;list_actionslists only what runs).Fixes #21321; landing through the queuedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T11:57Z- Contract review of record:
5951839992on the PR, atCONTRACT_REVIEW_TIER, head2e4e1ff4, PASS.- Route A:
bindAppArtifactHandlersis the one binder under ownerapp:APPID.AppPlugin.startand both install-local paths call it. No second registration path remains, and no boot path binds twice.os start --artifactis unchanged apart from a teardown that does nothing on a first bind. - Probe A: the probe's key walk matches the run door's exactly, so nothing is advertised that the door refuses and nothing runnable is hidden. There is no engine
hasActionand nopackages/objectqledit. - Hook half: an installed hook fires once, and a reinstall unbinds a hook the new version dropped.
- Semver:
@objectstack/runtimeminorwithClause-②: yes (widening), and@objectstack/cloud-connectionpatch, are right.
- Route A:
- Seat verification:
- Checks on
2e4e1ff4, collapsed latest-per-name: 34 names, 31success, 3 skipped, 0 red. - The net diff is 19 files, +1277 / −104.
check-governed-merges --pr 21401reads not governed.mergeable_statereadsclean. Fixes #21321is the only closing keyword.
- Checks on
- Deviations, accepted:
- The spawn pin sits in
packages/cli/test, the only place a realos startplusos package installcan run; the tsx entry is sanctioned bycheck-cli-test-child-env's own header. - Seven install-local suites gain a module-top runtime import, under the clocked-window rule; no assertion is touched.
- Two
http-dispatcher.test.tsfixtures list exactly the keys their doubles already answer.
- The spawn pin sits in
- Out-of-scope notes, one line each:
- An install-local package's
defineStackjobs are never scheduled. This is NOT MEASURED, from a source read only. Carrier: Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322, which edits the same install-local side-effect path and reuses this binder. The seat records it on Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 in this act so the carrier is explicit. list_actionsdoes not consult the ADR-0126 activation ledger thatrun_actionchecks first, so a switched-off action could still be listed. It predates this card and is NOT MEASURED; Acceptance notes. A card follows only with a measured reach.- The
actionCountlog line counts registrations, not distinct keys. Pre-existing, Acceptance notes. - Two AppPlugins sharing one app id now replace each other's action set, as hooks already did. No such caller was found; Acceptance notes.
- A code comment at
app-artifact-handlers.tsL62-64 says the re-sync "reads the same owner", but it reads the registry's artifact items. The described effect holds. It rides the next change to that file.
- An install-local package's
- Unlocks: Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322 is
pm:blockedon this card. Its unlock is triage's scan once this card closes. - Landing: not governed, so
pr_readyandautomerge_enablefollow in this act.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsLanded: PR #21401 →
1d0600bf66(an installed package's script actions and body hooks are bound;list_actionslists only what runs).Fixes #21321: the card is completedomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-10-02T12:22Z- Landing reading:
1d0600bf66is onorigin/mainas a single-parent squash: 19 files, +1277 / −104. Itsgit patch-id --stableis57e711880bbe…, equal to the reviewed net diff at2e4e1ff4. - The card: closed as completed by this merge.
pm:dispatchedis removed in the same act as this note. - Unlocks: Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322, which is
pm:blockedon this card, can now reusebindAppArtifactHandlers. The jobs half is noted on Hot install via os package install leaves record-change flows unbound and the package's permission sets unprojected until a restart, and says nothing #21322. Triage's unlock scan moves it.
Generated by Claude Code
- Landing reading:
- added 3 commits that reference this issue
on Oct 7, 2026
QA-source: #21318 · ai.mcp-run-action-exposure-gate · clause 2
Summary
On the published 17.6.0 train, an app installed into a running runtime through the documented community path —
os package install ./dist/objectstack.json(air-gapped inline install, no control plane) — registers its objects, app, flows and permission metadata, but none of itstype: 'script'actions with an inlinebodygets a handler. Every door refuses the action, even after a restart, while MCPlist_actionskeeps advertising it. The same artifact booted withos start --artifactdispatches normally.This breaks NORTH-STAR path step ④→⑤ (install into an environment, then have an Agent complete a real business operation): the exposed business action cannot run in the environment the app was installed into. It also violates
ai.mcp-run-action-exposure-gateNEG1 ("list_actions advertising an action that run_action then refuses is a FAIL") and NORTH-STAR priority 4 ("MCP 面暴露的就是应用真能做的").Reproduction (published packages, verified twice plus once by an independent agent)
npm create objectstack@17.6.0 tasks-app; add an objecttasks_app_taskand an action:npx os build(validate/lint green).OS_CLOUD_URL=off npx os start -p 4340 --home ./home --auth-secret <32+ chars>;POST /api/v1/auth/sign-up/email(withOrigin) for the first owner.npx os package install ./dist/objectstack.json --runtime http://localhost:4340 --email … --password …→✓ Package installed into the running kernel.POST /api/v1/data/tasks_app_task {"name":"probe"}→ 201.POST /api/v1/actions/tasks_app_task/complete_task {"recordId":"<id>"}200 {"success":true,"data":{"ok":true,…}}404 {"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Action 'complete_task' on object 'tasks_app_task' not found"}}POST /api/v1/keys; MCP Streamable HTTP withx-api-key:list_actions→ listscomplete_task;run_action {actionName:'complete_task', recordId}→isError: true,No handler registered for action 'complete_task' on 'tasks_app_task'. Record unchanged.npx os start --artifact ./dist/objectstack.json→ step 5 answers 200 and the record readsstatus: done; MCPrun_action→{ok:true}.Mechanism (read from source at the subject, for the fixer to confirm)
AppPluginstart (packages/runtime/src/app-plugin.ts, the block that callsql.registerAction(objectKey, action.name, handler, 'app:'+appId)for actions carrying an extractedbody).MarketplaceInstallLocalPlugin(packages/cloud-connection/src/marketplace-install-local-plugin.ts) registers the manifest and then, per its own comment, "Replicate[s] the AppPlugin start-time side-effects that the manifest service does NOT do on its own" — translations and seeds only. Action bodies are not among them, on the hot path or on thekernel:readyrehydrate.list_actionsreads action metadata, so it advertises what the dispatcher cannot run.Expected
An installed package's
script/bodyactions dispatch exactly as they do underAppPlugin— through REST/actions, MCPrun_actionand the Console button — or the install refuses / says loudly what it did not bring along, andlist_actionsnever advertises an action with no handler.Workaround for users today: deliver the app as the runtime's pinned artifact (
os start --artifact/OS_ARTIFACT_URL) instead of installing it.Generated by Claude Code