Skip to content

fix(cloud-connection,service-automation): stop two plugin classes renaming themselves in the shipped build, and enforce the class-name identity limb against Ctor.name - #8857

Merged
qq9340100 merged 4 commits into
mainfrom
claude/issue-8645-capability-class-name-identity
Aug 15, 2026
Merged

fix(cloud-connection,service-automation): stop two plugin classes renaming themselves in the shipped build, and enforce the class-name identity limb against Ctor.name#8857
qq9340100 merged 4 commits into
mainfrom
claude/issue-8645-capability-class-name-identity

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#8645

Triage settled this as option 3, both halves, on the reasoning "a limb that silently matches nothing is the trap; either enforce it or it must not claim redundancy." Both halves ship here, plus the retirement of #8357's local accommodation.

The defect

Serve.providesCapability (packages/cli/src/commands/serve.ts) recognises a host-supplied provider by comparing, by equality, both a loaded plugin's name and its constructor.name against a declared identity list. Every identity registry there declares two spellings per provider — the registered plugin.name id and the exported class name — so the class-name spelling is a claim about the built artifact.

Measured against the built packages, two of the 27 declared class-name identities matched nothing at all:

MISMATCH CAPABILITY_PROVIDERS.automation declared=AutomationServicePlugin runtime=_AutomationServicePlugin
MISMATCH Serve.MARKETPLACE_PROXY_IDENTITIES declared=MarketplaceProxyPlugin runtime=_MarketplaceProxyPlugin

Both classes referenced themselves by name inside their own body. esbuild rewrites such a class into var X = class _X { … _X … } so the inner reference binds to the class binding, and the emitted class reports _X as its .name.

There was no user-visible impact, because every guard naming these plugins also declares the registered id, which the instance carries as a plain field no bundler touches. What was dead is the redundancy — 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.

What landed

  1. fix(cloud-connection) — the marketplace proxy's MarketplaceProxyPlugin.prototype.version self-reference becomes a module-scope constant. That self-reference was also reading a field that was never there (version is an instance field, so prototype.version was always undefined): the outbound User-Agent announced the ?? '1.0.0' fallback on every request and now announces the real version, 1.1.0.
  2. test(cli) — the enforcement half. Every declared class-name identity, across CAPABILITY_PROVIDERS and the four marketplace identity lists, is compared to the runtime Ctor.name of the export it names in the built package, and must satisfy providesCapability through the class-name limb alone. The identity lists are re-derived from Serve itself, so a fifth list cannot be added unenumerated. finding: serve.ts's cloud-connected marketplace arm can silently replace a host config's own marketplace plugins #8357's "modulo one leading underscore" accommodation is retired rather than left as a third spelling of one rule.
  3. fix(service-automation) — a private static backoff helper called from an instance method, hoisted to module scope. See the scope note below.

Scope note: the third commit is outside the declared file surface

The claim declared packages/cli/test/serve-capability-identity.test.ts plus packages/cloud-connection/src/marketplace-proxy-plugin.ts. The third commit touches packages/services/service-automation/src/plugin.ts, outside that — flagged deliberately rather than absorbed silently.

It is kept here, and the reason is measured rather than argued. Reverting only that commit and rebuilding makes this PR's own guard fail:

FAIL #8645: every declared class-name identity equals the runtime class name
> 'CAPABILITY_PROVIDERS.automation'
AssertionError: CAPABILITY_PROVIDERS.automation declares the class-name identity
'AutomationServicePlugin', but the class @objectstack/service-automation ships is
called '_AutomationServicePlugin' — the limb matches nothing.

So the alternatives to keeping it are: land a knowingly red test, exempt automation from the enumeration (exactly the vacuity the ruling forbids), or split into a two-PR serial chain for a 21-line one-idiom fix in the same defect family. It is isolated in its own commit for easy review or revert, no other open PR or branch touches service-automation, and the card's body anticipated exactly this ("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"). The guard found the second instance before it had even landed, which is what it was built to do.

The fork clause was not taken

The ruling's fallback — dropping the class-name limb's redundancy claim if Ctor.name equality proved brittle against legitimate bundler output — did not trigger. All 27 identities hold under exact equality once the two source idioms are fixed. No special case, tolerance or underscore-stripping was added anywhere; the guard explicitly refuses that route (see the ablation below).

