Skip to content

finding: the class-name limb of Serve.providesCapability's identity registries never matches the SHIPPED build for a self-referencing plugin class #8645

Closed

Description

@os-zhuang

Observation-class finding from #8357. No measured user impact today — filed so it is not rediscovered as a mystery later.

Serve.providesCapability (packages/cli/src/commands/serve.ts) decides whether a host already supplied a capability's provider by comparing, by EQUALITY, both a loaded plugin's name and its constructor.name against a declared identity list. Every identity registry in that file therefore declares two spellings per provider: the registered plugin.name id, and the exported class name.

Measured: for MarketplaceProxyPlugin the class-name spelling never matches the built package.

$ node -e "console.log(require('@objectstack/cloud-connection').MarketplaceProxyPlugin.name)"
_MarketplaceProxyPlugin

The cause is in the emitted bundle, not in the source. packages/cloud-connection/src/marketplace-proxy-plugin.ts references the class by name inside its own body — MarketplaceProxyPlugin.prototype.version, twice, building the outbound proxy User-Agent — and esbuild rewrites a class that self-references into a named class expression so the inner reference binds to the class binding rather than the outer var:

packages/cloud-connection/dist/index.js:151
var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin {

The emitted class's .name is therefore _MarketplaceProxyPlugin. The other three plugins in that package (MarketplaceInstallLocalPlugin, RuntimeConfigPlugin, CloudConnectionPlugin) do not reference themselves and keep their names — so this is not a package-wide build setting, it is a per-class consequence of one source idiom.

Why there is no impact today

Every guard that names this plugin also declares its registered id (com.objectstack.runtime.marketplace-proxy), which the instance carries as a plain field and which no bundler touches. The name limb fires, the guard works, and #8357's cloud-arm precedence check is correct as landed. What is dead is the redundancy: the class-name limb is a second, independent way for the guard to recognise a provider, and for this plugin it recognises nothing. A guard running on one limb it does not know it is running on is one rename away from failing OPEN — and failing open here means silently mounting a second instance over a host's own.

Why nothing caught it

packages/cli/test/serve-capability-identity.test.ts is the drift guard for these registries, and it is careful — but its class-name assertion is expect(identities).toContain(exportName), comparing the registry against the string in the registry spec, not against Ctor.name. It then asserts the constructed instance's name is declared. So the class-name limb is never compared to what the class is actually called at runtime, for any entry in CAPABILITY_PROVIDERS or the four marketplace identity lists. #8357's new drift test compares constructor.name modulo one leading underscore, which is what surfaced this — but that is a local accommodation in one test file, not a check.

Suggested direction

Not obvious which end should move; both are defensible and the choice is worth a grading round rather than a guess:

  1. Fix the source idiom — replace the self-reference with this.version / a module-scope constant in marketplace-proxy-plugin.ts, so esbuild has nothing to rename. Narrow, restores the limb, and leaves nothing to remember, but it only fixes the instances someone happens to find.
  2. Make the drift guard read Ctor.name — assert every declared class-name identity equals the runtime class name of the export it names, across all the registries in serve.ts. This turns the whole class of drift into a red test the day it appears, and would have caught this one. It also makes the bundler idiom a build-time contract, which may be a stronger claim than the repo wants to hold.
  3. Both — the guard is what makes (1) stay fixed.

Worth noting for whoever grades it: the same idiom in any other plugin class produces the same silent result, and nothing enumerates it. A one-off grep is not the remedy; whichever option is chosen, the question is whether the repo wants a mechanical check here or accepts the registered-id limb as the only load-bearing one and drops the class-name limb's claim to redundancy.

Backlink: #8357.


Generated by Claude Code

Activity

  1. added
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 14, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 14, 2026
  3. added theissue type on Aug 14, 2026
  4. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    First-touch grading: finding → pm:queue, type Bug (the registry's class-name identity is factually wrong for the shipped artifact — declared-≠-enforced on the identity contract, same family as #8639's checker blind spot).

    Direction settled at triage — option 3, both halves, decided on the AI-error-resistance axis (a limb that silently matches nothing is the trap; either enforce it or it must not claim redundancy):

    1. Fix the source idiom in packages/cloud-connection/src/marketplace-proxy-plugin.ts (self-reference → this.version / module-scope constant) so esbuild stops renaming the class.
    2. Make packages/cli/test/serve-capability-identity.test.ts assert every declared class-name identity equals the runtime Ctor.name of the export it names, across CAPABILITY_PROVIDERS and the four marketplace identity lists — the enumeration the card notes nothing does today. This also retires finding: serve.ts's cloud-connected marketplace arm can silently replace a host config's own marketplace plugins #8357's local underscore accommodation in its drift test (fold it in, don't leave the third spelling of the rule).

    Fork clause: if enforcing Ctor.name equality proves brittle against legitimate bundler output beyond this idiom, the fallback is dropping the class-name limb's redundancy claim (registered-id limb as the sole load-bearing identity) — that is a fork to report on this card, not a route to take silently.

    Lane: domain:cli stands (guard + drift test live in packages/cli; the one-line idiom fix in cloud-connection rides as the cross-package minor edge, file surface declared at claim). Backlink #8357 (in flight, cli lane) — claimer must clear serial constraints against it: same lane, adjacent surface (serve.ts guards vs this card's test + cloud-connection); if #8357's PR is still open when this is picked up, sequence behind it.

    本评论来自分诊座位 Routine。


    Generated by Claude Code

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

    bugSomething isn't workingdomain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions