Skip to content

feat(cloud-connection,metadata-core): an install the protocol handshake admitted unchecked says so in its answer - #30409

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-fqxfr0
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-fqxfr0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

维护者速读

  • 改了什么:从市场或文件安装一个没声明平台协议版本(engines.protocol)的应用时,安装成功的应答里多了 protocolCheckSkipped:说明没做兼容性检查、按哪个协议版本装的,并带一句给管理员看的话;声明了版本范围、检查通过的包,应答和以前一字不差。
  • 为什么改:兼容性检查被跳过时,只有服务器日志里有一行 warn,点"安装"的管理员看不到;这是卡 A package with no protocol range installs with the compatibility check skipped, and only the server log says so #30326 的 server 半,页面上怎么展示是之后的 ui 卡。
  • 风险与回滚:只在成功应答里加一个可选键,不改任何拒绝路径,日志行照旧;assertProtocolCompat 从无返回值改为返回它本来就算出的判决,旧调用方不受影响。回滚即撤本 PR,无数据迁移。
  • 先看:packages/cloud-connection/src/marketplace-install-local-plugin.ts 的 protocolCheckSkippedOf 和步骤 1c 里接住判决的那几行;packages/metadata-core/src/protocol-handshake.ts 的 assertProtocolCompat 返回值。
  • 两分钟验证:pnpm --filter @objectstack/cloud-connection exec vitest run src/marketplace-install-local-protocol-handshake.test.ts -t 30326,预期 5 条全绿;在 main 的代码上其中 4 条是红的(对照那条本来就绿)。
  • 需要你做的:无,席位落地。

Part of #30326

Clause-②: yes (widening) — the three success answers of POST /api/v1/marketplace/install-local gain the optional data.protocolCheckSkipped; @objectstack/metadata-core's assertProtocolCompat returns ProtocolCompatAdmitted (new exported type) where it returned void, and the no-range / unparsed-range members of the exported ProtocolCompatResult gain protocolVersion. Nothing accepted or answered is taken away.
Metadata-protocol: none — an install answer and a runtime helper's return value, not anything a metadata author writes
Skills-docs: none — git grep -n -i "install-local\|declares no engines\|without a compatibility check\|protocolCheck\|engines.protocol" -- skills (evals excluded) hits four lines: objectstack-platform/SKILL.md:118 (a declared range is checked and an out-of-range app refused, still true), objectstack-platform/references/operations.md:106 and :111 (what os package install and the plugin are), objectstack-upgrade/SKILL.md:87 (where the declared range is read). None describes what an install answers for a package with no range.

What changes

The card: "The install answer carries this, and the page shows it in plain words". The triage (6652014530) puts the server half in marketplace-install-local-plugin.ts; the UI half is a later domain:ui card, so #30326 stays open after this PR (the seat releases it at landing).

The handshake hands its verdict back. assertProtocolCompat (ADR-0087 D1) already computed a verdict and threw on incompatible; for ok, no-range and unparsed-range it returned nothing, so a door that answers a caller could only log. It now returns the verdict checkProtocolCompat gave (ProtocolCompatAdmitted = every member but incompatible), and the no-range / unparsed-range members carry protocolVersion, the version they were judged against (the incompatible member and its diagnostic already did). No new judgement: the range is read once, by the handshake.

The install answer carries the skipped check. Step 1c of handleInstall keeps the verdict, and protocolCheckSkippedOf turns a no-range or unparsed-range verdict into

protocolCheckSkipped: { reason, packageId, protocolVersion, message }   // + requiredRange, rangeSource for unparsed-range

reason is the handshake's own status value, the stable machine code; packageId is the id the door's id gate parsed; protocolVersion is the protocol the package was installed under unchecked; message says it in plain words for the person who pressed Install. It rides all three success answers: managed, unmanaged (installUnmanaged) and a paired customization package (installCustomizationPair), passed down the way withheldMaps is. It is omitted, not nulled, when the range was checked, so that answer's key set is unchanged. The warn line [MarketplaceInstallLocal] [protocol] package '…' declares no engines.protocol range; … is unchanged.