Reverse verification — four ablations, each restored

The guard consumes built output (it imports each package through its exports map, which resolves to dist/), so the two artifact-level ablations rebuild between mutating and judging, and scripts/ablation-dist-preflight.mjs proves the mutation reached dist/ before the run's colour is allowed to mean anything. Ablations C and D mutate serve.ts, which vitest reads from source — no rebuild attaches there, and none was claimed.

#MutationConsumesResult
AReintroduce the prototype.version self-reference in the marketplace proxydist (rebuilt, marker proven present)RED — Serve.MARKETPLACE_PROXY_IDENTITIES, runtime _MarketplaceProxyPlugin
BRevert only the service-automation commitdist (rebuilt, marker proven present)RED — CAPABILITY_PROVIDERS.automation, runtime _AutomationServicePlugin
CPin the bundler's rename _MarketplaceProxyPlugin into the registrysourceRED — "declares class-name identities other than 'MarketplaceProxyPlugin'"
DAdd a fifth identity list to Serve without enumerating itsourceRED — "a new list cannot escape"

Ablation C matters as much as A and B: it proves the obvious way to "fix" a red guard — declaring the bundler's output as an identity — is mechanically refused, which is the fork clause's concern one level up. Each ablation was restored, rebuilt, and the marker proven absent from dist/ afterwards, so no mutated artifact survives in the worktree.

Verification

All runs below at HEAD 782ca97fd (post-merge with main, the tree this PR proposes):

pnpm --filter @objectstack/cli test 122 files, 1358 tests passed
pnpm --filter @objectstack/cloud-connection test 19 files, 138 tests passed
pnpm --filter @objectstack/service-automation test 80 files, 962 tests passed
typecheck (cli, cloud-connection, service-automation) exit 0

Gate families re-derived from the actual changed paths via node scripts/pm/dispatch-gates.mjs, all exit 0: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:objectui-changeset, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:nul-bytes, check:query-options-erasure, check:type-check-coverage, and check:type-check-debt --re-measure (run against a built workspace closure, so it is measured rather than refused).


Generated by Claude Code

…ass body so esbuild stops renaming AutomationServicePlugin
A `private static` called from an instance method made the class reference
itself by name inside its own body; esbuild emits `var X = class _X { … }` for
that shape, so the shipped class was called `_AutomationServicePlugin` and the
`automation` capability's class-name identity in the CLI matched nothing.
Isolated in its own commit: `packages/services/service-automation` is OUTSIDE
this card's declared file surface. It is the same one-line idiom as #8645's
named instance, surfaced by the enumeration the card mandates, and the guard
cannot be green without it — see the PR body.
Refs #8645
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
… the shipped build
`MarketplaceProxyPlugin.prototype.version` inside the class body made esbuild
emit `var MarketplaceProxyPlugin = class _MarketplaceProxyPlugin { … }`, so the
built class reported `_MarketplaceProxyPlugin` and the class-name limb of every
identity registry naming it matched nothing.
The self-reference also read a field that was never there: `version` is an
instance field, so `prototype.version` was `undefined` and the outbound
User-Agent always announced the `?? '1.0.0'` fallback.
Refs #8645
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
…time Ctor.name
Across CAPABILITY_PROVIDERS and the four marketplace identity lists, each
declared class-name identity must equal the runtime name of the export it names
in the BUILT package, and must satisfy `providesCapability` through the
class-name limb alone. The `*_IDENTITIES` statics are re-derived from `Serve`,
so a fifth list cannot be added unenumerated.
Retires #8357's local "modulo one leading underscore" accommodation rather than
leaving a third spelling of the same rule.
Fixes#8645
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
@vercel

vercelBot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 15, 2026 11:20am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/service-automation.

4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx(via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx(via @objectstack/service-automation)
  • content/docs/protocol/kernel/metadata-service.mdx(via @objectstack/cloud-connection)

2 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx(via @objectstack/cloud-connection, @objectstack/service-automation)
  • content/docs/releases/v9.mdx(via @objectstack/service-automation)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants

@qq9340100@claude