Repository navigation
fix(cloud-connection)!: install-local reads the manifest of a compiled artifact as os package install sends it, so the protocol handshake runs on it - #28603
Conversation
…d artifact as os package install sends it The door flattened a compiled artifact only when the body had no top-level id or name. The CLI lifts id and version beside the nested manifest, so engines and dependencies stayed one level down and the ADR-0087 handshake and the ADR-0141 dependency check never read them. Detect the artifact by its nested manifest object alone, through flattenPackageBody. Claude-Session: https://claude.ai/code/session_01FEkQ5mLNytcuq6kvRwXHMq Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cd941396609e2930e2e2792b5359279f6b484866 && git checkout cd941396609e2930e2e2792b5359279f6b484866
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 8454de72440b7bef7a411e4899b8d126f3f5f09c && git checkout -B drift-repro ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 && git merge --no-ff 8454de72440b7bef7a411e4899b8d126f3f5f09c
node scripts/docs-audit/affected-docs.mjs --json ce001f97abbe460d9a6cc9a4bde6c6e38c9d37f8 |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Face: narrowing — Ruling read: card #28535, triage 6554172575 ("measure with the CLI binary first, then fix whichever side owns the shape"; no second handshake, the 422 ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentNarrowing face, no Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28535
Clause-②: no (narrowing) —
POST /api/v1/marketplace/install-localrefuses, with the 422OS_PROTOCOL_INCOMPATIBLE(orDEPENDENCY_MISSING) a flat manifest already got, a compiled artifact sent withidandversionlifted beside its nestedmanifest(the bodyos package install ./dist/objectstack.jsonsends) whoseengines.protocolexcludes this runtime or whosedependenciesthis runtime does not satisfy. That body installed before. Nothing is widened.Metadata-protocol: none
Skills-docs: none — the pages already state the behaviour this restores:
skills/objectstack-platform/SKILL.md("The runtime checks it before it loads anything and refuses an out-of-range app at the boundary") andcontent/docs/protocol/kernel/plugin-spec.mdx("the pre-install dependency check every install door runs"). No page described the CLI path as an exception.BREAKING accept-set narrowing at
POST /api/v1/marketplace/install-local(@objectstack/cloud-connection), reached byos package install ./dist/objectstack.json.维护者速读
POST /api/v1/marketplace/install-local)现在按「body 里是否有嵌套的manifest对象」识别编译产物并展平,不再要求顶层没有id/name。os package install ./dist/objectstack.json发来的产物因此会被展平,协议握手(engines.protocol)和依赖检查(dependencies)都读得到真实值。CLI 不改。enginesnested undermanifest.manifest, so install-local logs "declares no engines.protocol range" and loads an artifact built for^18without the ADR-0087 protocol check #28535(分诊 6554172575):CLI 把id、version提到顶层,正好绕过了服务端的展平条件,engines留在下一层,服务端只记一条「未声明 engines.protocol」警告就装上了。用真实 CLI 二进制实测:^17、^19的产物都装成功、退出码 0。POST /packages完全一致;^18和不声明engines的产物照旧安装(^18不再出现那条误报警告)。目录(catalog)路径的已发布产物本来就没有顶层id,行为不变。回滚即 revert。packages/cloud-connection/src/marketplace-install-local-plugin.ts(normalizeBundle改为调用已有的flattenPackageBody);packages/cloud-connection/src/marketplace-install-local-protocol-handshake.test.ts(新[#28535]用例组);.changeset/28535-install-local-nested-artifact-manifest.md。pnpm --filter @objectstack/cloud-connection exec vitest run src/marketplace-install-local-protocol-handshake.test.ts src/marketplace-install-local-dependency-missing.test.ts→ 28/28 通过。What changes
Which side owns the shape, and why the server. The measurement (below) separates the two candidates. On
origin/main, the bare compiled artifact{ manifest: { …, engines }, objects }posted to install-local is already flattened by the door and refused 422 (therefuses the bare compiled artifactcase passes on base). Only the body the CLI builds escapes:install.tsspreads the artifact and liftsidandversionto the top, and the door'snormalizeBundleflattened only when the body had NO top-levelidandname. So the intended contract is the door's (ADR-0130 D4: a compiled artifact nests its manifest undermanifest:; the door already flattens it on the inline and catalog branches, andflattenPackageBodyalready flattens it for the unmanaged import). The defect is that the door's detection depended on which sender built the body. Fixing the door closes it for every CLI already published, which all send the lifted shape; a CLI-only fix would leave every shipped CLI bypassing the handshake. ⛔ No second handshake: the oneassertProtocolCompat(packages/metadata-core/src/protocol-handshake.ts) at step 1c is unchanged; it now receives the flat manifest.The door (
packages/cloud-connection/src/marketplace-install-local-plugin.ts).normalizeBundlebecomesisPlainObject(m) ? flattenPackageBody(m) : m: a body with a nested plain-objectmanifestis a compiled artifact, flattened{ ...meta, ...sections }(sections win, the orderflattenPackageBodyrecords). The old guard (!m.id && !m.name && (m.manifest.id || m.manifest.name)) is gone. One flatten now serves the inline branch, the catalog branch and the unmanaged import. For the CLI body the lifted top-levelid/versionequal the nested ones (artifact.manifest.id ?? artifact.id), so the flattened identity does not change.What else stayed nested, and now is read. Reading every top-level manifest key
handleInstallreads:engines(handshake, step 1c) anddependencies(ADR-0141 D2 check, step 1c') were both one level down for the CLI body.installModesandnamespacewere already read throughdeclaredPackageManifest, which reads the nestedmanifestitself, so they were right before and stay right.packages(multi-package artifacts) is a collection beside the manifest and was already top level.namenow reaches the registered package for the CLI body (it was nested before).Not changed:
packages/cli/src/commands/package/install.ts(the lift is now harmless against this runtime; dropping it would also let a new CLI get the check from an older runtime, but that is a second side and outside the ruling's "fix the side that owns the contract"). PR #28571'shandleUninstallhunks and open PR #28554's hunks are untouched (re-read at start and beforepr_create; #28554's nearest hunk is 30 lines fromflattenPackageBody, which this PR does not edit).Tests
marketplace-install-local-protocol-handshake.test.ts, new group[#28535] a compiled artifact as os package install sends it. The body is built exactly asinstall.tsbuilds it (cliInstallBody). Cases:^17refused 422 with the five diagnostic fields and nothing registered, synced or ledgered; the CLI body answers byte-for-byte what the flat manifest answers; the bare compiled artifact is refused the same way (control, green on base too);^18installs with NO[protocol]warning, registered and ledgered flat (enginestop level, nomanifestkey); noenginesinstalls with exactly one warning, as before.marketplace-install-local-dependency-missing.test.ts, new group: the CLI body with an absent dependency is refused 422DEPENDENCY_MISSINGand nothing is written; the lit control installs once the dependency is installed in range.Red on base (plugin source swapped to
origin/maina2e75562, mutation proved on disk:flattenPackageBody(m)count 0, old guard count 1; restored withgit checkout HEAD --,git diff HEAD0 bytes, blob hash equal to HEAD): 4 failed / 24 passed of 28. Failing exactly:refuses ^17,answers exactly what the flat manifest answers,lit control: ^18 installs with no protocol warning(the false warning), and the dependency refusal. The bare-artifact and no-range controls pass on both trees. Green on8454de72: 28/28. Full package:pnpm --filter @objectstack/cloud-connection test63 files / 779 tests passed;typecheckandbuildexit 0. This revert is also the ablation: the tests import the plugin by relative path (source, no dist), and reverting the one changed expression reds the^17,^18-no-warning and dependency cases, which is the whole behaviour change.Acceptance notes
enginesnested. Measured with a one-off probe (not kept) in this package's harness: a ledger entry in that shape declaring^17is re-registered atkernel:readywith no error and no[protocol]line (registered = [..., "com.example.qaold"],errors = 0,warns = []), because the rehydrate'scheckProtocolCompat(entry.manifest)reads the entry as stored. Not changed here: the claim's surface is the install read, and refusing an already-installed package at boot is its own narrowing for deployments that hold such entries. Reported to the seat as a finding. Reinstalling the package through the door now judges it.declaredPackageManifestandflattenPackageBodyat the unmanaged-import and install-mode steps now always receive a flat body on both branches, so they act as identity there. Left as they are: they still state where each shape keeps its keys.Verification
Real CLI binary, before and after.
packages/cli/bin/run.js(builtdist) runningos package install ./dist/objectstack.json --runtime … --email … --password …against a realos start --artifact host.json(development posture,OS_CLOUD_URL=off). Four artifacts, each{ manifest: { id, namespace, version, type, name, engines? }, objects: [one object] }. Script:pm-orders/server/evidence/28535/measure.mjsin the project files.Base (
origin/maina2e75562):Head (
8454de72, cloud-connection rebuilt):Gates on
8454de72.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 60 families for the 4 changed paths; each ran with its exit code captured before any pipe, and--rananswers✓ dispatch-gates --ran: 60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED. All 60 exit 0. Two first ran into prerequisites and were re-run, not counted green on the first reading:check:dual-build-cjs-loads(exit 3, no fulldist/; after the full packages build:115 published require entry point(s) across 70 package(s) load) andcheck:type-check-debt(exit 3, lock queue-timeout behind this PR's own dogfood run; re-run under the lock: exit 0). Record:pm-orders/server/evidence/28535/gates-ran-8454de72.txt.eslint packages/cloud-connection --no-inline-config: exit 0.Dogfood battery (owed:
(narrowing)arm) on8454de72, under the lock,pnpm --filter @objectstack/dogfood testafter the^...closure build:VERDICT command-exit 0, Test Files 364 passed / 3 skipped (367), Tests 2934 passed / 28 skipped (2962).CI: the families CI runs are read from this PR's check-runs; not re-run locally.
Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5mLNytcuq6kvRwXHMq