Why the code is in reason, not code. A code: literal in the answer would be read by check:dispatcher-error-vocabulary as an ADR-0112 error code and would need a row in the spec ledger. This is a notice on a 200, not an error code (the D6c family check:error-code-casing describes), and the handshake's own status value is the code the ruling asks for.

Contract stop: measured, not reached

The order: stop if carrying the notice needs a packages/spec change. Measured on fdad1786 and again on 870ebce3: no schema in packages/spec/src describes the install-local success answer. The spec's withheldMaps (PackageExportManifestSchema) is the export manifest's, not the answer's; git grep -n "hotLoaded\|upgradedFrom\|credentialNote" -- packages/spec/src (three keys only this answer carries) hits nothing. The answer is a cloud-connection-internal REST shape, so the field is added there.

Tests

  • marketplace-install-local-protocol-handshake.test.ts, new top-level describe('[#30326] …'): an inline manifest with no range answers protocolCheckSkipped { reason: 'no-range', packageId, protocolVersion } with the plain-words message, is registered, and logs exactly one [protocol] warn line; the compiled artifact os package install FILE sends answers the same; a range the handshake cannot read (workspace:*) answers unparsed-range with requiredRange and rangeSource; an unmanaged install of the same package carries it; control: under ^CURRENT_MAJOR the answer has no protocolCheckSkipped key. The harness's mount gained an optional protocol importUnmanagedPackage stub for the unmanaged case.
  • marketplace-install-local-customization-pair-install.test.ts, new top-level describe: a pair export with no range answers no-range; with ^CURRENT_MAJOR no key.
  • protocol-handshake.test.ts, new top-level describe: assertProtocolCompat returns the no-range and unparsed-range verdicts with protocolVersion, and the ok verdict equal to checkProtocolCompat's.
  • Red first, on fdad1786 with the fix reverted and the pins in: the 5 notice cases red (200 with no protocolCheckSkipped), the 2 controls green. With the fix: 43/43 in the two files.
  • marketplace-install-local-jobs.test.ts: its fixture now declares engines.protocol: ^CURRENT_MAJOR, as a scaffolded app does. Its jobs-free case pins the answer's exact key set, and the fixture declared no range, so with this change the set gained protocolCheckSkipped; with a declared range the pin keeps meaning "the jobs gate adds nothing".
Suites and typecheck on 4d926a7

os-verify-lock VERDICT command-exit 0: @objectstack/metadata-core typecheck exit 0, @objectstack/cloud-connection typecheck exit 0; @objectstack/metadata-core test Test Files 23/23, Tests 911/911; @objectstack/cloud-connection test Test Files 70/70, Tests 877/877.

Ablation

One leg, through scripts/ablation-replace.mjs on 4d926a74: deleting protocolCheckSkipped = protocolCheckSkippedOf(admitted, manifestId); (anchor 1 → 0, blob 8e7fdb92 → 5f6c1432) turns the 5 notice cases red, 38 green; restored to blob == HEAD 8e7fdb92, git diff HEAD empty. The cloud-connection suite resolves @objectstack/metadata-core to src/ (vitest alias), and the plugin is imported from src/, so no dist/ leg applies.

Acceptance notes

  • The other install door, POST /api/v1/packages (packages/runtime/src/domains/packages.ts, assertProtocolCompat(manifest)), still answers success with no notice for a package with no range. It is outside this order's surface; with assertProtocolCompat now returning the verdict, the same spread there is a small follow-up. carrier: seat 5.
  • os package install prints the answer's note; whether the CLI also prints protocolCheckSkipped is the CLI's call. carrier: seat 5.
  • Whether publishing should warn or refuse such a package is out of scope (the card says so).

Verification


Generated by Claude Code

…ke admitted unchecked says so in its answer

