Skip to content

package-registry is on the always-on slate but Serve.CAPABILITY_PROVIDERS does not key it — the always-on mount is silently inert (#17676 ruling A' runtime half) #19387

Description

@huangyiirene

What is wrong

'package-registry' is an always-on platform capability that mounts nothing, and it fails silently. Ruling A′ on #17676 (decision batch #125, maintainer 「同意」 2026-09-13) landed its spec half; the runtime half — the entry that actually mounts the provider — is not landed, so the ruling's own consequence is false on today's tree.

The chain, measured on origin/main 8271c814253fdd2629d3b608b73de14efb67f168

link reading
the token is always-on packages/spec/src/kernel/platform-capabilities.ts:312+ — PLATFORM_ALWAYS_ON_CAPABILITIES lists 'package-registry'
the spec already names its provider same file :179 — 'package-registry': { package: '@objectstack/service-package', edition: 'open' }
serve force-appends every slate token packages/cli/src/commands/serve.ts:2800-2802, the ALWAYS_CAPS loop
⛔ but the CLI mount table does not key it Serve.CAPABILITY_PROVIDERS (serve.ts:1863+) keys automation · analytics · audit · cache · storage · queue · job · messaging · triggers · realtime · mcp · **marketplace** · email · sms · sharing · pinyin-search · reports · approvals · settings · webhooks — ⛔ no package-registry
⛔ and the miss is silent the no-provider branch warns only when declaredRequires.has(cap) && !PLATFORM_CAPABILITY_TOKENS.includes(cap). A force-appended token that is in the vocabulary fails ⛔ both conjuncts ⇒ bare continue — no warn, no error

⇒ on a stock boot of an app that does not itself declare requires: ['marketplace'] (examples/app-crm declares ['ui','automation']), POST /api/v1/packages reaches protocol.installPackage with no package service and registers the package in memory only. It does not survive a restart. That is #17676's defect, still live.

⭐ Worse than a missing mount: a reader of the spec sees package-registry on the always-on slate and reasonably concludes it is mounted everywhere. AGENTS.md principle 3 — Absence must be loud is exactly what this violates.

The shape question — and how much of it is already decided

Two shapes are available. ⚠️ They are not equal candidates, and the reason is textual rather than a preference:

what it does cost
A — ADD key package-registry at @objectstack/service-package too, leaving marketplace pointing there as well Smallest diff, and hasPluginMatching already stops a double mount. ⛔ But it re-enacts the conflation the carve-out exists to end, and it contradicts ruling A′ item 1's own sentence.
B — MOVE package-registry takes @objectstack/service-package; marketplace is repointed at the browse surface (MarketplaceProxyPlugin / MarketplaceInstallLocalPlugin in @objectstack/cloud-connection, off a resolved marketplace URL, ADR-0008) ⚠️ Changes what requires: ['marketplace'] means for apps shipping today. Needs a migration story, and probably an ADR-0087 disposition.

Ruling A′ item 1 already says what the two names mean, and it is quoted in the spec file itself: the persistence half "is a core capability named for what it is and mounted always … marketplace is left naming only the optional catalogue / browsing half." ⇒ A contradicts the landed ruling; only B makes item 2's sentence ("therefore always find the package service on a stock boot") true while leaving the two names meaning what the ruling says.

⚠️ What ruling A′ did not decide, and this card must: the migration for declarers. The spec's provider row is explicit that it declines to pre-empt this —

"the two rows agreeing on a package is what the carve-out INHERITED, not what it decided: repointing marketplace at the browse surface moves the runtime's own resolver with it and is #17676's engine-lane half, which this row deliberately does not pre-empt."

A real declarer to design against, ⛔ not a hypothetical: examples/app-showcase/objectstack.config.ts:107 declares 'marketplace', and its own comment at :103 says it declares it for "PackageServicePlugin (sys_packages store)" — i.e. for the persistence half. The same file separately wires the browse plugins through plugins[]. ⇒ under B that declaration becomes vestigial (the store arrives always-on) and the browse auto-mount is skipped by the identity guard — but an out-of-repo consumer that declared marketplace for the store has no such explicit wiring, and the taker must say what happens to it.

⭐ The pin that is waiting to be written

packages/cli/test/serve-capability-vocabulary.test.ts deliberately declines to pin that Serve.CAPABILITY_PROVIDERS lacks package-registry, so the engine lane's own change would not be turned red for doing the ruled thing. ⇒ today nothing mechanical would notice if this half were forgotten again — the gap lives only in two prose comments, in that test and in the spec.

Whoever takes this should convert that prose into the pin it was waiting to be able to write. ⛔ Otherwise the next carve-out repeats this exact failure.

Acceptance

  1. A stock boot (an app declaring neither marketplace nor package-registry) mounts the package service, and an API-created package survives a restart — which is A writable package created via POST /api/v1/packages is registered in memory only — it does not survive a restart, and leaves its published metadata orphaned #17676 ruling A′ item 5, whose three-probe acceptance ⛔ cannot be met by anything until this lands.
  2. The absence direction is pinned, so a future slate entry with no mount entry goes red.
  3. The declarer migration is stated in writing, with its ADR-0087 disposition answered rather than assumed.

File surface

packages/cli/src/commands/serve.ts (the mount table) and packages/cli/test/serve-capability-vocabulary.test.ts (the pin). ⚠️ Touching examples/app-showcase/objectstack.config.ts depends on the migration answer, ⛔ not assumed here.

Lane — ⛔ deliberately unlabelled

Left for triage. ⚠️ The two prose notes call this "the engine lane", meaning #17676's lane — but the files live in packages/cli, which the lane table routes elsewhere. ⇒ the routing is genuinely a triage call and ⛔ this filer is not making it and has written no domain:* label.

⚠️ If the taking lane's PM reads the declarer-meaning change as a contract-shape proposal rather than execution, the four-axis decision route is the right escalation — ⛔ this filer is not foreclosing it, and is equally ⛔ not manufacturing a decision the maintainer already made in item 1.

Dedupe words (⛔ the filer does not dedupe)

package-registry inert · CAPABILITY_PROVIDERS · ALWAYS_ON silent continue · marketplace carve-out · #17676 runtime half

Provenance

Surfaced as the fork in the os-dev report on #17676 (PR #19385) and independently re-derived link-by-link by the domain:engine execution PM seat, round 7 — which also records there that the dispatch order's assumption that item 1 discharged item 2 was the seat's own error. Related: #17676 (the ruling) · #18053 (item 1's carrier) · PR #19385 (items 2/3).


Generated by Claude Code

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

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:clipriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions