refactor(spec)!: remove manifest.loading — an inert loading block whose sandbox isolated nothing (#4914) - #7067
Draft
os-zhuang wants to merge 4 commits into
Draft
refactor(spec)!: remove manifest.loading — an inert loading block whose sandbox isolated nothing (#4914)#7067os-zhuang wants to merge 4 commits into
manifest.loading — an inert loading block whose sandbox isolated nothing (#4914)#7067os-zhuang wants to merge 4 commits into
Conversation
…hose sandbox isolated nothing (#4914) ADR-0049 enforce-or-remove; maintainer ruling 2026-08-04. The whole `manifest.loading` block (strategy / preload / codeSplitting / dynamicImport / initialization / dependencyResolution / hotReload / caching / sandboxing / monitoring) had zero runtime readers in objectstack, cloud and objectui — every reference lived inside packages/spec itself. `loading.sandboxing` is why this outranked ordinary inert-key cleanup: it declared process/vm/iframe/web-worker isolation, IPC transports and an `allowedServices` ACL and applied none of it. An inert security control is worse than an absent one, because it is believed (ADR-0033). Hot reload converges on the surviving vocabulary: `HotReloadConfigSchema` (read by `HotReloadManager`) is KEPT, unenforced, as the starting point for a separate future decision. - `Manifest.loading` → `retiredKey()` tombstone (ManifestSchema is not strict, so a plain deletion would silently strip it) - 11 whole defs unpublished, registered in RETIRED_DEFS_BY_MAJOR[17] - `kernel/Manifest:loading` in RETIRED_KEYS_BY_MAJOR[17] + D3 SemanticMigration `plugin-manifest-loading-retired` (no D2: a manifest is not a stack collection) - pin tests, changeset, PLUGIN_STANDARDS §5.1/§5.2/§5.4, PROTOCOL_MAP, baselines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
#4914 retirement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
#7040 (#5488) appended `api-runtime-create-withdrawn` to step17.semantic while this branch appended `plugin-manifest-loading-retired`; the conflict was purely additive and BOTH are kept. Recounted from the file (#6526): RETIRED_KEYS_BY_MAJOR[17] = 12 entries, RETIRED_DEFS_BY_MAJOR[17] = 45 — every sibling's registrations survive. Generated artifacts regenerated from the merged tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PiRUoQkTSBBmpyXBY3cVn2
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4914
Retires the plugin manifest's whole
loadingblock under ADR-0049enforce-or-remove. The block declared a complete plugin loading policy —
strategy,preload,codeSplitting,dynamicImport,initialization,dependencyResolution,hotReload,caching,sandboxing,monitoring— andnothing read any of it. Authoring it parsed cleanly, entered the manifest, and
configured nothing.
loading.sandboxingis why this outranked ordinary inert-key cleanup. Itdeclared
isolationLevelofprocess/vm/iframe/web-worker, IPCtransports and an
allowedServicesACL, and applied none of it. An author —very often an AI (ADR-0033) — read that vocabulary as proof the platform
isolates plugins, wrote the config, and got a clean parse and zero isolation. An
inert security control is worse than an absent one, because it is believed.
Maintainer ruling (2026-08-04) — quoted verbatim, untranslated
The hard prerequisite: three-repo bare-name reverse lookup
Both halves clean. Each probe ran with a control probe proving the scan saw the
tree, per the #4878 method.
objectstack (this repo) — every hit for the eleven schema names and
manifest.loadinglives insidepackages/specitself: the declarations, themodule's own unit tests, the
manifest.zod.tsembed, one type-alias pin, andgenerated artifacts. Zero readers in
packages/core,packages/runtime,packages/metadata.cloud — measured by the spec seat against cloud
origin/main, fetched2026-08-09 ~06:50Z (issue comment):
PluginHotReloadSchema|PluginSandboxingSchema|manifest\.loading— 0 hits["'](hotReload|sandboxing)["']— 0 hits@objectstack/spec— hits across many files, so the zeros are realobjectui — measured in this lap against objectui
origin/main=2c632d94ed94b9dcd14aec122452d1769fd64b49:PluginHotReloadSchema|PluginSandboxingSchema|manifest\.loading— 0 hits["'](hotReload|sandboxing)["']— 0 hitsPackagesPage— hits across many source files["'](contributes|capabilities)["'](same quoted-key regex form as probe 2) — hits inplugin-chatbot/src/useAgents.tsandvscode-extension/package.json, so the probe-2 form matches real quoted keys and its zero is realhotReload|sandboxing|isolationLevel|allowedServices) returned exactly one hit —packages/app-shell/src/views/metadata-admin/PackagesPage.tsx:77, a prose comment listing manifest key names in a doc block. Not a consumer, and it refers toPackageManifestrather thanmanifest.loading.sandboxing. I checked whether the spec's cloudPackageManifesthas its own top-levelsandboxing: it does not.Route: tombstone, not plain deletion
ManifestSchemais not.strict()(it closes with no.strict()), so deletingthe key would let zod strip it in silence — trading an inert declaration for an
invisible one, the #3726 / #3733 shape (ADR-0104). So
Manifest.loadingis aretiredKey()tombstone, audible throughtsc(input typenever) and throughthe parse, which raises the prescription itself.
This differs from the neighbouring #3896 / #4834 precedents, which plain-deleted,
and the difference is measured rather than assumed: those shapes were parsed by
nothing, so a prescription could never be received. A manifest is parsed by
real code —
packages/objectql/src/registry.ts:1633andpackages/cli/src/commands/plugin/build.ts:111— so the tombstone reaches anauthor.
The eleven value schemas the key carried are orphaned by its removal and go with
it (playbook §4; an exported schema with no consumer is read as a capability,
#3950).
PluginLoadingEventandPluginLoadingStatedeliberately survive: samemodule, but never embedded in the config block, not authorable, still emitted.
Module adjacency is not evidence.
Why D3-only, no D2 conversion
The dispatch asked for this to be evaluated explicitly rather than copied.
Measured: the conversion chain walks a normalized stack
(
mapCollection(stack, 'objects' | 'views' | …)), andapplyConversionsToStoredItemmaps a metadata type onto one of those collections. A package manifest is
neither —
PLURAL_TO_SINGULARhas nopackagesorpluginsentry, so amanifest is not a stack collection member, and
stored.tsdocuments that a typewith no stack collection passes through unchanged. A
MetadataConversionherewould be a transform with no seam that ever runs.
So the registration is a D3
SemanticMigration(plugin-manifest-loading-retired)plus the exact-key entry, exactly as
automation/ActionDescriptor:isAsync(#6748) and the notification cursor keys (#6361) did in this same major.
Is a D2 conversion needed for stored metadata anyway? No, and the reason is
worth stating because a manifest is durable (
sys_packages). The at-rest readpath is
Registry.validate(), which the code documents as a deliberatediagnostic and not a gate: it catches, logs
[metadata_spec_invalid], andregisters the item anyway so bad metadata is never a data outage. So an installed
package whose stored manifest still carries
loadingkeeps working and degradesto one log line. The enforced channel is
os plugin build, which runsManifestSchema.safeParsewith the author present and exits non-zero carryingthe prescription.
No liveness-ledger change. The ledger walks
BUILTIN_METADATA_TYPE_SCHEMAS(authorable metadata types) and the kernel manifest is not one — there is no
liveness/manifest.jsonandManifestSchemais not registered — somanifest.loadingnever had a row to keep or to orphan.Hot reload: the two-source convergence (ruling §2)
PluginHotReloadSchemawas the dead one of two hot-reload vocabularies, andPLUGIN_STANDARDS.md§5.1 pointed readers at exactly that one. The survivingside is
HotReloadConfigSchema(plugin-lifecycle-advanced.zod.ts), whichHotReloadManager(packages/core/src/hot-reload.ts) actually reads. It isKEPT.
§5.1 now points there and states its real status honestly:
HotReloadManagerexists and is unit-tested, but no runtime composes one — the only
constructions are its own test and
packages/core/examples/phase2-integration.ts— so it is a foundation, not a shipped capability. Enforcing it is deliberately
left to a separate future decision and is not part of this PR.
Docs
manifest.runtimetrust tier, permission declarations); §5.4 rows for Hot Reload and Plugin Isolation corrected.plugin-loading.zod.tsand would have dangled. The capability is real (resolvePluginOrder,packages/core/src/plugin-order.ts), only the schema pointer was wrong, so the row is re-pointed rather than downgraded.PROTOCOL_MAP.mdrow rewritten;content/docs/protocol/kernel/index.mdxsummary line said the plugin system ships "sandboxing", which is now false in every reading, so it names trust tiers instead.Reverse verification — direction predicted first, then measured
Predicted before running: restoring the retired declarations turns 4 of the 5
new pins RED, and
not.toHaveProperty('loading')stays GREEN, because an absentkey is absent either way.
Measured (declarations restored via
git checkout origin/main --, nevergit stash— the stash stack is shared across worktrees):Tests 4 failed | 1 passed (5).loadingblockexpected true to be falseexpected true to be falseexpected true to be falsePluginLoadingConfigSchema must not be exportedloadingpropertyA note on the rejection-envelope floor (ADR-0112 / #6142)
The dispatch set
code+statusas the floor for a rejection-class case. Thatenvelope is the API error surface —
ApiErrorSchema, theERROR_CODE_LEDGERvocabulary, an HTTP status — and a schema tombstone does not raise one. Measured,
not assumed: a
retiredKeyrefusal raises aZodErrorwhose issue carriescodeandpathand has nostatusfield at all.Asserting a
statushere would be a fabrication that reads as verification, sothe pins assert the strongest set this surface really has — refusal, the issue
code, thepathnaming which key was refused, and the prescription text(#5240, wording-as-contract). That still delivers what #6142 is after: the pins
go red if the refusal moves to the wrong key, loses its code, or stops carrying
the fix, none of which a bare
toThrow()can see.Registries, recounted from the file after the merge lap (#6526)
#7040 (#5488) appended
api-runtime-create-withdrawntostep17.semanticwhilethis branch appended
plugin-manifest-loading-retired. Purely additive conflict,union-keep, both retained. Recounted from the merged file:
RETIRED_KEYS_BY_MAJOR[17]— 12 entries; every sibling's survives (3transform, 4 record-picker / card,ui/PageTabsProps:type,automation/ActionDescriptor:isAsync, 2 notification cursor) pluskernel/Manifest:loadingRETIRED_DEFS_BY_MAJOR[17]— 45 entries = 34 pre-existing + the 11 added herestep17.semantic— 44 entries, including both this branch's and refactor(spec)!:apiis code-only — withdraw a runtime create door the endpoint matcher could never read (#5488) #7040'sRatchets fired in sequence, each answered deliberately
Both are the self-proving signal the playbook describes, and the readings are the
evidence the removal is real rather than a silent unpublish:
json-schema.manifest/ratchet (gen:schema silently drops PageTabsProps since #2967 — references regen would delete real docs #2978 / json-schema.manifest.json 的「deliberate removal」删行仍是纪律而非门禁 —— #4650 的同类洞,上移一层(整 schema 级) #4725) named exactly the 11 defs that stopped being emitted — matching theRETIRED_DEFS_BY_MAJORregistration one-for-one. Manifest keys deleted deliberately (11 deletions, 0 insertions).authorable-surface/deletion gate (authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650) then named 67 authorable keys under those defs. Deleted deliberately, adjudicated by route 3 (whole def no longer emitted).kernel/Manifest:loadingcorrectly flipped to[RETIRED]rather than vanishing.Counts: authorable surface loses 67 keys and gains one
[RETIRED]marker.Verification
pnpm --filter @objectstack/spec build— green (after both ratchets were answered)pnpm --filter @objectstack/spec test— 351 files / 9081 tests / 0 failedpnpm --filter @objectstack/spec exec tsc --noEmit— clean.github/workflows/lint.ymllint job — PASScheck:liveness,check:empty-state,check:skill-examples)node scripts/check-adr-0087-registration.mjs --base origin/main— PASS:registered plugin-manifest-loading-retired (new here)type-alias-convention.pin.test.tscount 824 to 823 (Iso441pinnedPluginLoadingStrategySchema; recorded in that file's own receipt style) and a stalegen:strictness-ledgerartifactcheck:i18n/check:i18n-coverage/check:app-nav-i18nare prerequisite-blocked locally in a fresh worktree (they need the built CLI and say "nothing was checked"); no*.form.tsinput or translation key touches this surface, and CI builds the closureGenerated by Claude Code