Skip to content

Config-booted apps lose their onEnable hook — every script action handler goes unregistered and 404s at dispatch (examples/app-todo: all 8) #4095

Description

@os-zhuang

Found immediately after #4084 made boot-phase warnings visible. The ADR-0110 D5 [action-governance] inventory fired on the platform's own example app, and it was right: the handlers really are missing.

This is the D5 checklist doing exactly the job it was built for, on its first boot where anyone could see it.

The finding

Booting examples/app-todo through os serve (or os dev) reports all eight of its todo_task script actions as unbound:

WARN [action-governance] declared script actions with NO handler — a button wired to
nothing (ADR-0078); add a `body`, or register a handler under the declared `target`
{"count":8,"actions":["todo_task:clone_task","todo_task:complete_task",
"todo_task:defer_task","todo_task:delete_completed","todo_task:export_csv",
"todo_task:mass_complete","todo_task:set_reminder","todo_task:start_task"]}

The example is correctly authored. task.actions.ts declares complete_task with target: 'completeTask', register-handlers.ts registers engine.registerAction('todo_task', 'completeTask', completeTask), and objectstack.config.ts exports export const onEnable = async (ctx) => registerTaskActionHandlers(ctx.ql). That pairing is exactly what resolveActionHandlerKeys resolves, so a registered handler would reconcile.

It is not a false positive — the action 404s

$ curl -sX POST http://localhost:44340/api/v1/actions/todo_task/complete_task \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}'
{"success":false,"error":{"code":"RESOURCE_NOT_FOUND",
"message":"Action 'complete_task' on object 'todo_task' not found","httpStatus":404}}

Every "Mark Complete" / "Start" / "Clone" / "Export CSV" button in the example app is dead over HTTP.

Root cause

AppPlugin runs, reaches the runtime-hook resolution at packages/runtime/src/app-plugin.ts:563, and finds nothing:

$ os serve objectstack.config.ts --dev --log-level debug | grep onEnable
DEBUG No runtime.onEnable function found {"appId":"com.example.todo"}

So the bundle AppPlugin receives has no onEnable, even though the config module exports one. Note that serve.ts carries explicit code for exactly this hazard:

// Preserve module-level named exports (e.g. `onEnable`, `onDisable`// lifecycle hooks) that would otherwise be dropped when we unwrap// `mod.default`. Without this AppPlugin can never invoke runtime hooks// declared as `export const onEnable = ...` alongside the default// `defineStack(...)` export.

…and app-plugin.ts:559-565 carries a matching fallback for the same reason. Both guards exist, and the hook still does not arrive — so the loss is somewhere between them. The [StandaloneStack] artifact read path is the obvious suspect: dist/objectstack.json is pure JSON metadata and cannot carry a function, so if the bundle handed to AppPlugin is derived from the artifact rather than the loaded module, metadata (the 8 declarations) survives and code (the handlers) does not — which is precisely the observed split.

Repro

cd examples/app-todo
os serve objectstack.config.ts --port 44340 --dev
# banner now shows the [action-governance] WARN naming all 8 (needs #4084)# then, with a session token:
curl -sX POST localhost:44340/api/v1/actions/todo_task/complete_task \
-H "Authorization: Bearer $TOKEN" -d '{}'# → 404

Why this matters

  • declared ≠ enforced at the call site (Prime Directive chore: version packages #10): the app declares eight executable actions and the runtime serves none of them. The showcase demonstrates a capability that does not work.
  • It is silent without fix(cli): serve's boot-quiet window buffers plugin log lines instead of discarding them (#4012) #4084. The 404 only appears if someone clicks the button; nothing at boot said the handlers were missing, which is how this survived.
  • ADR-0110 D3 refuses undeclared handlers at dispatch with no opt-out. The mirror case — declared, handler dropped by the host — currently fails the same way at the same place, with the operator having done nothing wrong.

Scope check worth doing

I only verified the os serve <config> path on examples/app-todo. Worth confirming whether the compiled-artifact path (os buildos start), the os dev fast path, and app-showcase / app-crm behave the same, since they share AppPlugin.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions