Skip to content

Published plugin types do not enforce what CommandRegistry.register now enforces at runtime #1007

Description

@philcunliffe

Found by Neutral's ship-risk shadow gate on PR #1004 (record: #1004 (comment), classified high e4).

The gap

PR #1004 makes CommandRegistry.register copy the registration with a spread before validating and storing it. A spread carries own enumerable properties only, so the runtime now narrows what it accepts. The gate measured a base-vs-head differential and found three shapes went from working to throwing, and two semantics from live to snapshotted:

Shape Before #1004 After #1004
class MyCommand with run() on the prototype worked (stored with prototype run intact) throws missing run()
Object.create(proto) registration worked throws
non-enumerable run worked throws
getter properties live on every read snapshotted at register time
post-register mutation of the registration object visible to the registry invisible

That narrowing is deliberate and correct, and the gate confirmed zero in-repo reliance on any of it across all 248 .register( sites (the only such uses are the PR's own negative tests).

The problem is that the published types do not say so. hypaware-plugin-kernel-types.d.ts ships in package.json#files for hypaware@1.25.0, so CommandRegistry.register is a third-party plugin API. A plugin author writing:

class MyCommand implements CommandRegistration {
  name = 'my cmd'
  summary = '...'
  usage = '...'
  run() { /* ... */ }        // on the prototype
}

compiles clean against the shipped .d.ts (tsc --strict exits 0) and then throws at runtime. PR #1004 states the rule in a JSDoc comment, which is prose a compiler cannot enforce.

Blast radius is bounded but not nil: src/core/runtime/loader.js:108 catches per plugin and logs plugin.activate_failed, so the kernel and other plugins keep running. The failing plugin simply does not load.

Options

  1. Tighten the type so the compiler rejects what the runtime rejects. Most durable, but needs care: expressing "own enumerable properties only" in TypeScript is awkward, and an over-tight type could reject shapes that do still work.
  2. A release note on whichever release carries PR Deferred PR #857 review findings: registry copy, group headers, doc hygiene #1004 naming the three rejected shapes and the two snapshotted semantics, so plugin authors upgrading have something to search for when their plugin stops loading.
  3. Both. Option 2 is cheap and should probably happen regardless of whether option 1 is judged practical.

Worth deciding before the next published release, since the failure mode for a third-party plugin author is a plugin that silently does not load with only a log line to go on.

Backlink

Deferred from PR #1004 at head 20dd24cb6d6c95aaa15adc0125f9eb95e111dae9.


Correction (later tick): this issue originally named 1.26.0 as the release to note this in. That was wrong. 850fee36 published v1.26.0 without PR #1004, so the narrowing described above is not in 1.26.0 and will first ship in whatever release carries #1004. The hypaware@1.25.0 reference above is still accurate for where the published .d.ts stands today.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    neutral:fixDelegate this issue to neutral for an autonomous fix attempt (reproduce -> fix -> PR)neutral:stuckneutral attempted this but cannot complete it autonomously — needs a human

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions