Skip to content

feat(protocol): enforce the engines.protocol handshake (ADR-0087 P0) - #2650

Merged
os-zhuang merged 2 commits into
mainfrom
claude/bold-clarke-o4op29
Jul 6, 2026
Merged

feat(protocol): enforce the engines.protocol handshake (ADR-0087 P0)#2650
os-zhuang merged 2 commits into
mainfrom
claude/bold-clarke-o4op29

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Implements P0 of the ADR-0087 epic (#2643) — resolves#2644. First real code change of the metadata-protocol upgrade contract.

What & why

PluginEnginesSchema.protocol (packages/spec/src/kernel/manifest.zod.ts, ADR-0025 §3.2, protocol-first per §3.10 #3) was declared, documented, and checked by no loader or installer — an ADR-0078 "declarable-but-inert" violation, and the root cause of "a version mismatch crashes the app": a package built against an incompatible protocol major failed deep in a schema .parse() or renderer contract instead of at the boundary.

This turns that into a structured, machine-actionable load-time refusal.

Changes

  • @objectstack/spec — exports PROTOCOL_VERSION / PROTOCOL_MAJOR from /kernel, the single source of truth the handshake checks against. A drift test (protocol-version.test.ts) asserts it against package.json so the protocol major and the published package major can never diverge.
  • @objectstack/metadata-core — the pure logic core:
    • checkProtocolCompat(manifest, runtimeVersion?) — major-grained range check supporting ^, ~, >=/</>/<=, compound (>=11 <13), hyphen, and wildcard forms. Returns a discriminated result (ok / no-range / unparsed-range / incompatible).
    • assertProtocolCompat() + ProtocolIncompatibleError (code: OS_PROTOCOL_INCOMPATIBLE) carrying both versions and the exact objectstack migrate meta --from N command (wired end-to-end in P2).
    • Refuses only on a positive mismatch: absent ranges are grandfathered (warn), unrecognized ranges never cause a false rejection.
  • @objectstack/metadata-protocolinstallPackage runs the handshake before the registry write, so an incompatible package is refused with a diagnostic instead of crashing later.

Design notes (from ADR-0087 D1)

  • The diagnostic is identical in shape one major behind or five — it names the migrate command, not a guide the consumer "should have read" (timeliness is never load-bearing).
  • migrate meta --from N (P2) does not exist yet; P0's value is the diagnosable refusal, and the diagnostic already carries the command string for when P2 lands.
  • Deferred to follow-ups (tracked in ADR-0087 P0: enforce the protocol handshake (make engines.protocol real) #2644): the boot-time load-path handshake (AppPlugin.start() + durable rehydration), the objectstack lint nudge for a missing range, and scaffold stamping (create-objectstack / defineStack templates) — the ratchet that closes grandfathering. This PR delivers the enforcement core + the primary install seam.

Verification

  • @objectstack/spec — full suite green (incl. the drift test); check:api-surface shows 2 added, 0 breaking, snapshot regenerated per ADR-0059 §4.
  • @objectstack/metadata-core — 99 tests green (exhaustive range-logic coverage incl. the <13.0.0 boundary and the bare->N npm-desugaring case).
  • @objectstack/metadata-protocol — 15 tests green: new install-seam tests prove reject-before-registry-write + structured diagnostic + compatible-installs + grandfathering; pre-existing durable-package tests still pass.
  • DTS builds clean for all three packages; changeset added (minor, fixed group).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjDghc79qFSJkJt2zvBxtU


Generated by Claude Code

Turn a protocol/consumer version mismatch from an arbitrary downstream
crash into a structured, machine-actionable load-time refusal, and pay
down a standing ADR-0078 violation: engines.protocol (ADR-0025 §3.2,
protocol-first per §3.10 #3) was declared, documented, and checked by no
loader or installer.
- spec: export PROTOCOL_VERSION / PROTOCOL_MAJOR from /kernel — the single
source of truth the handshake checks against; a drift test keeps it in
lockstep with the package major.
- metadata-core: checkProtocolCompat() (pure, major-grained range check
supporting ^/~/>=/</ranges/wildcards), assertProtocolCompat(), and the
structured ProtocolIncompatibleError (OS_PROTOCOL_INCOMPATIBLE, carrying
both versions and the 'migrate meta --from N' command). Refuses only on a
positive mismatch; absent ranges are grandfathered (warn), unrecognized
ranges never cause a false rejection.
- metadata-protocol: installPackage runs the handshake before the registry
write — incompatible packages are refused with a diagnostic, not a crash.
Additive and backward compatible; api-surface snapshot regenerated (2 added,
0 breaking). Part of #2643; resolves#2644.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjDghc79qFSJkJt2zvBxtU
@vercel

vercelBot commented Jul 5, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 5, 2026 5:04pm

Request Review

@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling and removed size/l labels Jul 5, 2026
@github-actions

github-actionsBot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-core, @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via packages/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/profiles.mdx(via @objectstack/spec)
  • content/docs/permissions/roles.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)

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.

Comment threadpackages/metadata-core/src/protocol-handshake.ts Fixed
Comment threadpackages/metadata-core/src/protocol-handshake.ts Fixed
…838)
The comparator match (`^(<=|>=|<|>)\s*(.+)$`) and the hyphen-range match
(`^(.+?)\s+-\s+(.+)$`) had whitespace/any-char overlap — polynomial-ReDoS
shapes on the externally-authored engines string.
- comparator: peel the operator by fixed prefix + trim, no backtracking match
- hyphen range: split on the whitespace-delimited hyphen (fixed anchor), not a
lazy (.+?)…(.+) match
- bound the range string to 128 chars up front (a real SemVer range is short;
overlong input is unrecognized = admit-with-warning, never a slow scan)
Behavior unchanged (100 tests green); adds a ReDoS-regression test asserting
pathological inputs resolve to null in well under 50ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjDghc79qFSJkJt2zvBxtU
@os-zhuang
os-zhuang marked this pull request as ready for review July 6, 2026 00:28
@os-zhuang
os-zhuang merged commit 60dc3ba into mainJul 6, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/bold-clarke-o4op29 branch July 6, 2026 00:28
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0087 P0: enforce the protocol handshake (make engines.protocol real)

3 participants

@os-zhuang@github-advanced-security@claude