POST /api/v1/marketplace/install-local answers data.protocolCheckSkipped
({ reason, packageId, protocolVersion, message }, plus requiredRange and
rangeSource for unparsed-range) when the ADR-0087 handshake admitted the
package without checking it, on the managed, unmanaged and paired
customization success answers; omitted when the range was checked. The
reason is the handshake's own verdict: assertProtocolCompat now returns the
verdict it admitted the package under, and the no-range / unparsed-range
verdicts carry the protocolVersion they were judged against. The warn log
line stays.

Claude-Session: https://claude.ai/code/session_01Utp6CZ35sjTKgyrvdkytCz
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/metadata-core, touching 14 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via protocolVersion (symbol, a field of type ProtocolCheckSkipped; a field of type ProtocolCompatResult))
  • content/docs/concepts/metadata-lifecycle.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/deployment/app-dependencies.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/deployment/validating-metadata.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/frontend/plugins/plugin-chatbot.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/kernel/contracts/metadata-service.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/permissions/authorization.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/permissions/capabilities.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/permissions/permission-sets.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/protocol/kernel/plugin-spec.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/ui/doc-pages.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v12.mdx (via assertProtocolCompat (symbol, a top-level function), checkProtocolCompat (symbol, a top-level function))
  • content/docs/releases/v17/17-5.mdx (via protocolVersion (symbol, a field of type ProtocolCheckSkipped; a field of type ProtocolCompatResult))
  • content/docs/releases/v18/18-0.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))
  • content/docs/releases/v18/18-3.mdx (via packageId (symbol, a field of type ProtocolCheckSkipped))

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.

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 779fd05b4856fb96e11734090c7af8cdd63d2cd1 — the merge of head 6c1d2ef227fc2a495b50947229606a7184c2ddae into base fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 779fd05b4856fb96e11734090c7af8cdd63d2cd1 && git checkout 779fd05b4856fb96e11734090c7af8cdd63d2cd1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a 6c1d2ef227fc2a495b50947229606a7184c2ddae && git checkout -B drift-repro fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a && git merge --no-ff 6c1d2ef227fc2a495b50947229606a7184c2ddae

node scripts/docs-audit/affected-docs.mjs --json fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs fe835b49aadbe5ef51dd1b7d4a4922b4c9b97d8a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb

席位核对 · ACCEPT · 2026-11-05T10:08Z · head 6c1d2ef2

Reviewed against order server-seat5/dispatch-30326.md (Claim 6652933868), server half only.

  • One judgement. assertProtocolCompat returns the verdict it admitted (ProtocolCompatAdmitted, every status but incompatible); no-range / unparsed-range carry protocolVersion. The install door maps that verdict to protocolCheckSkipped (protocolCheckSkippedOf) and never re-reads the range. The warn line is unchanged.
  • Every success answer. Managed, unmanaged and the paired-customization answer spread the optional data.protocolCheckSkipped (reason, packageId, protocolVersion, message; requiredRange / rangeSource for unparsed-range), omitted for a checked range, so those answers' key sets are unchanged. The contract stop did not fire: no install answer has a packages/spec schema (dev measurement, confirmed on the file list).
  • Pins. Five red→green (no-range on each answer, unparsed-range, a satisfied-range control) with one ablation; the jobs fixture gains engines.protocol so it stays a checked install.
  • Declarations. Clause-②: yes (widening), Metadata-protocol: none, patch on both packages, Skills-docs: none. Part of #30326: the ui half (the install dialog's wording) remains. The body sentence that read as a closing keyword to the PR-body guard was reworded by the seat (guard green on re-run).
  • Not covered, recorded: the other install door, POST /api/v1/packages (packages/runtime/src/domains/packages.ts), still answers no notice — the seat files it as its own card.
  • Gates. check-governed-merges: NOT governed, 256 lines; the remaining CI runs gate the queue.

Landing: ready + auto-merge. At merge the seat releases #30326 to the queue for its ui half.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant