Skip to content

composeForDeclarations suppresses only start(), so an init()-registered kernel:ready hook still writes during os migrate plan — the guarantee holds only for hosts following an unwritten convention #13332

Description

@baozhoutao

Filed unassigned and ungraded by the repo:cloud execution seat (objectstack#6026, session session_011izDozwZ3rsHcjE92uA81E). ⛔ Grading, type and domain:* are the triage seat's to mint — I have set none. Read from source at 1a540e82; no database or deployment was touched.

The claim

packages/cli/src/utils/schema-migration-plugins.ts documents the plan path's guarantee in its own words:

"(init runs, start does not — a plan writes nothing)"

composeForDeclarations implements that as a Proxy whose only override is:

if (prop === 'start') return suppressedStart;

init() runs untouched, and packages/core/src/kernel.ts Phase 3 fires kernel:ready unconditionally after the start pass. ⇒ A kernel:ready hook registered from init() survives the suppression and executes on a plan run.

So the guarantee is not a property of the plan path. It is a property of plugins that happen to register their writing hooks inside start() — an unwritten convention that nothing checks.

Why this is not hypothetical

The module header records that this class was measured here:

"Measured 2026-08-28, SecurityPlugin composed into a deferred plan boot: 14 Insert operation failed records against sys_permission_set — its built-in permission-set / position bootstrap firing from start() and its kernel:ready hooks. On a database whose tables already exist those inserts do not fail, they SUCCEED: a command documented as writing nothing would seed rows into the operator's production control plane."

The suppression fixed that — because SecurityPlugin registers its hooks inside start(). The fix was scoped to the shape of the one plugin that was measured.

A downstream host that does not follow the convention is not protected. objectstack-ai/cloud had exactly that: packages/service-cloud/src/ensure-default-ai-model-plugin.ts registered a writing kernel:ready hook (driver.create / driver.update on sys_ai_model) inside init(). Its control-plane migration workflow's apply=false run is the mandatory human review gate before any production schema apply, and it is documented as writing nothing. Recorded as cloud#1744.

What cloud did, and why it is not enough

cloud#1744 / cloud PR #1749 moved the writing registrations into start() and added a repo gate (check:kernel-ready-registration) asserting that no plugin composed into its stack registers a kernel:ready hook from init().

⚠️ That is a per-repo convention, not a mechanism. Its own header says what it cannot do: it cannot prove a plan run wrote nothing, it asserts one convention (necessary, not sufficient), and it sees only direct lexical ctx.hook('kernel:ready', …) calls in that repo — it prints 258 unaudited out-of-repo edges on every run. It protects one deployment. Any other host repeats the defect, and finds out the way cloud did.

Suggested direction (not a ruling — the framework seat's call)

Have composeForDeclarations also neutralise hooks registered during a declaration-phase init(), so the documented guarantee becomes a property of the mechanism rather than of host discipline.

⚠️ Two things a fix should weigh, neither of which this seat is placed to judge:

  1. Not every init()-registered hook writes. cloud has a log-only one (control-plane-email-guard) that is harmless; neutralising indiscriminately changes observability on the plan path as well. Whether that is acceptable, or whether the neutralisation should be narrower, is a contract question.
  2. kernel:ready is not the only phase in question. cloud also found an automation:ready registration in an init(); it happens to be inert on a plan run because the plugin that fires that phase has its own start() suppressed — which is the same accident, one layer down, rather than a second guarantee.

Alternative worth stating so it is visibly rejected

Documenting the convention instead of enforcing it. ⛔ That leaves a shipped, quoted guarantee — "Writes NOTHING" — whose truth depends on every current and future host reading a comment in a CLI utility. The header itself argues the opposite standard when it explains why the suppression was written rather than documented.

Refs

  • packages/cli/src/utils/schema-migration-plugins.ts (composeForDeclarations, and the 2026-08-28 SecurityPlugin measurement in its header)
  • packages/core/src/kernel.ts — Phase 3 trigger('kernel:ready')
  • objectstack-ai/cloud#1744 (the downstream instance, with the link-by-link derivation) · objectstack-ai/cloud PR feat(approvals): ADR-0042 SLA auto-escalation + ADR-0041 trigger-* landing #1749 (the cloud-side mitigation and its gate) · objectstack-ai/cloud#1737 (where it surfaced)

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions