Skip to content

fix(message-parser): gate the functional route on the bound schema - #174

Merged
facontidavide merged 1 commit into
mainfrom
fix/parser-functional-bound-schema
Aug 9, 2026
Merged

fix(message-parser): gate the functional route on the bound schema#174
facontidavide merged 1 commit into
mainfrom
fix/parser-functional-bound-schema

Conversation

@facontidavide

Copy link
Copy Markdown
Contributor

Closes #171.

Problem

trampoline_get_plugin_extension advertised pj.parser_functional.v1 whenever the instance had any registered SchemaHandler:

if (sv == PJ_PARSER_FUNCTIONAL_EXTENSION_V1 && !self->handlers_.empty()) {

But parseScalars/parseObject are final and dispatch on the bound schema, erroring with "parser does not register schema: <bound>" when that specific schema has no handler. So a mixed-model plugin — handlers for some schemas, legacy parse() for the rest, which is exactly the shape parse()'s own doc-comment sanctions ("a fallback for type names not in the registered table (e.g. a ROS-style generic flattener)") — claimed the functional route on every schema. An SDK 0.21 host that prefers that route then fails every message on the unhandled topics.

This isn't hypothetical: PJ4's streaming_caching_parser_plugin test fixture had this shape and went zero-rows the moment the host started preferring the functional route; it had to have its handler removed to keep working.

Fix

Once a schema is bound, advertisement requires a handler for that schema. Before binding, any registered handler still advertises the capability — no schema-specific answer exists yet, and hosts are already required to re-query after binding rather than cache an earlier absence (the contract HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching pins).

Header-inline behavior change only: no ABI, vtable, protocol, struct_size, or member-layout change; abi/baseline.abi untouched.

Tests

Two new cases, both observed failing before the fix with Actual: true (i.e. they reproduce the bug), passing after:

  • MixedModelParserAdvertisesOnlyForHandledSchemas — a plugin with a handler for example/Image plus legacy parse(): bound to the handled schema it advertises; bound to example/Unhandled it does not, and parseScalarsFunctional reports the extension as unavailable rather than the host discovering it via a per-message parse error.
  • RebindingToAnUnhandledSchemaWithdrawsTheFunctionalRoute — advertisement follows a re-bind in both directions.

Pre-existing contracts verified unchanged: NewlyBuiltParserExposesStableExtensionAutomatically (pre-bind capability advertisement) and HandlerRegisteredDuringBindEnablesExtensionWithoutCapabilityCaching (handler registered during bindSchema) both still pass. Full suite green (64/64).

Release

Per the versioning policy this is a PATCH-level fix to installed-header behavior; I propose 0.21.1 when a release is next cut. Following the precedent of #169 (same class of fix) I have not bumped VERSION in this PR, and have not tagged anything. Note that open PRs #172/#173 currently claim 0.22.0/0.23.0.

Downstream

No PJ4 change is required — its parser host already re-queries supportsFunctionalParsing() per call on both routes, so it simply stops taking the functional route for unhandled schemas. This should land before the official plugin fleet rebuilds on 0.21.

🤖 Generated with Claude Code

trampoline_get_plugin_extension advertised pj.parser_functional.v1
whenever the instance had ANY registered SchemaHandler. A mixed-model
plugin — handlers for some schemas, legacy parse() for the rest, the
shape parse()'s own doc-comment sanctions for a generic flattener —
therefore claimed the functional route on every schema, and the final
parseScalars/parseObject dispatchers then rejected each message for the
unhandled ones ("parser does not register schema"). A 0.21 host that
prefers the functional route fails every message on those topics.

Once a schema is bound, advertisement now requires a handler for THAT
schema. Before binding, any registered handler still advertises the
capability: no schema-specific answer exists yet, and hosts are already
required to re-query after binding rather than cache an earlier absence.
Header-inline behavior fix; no ABI, vtable, protocol, or member-layout
change.

Closes #171

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit 8dd36c5 into main Aug 9, 2026
4 checks passed
@facontidavide
facontidavide deleted the fix/parser-functional-bound-schema branch August 9, 2026 13:51
facontidavide added a commit that referenced this pull request Aug 9, 2026
…0.22.0)

SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272) — the complete wasmer-free surface, with the dual-target module ABI
frozen here:

- pj.parser_route_claims.v1 extension: exact handler-table route claims
  (scalar/object, exact-only match), auto-implemented by
  MessageParserPluginBase from its handler table; delivery via
  get_plugin_extension, zero layout changes
- pj.parser_functional.v2: object sink gains accept_object_spliced (one
  splice per object, input-space offsets, frozen per-type eligibility table
  in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical
- parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space
  tokens, token-0 creation-error channel, 512-byte error buffer) +
  bounds-checked little-endian codecs for BindingInfo / parse-input /
  output-descriptor blocks
- host claim catalog + route resolver: §4 admission matrix (priority bounds,
  wildcard/object rules, encoding registry, duplicate identity, provenance
  never from manifests), module-manifest ingestion, synthesized plugin claim
  ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection
  (pin fail-closed → exact > wildcard → provenance tier → priority →
  identity tie-break) with split per-route probe caches and selection traces
- native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export
  resolution, session never-unload) + module runtime over the codecs with
  splice eligibility/bounds validation and the fault-vs-data-error strike
  tracker (3 strikes → quarantine → recreate; repeat → session disable)
- authoring kit pj_base/include/pj_base/parser_module/: header-only,
  C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable);
  CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds),
  ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire
  ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser +
  PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target
- wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps
  it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same
  toy module, binary-format audit of export names/signatures, reactor model,
  and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired
  into linux CI

Docs and the in-repo authoring skill are synchronized to this surface: a new
parser-module authoring reference, the parser-module choice rule and route-claim
semantics in the MessageParser guidance, and corrections to stale claims (wrong
plugin-base include path, two-builder ObjectWriter, builtin-type inventory).
Editing VERSION now re-runs configure, so a stale build tree can no longer stamp
a previous version into the generated version header.

Rebased onto main: the schema-aware functional-route gate from #174 now governs
the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it
only implements through legacy parse() withdraws both revisions (a host prefers
v2, so leaving v2 advertised would route every message on those topics into a
parser that can only reject them).

Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout
sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a
kit-authored module E2E (load → admit → bind → full + spliced PointCloud).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
facontidavide added a commit that referenced this pull request Aug 9, 2026
…0.22.0) (#172)

SDK core of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272) — the complete wasmer-free surface, with the dual-target module ABI
frozen here:

- pj.parser_route_claims.v1 extension: exact handler-table route claims
  (scalar/object, exact-only match), auto-implemented by
  MessageParserPluginBase from its handler table; delivery via
  get_plugin_extension, zero layout changes
- pj.parser_functional.v2: object sink gains accept_object_spliced (one
  splice per object, input-space offsets, frozen per-type eligibility table
  in builtin_object_abi.h); frozen error-kind constants; v1 byte-identical
- parser_module_abi.h: frozen pj_module_* export ABI (u64 module-space
  tokens, token-0 creation-error channel, 512-byte error buffer) +
  bounds-checked little-endian codecs for BindingInfo / parse-input /
  output-descriptor blocks
- host claim catalog + route resolver: §4 admission matrix (priority bounds,
  wildcard/object rules, encoding registry, duplicate identity, provenance
  never from manifests), module-manifest ingestion, synthesized plugin claim
  ids (wildcard:<encoding>, handler:<encoding>:<type>), §5 selection
  (pin fail-closed → exact > wildcard → provenance tier → priority →
  identity tie-break) with split per-route probe caches and selection traces
- native module loader (dlopen RTLD_LOCAL|RTLD_NOW, per-handle export
  resolution, session never-unload) + module runtime over the codecs with
  splice eligibility/bounds validation and the fault-vs-data-error strike
  tracker (3 strikes → quarantine → recreate; repeat → session disable)
- authoring kit pj_base/include/pj_base/parser_module/: header-only,
  C++17, wasi-clean (own Status/Expected/arena; -fno-exceptions capable);
  CdrReader + CdrFieldLocator (XCDR1 traversal plans, depth caps, bounds),
  ProtoReader + ProtoFieldLocator, checked time normalization, canonical-wire
  ObjectWriter (PointCloud/Image + splice path), pj::FunctionalParser +
  PJ_FUNCTIONAL_PARSER macro, pj_add_parser_module() native target
- wasm manifest custom-section codec (shared embed/read; 1b's tooling wraps
  it) + static wasm ABI conformance: wasi-sdk 27 reactor build of the same
  toy module, binary-format audit of export names/signatures, reactor model,
  and single manifest section — no wasmer, gated on PJ_WASI_SDK_ROOT, wired
  into linux CI

Docs and the in-repo authoring skill are synchronized to this surface: a new
parser-module authoring reference, the parser-module choice rule and route-claim
semantics in the MessageParser guidance, and corrections to stale claims (wrong
plugin-base include path, two-builder ObjectWriter, builtin-type inventory).
Editing VERSION now re-runs configure, so a stale build tree can no longer stamp
a previous version into the generated version header.

Rebased onto main: the schema-aware functional-route gate from #174 now governs
the v2 advertisement as well as v1, so a mixed-model parser bound to a schema it
only implements through legacy parse() withdraws both revisions (a host prefers
v2, so leaving v2 advertised would route every message on those topics into a
parser that can only reject them).

Tests: 75/75 Debug+ASAN (74 with the wasm gate skipped), incl. layout
sentinels, golden byte fixtures, adversarial loader/runtime fixtures, and a
kit-authored module E2E (load → admit → bind → full + spliced PointCloud).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
facontidavide added a commit that referenced this pull request Aug 30, 2026
…get (0.24.0)

SDK PR 1b of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272), following the core PR (#172, 0.22.0): a second loader for the
already-frozen module ABI — wasmer execution, hardening, and the wasm
authoring target. Ships as its own 0.24.0 release (0.23.0 was taken by the
symbol-provenance fix, #175), so an SDK with the wasm loader is
distinguishable from one without it. Rebased onto main after #174#176: the
budget-aware NativeParserModule::load keeps #175's defining-object symbol
provenance, and the docs no longer describe wasm as unavailable.

- wasmer 7.0.1 statically linked (pinned; required only by the plugin_host
  component — plugin_sdk consumers stay wasmer-free). Exit-criterion prototype
  findings encoded in the loader contract: the pinned static lib exports no
  wasm_module_share/obtain symbols (engine-owned module reuse: one compilation,
  store-per-bound-instance with isolated state) and no creator-thread affinity
  exists — calls are sequential-only, host-serialized. Pin re-evaluated against
  7.2.1: no C-API gains, WASI-syscall CVEs unreachable under the empty import
  allow-list, and 7.2 drops x86_64-darwin (rationale in ARCHITECTURE.md)
- wasm loader: validation before any instantiation — manifest custom section
  via the shared codec (exactly one), reactor model enforced (_initialize
  required, start section/_start rejected), operational export set verified
  by name AND signature through the shared pj_base wasm inspector, and a
  frozen EMPTY import allow-list (a parser module may import nothing)
- execution runtime: metered store-per-instance calls (the pinned lib exports
  the wasmer_metering_* C API but no interrupt/epoch/deadline surface, so
  limits are enforceable instruction metering — fresh point allowance per
  guest call, exhaustion = distinct contract violation), linear-memory base
  re-acquired at every point of use with overflow-safe bounds, splices
  resolved against the original host payload, shared fault taxonomy + strike
  tracker with quarantine replay
- memory caps at validation: artifacts must declare a linear-memory maximum
  (default cap 256 MiB); the engine enforces it at runtime. Aggregate session
  budgets (modules, artifact size, claims, active instances, declared memory)
  gate admission with DECLINE and mutate nothing on rejection
- adversarial fixtures: unreachable trap, metered infinite loop, memory-growth
  bomb, admission limits, quarantine replay — plus the M1 rejection matrix
- pj-wasm-embed-manifest installed CLI (embed/verify) wrapping the shared
  section codec; pj_add_parser_module(... TARGETS native wasm) builds both
  artifacts from one source with post-link audit, dogfooded on the toy module
- fix: release the metering middleware on the adapter-failure path
- VERSION 0.24.0, CHANGELOG entry, CI wasmer job (metering-symbol check)

Tests: 80/80 Debug+ASAN with both toolchain roots; graceful skip verified for
each root independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_017U391nLF7Motf4FehVRiXz
facontidavide added a commit that referenced this pull request Aug 30, 2026
…get (0.24.0)

SDK PR 1b of the parser-extensibility v4 architecture (spec: pj-official-plugins
PR #272), following the core PR (#172, 0.22.0): a second loader for the
already-frozen module ABI — wasmer execution, hardening, and the wasm
authoring target. Ships as its own 0.24.0 release (0.23.0 was taken by the
symbol-provenance fix, #175), so an SDK with the wasm loader is
distinguishable from one without it. Rebased onto main after #174#176: the
budget-aware NativeParserModule::load keeps #175's defining-object symbol
provenance, and the docs no longer describe wasm as unavailable.

- wasmer 7.0.1 statically linked (pinned; required only by the plugin_host
  component — plugin_sdk consumers stay wasmer-free). Exit-criterion prototype
  findings encoded in the loader contract: the pinned static lib exports no
  wasm_module_share/obtain symbols (engine-owned module reuse: one compilation,
  store-per-bound-instance with isolated state) and no creator-thread affinity
  exists — calls are sequential-only, host-serialized. Pin re-evaluated against
  7.2.1: no C-API gains, WASI-syscall CVEs unreachable under the empty import
  allow-list, and 7.2 drops x86_64-darwin (rationale in ARCHITECTURE.md)
- wasm loader: validation before any instantiation — manifest custom section
  via the shared codec (exactly one), reactor model enforced (_initialize
  required, start section/_start rejected), operational export set verified
  by name AND signature through the shared pj_base wasm inspector, and a
  frozen EMPTY import allow-list (a parser module may import nothing)
- execution runtime: metered store-per-instance calls (the pinned lib exports
  the wasmer_metering_* C API but no interrupt/epoch/deadline surface, so
  limits are enforceable instruction metering — fresh point allowance per
  guest call, exhaustion = distinct contract violation), linear-memory base
  re-acquired at every point of use with overflow-safe bounds, splices
  resolved against the original host payload, shared fault taxonomy + strike
  tracker with quarantine replay
- memory caps at validation: artifacts must declare a linear-memory maximum
  (default cap 256 MiB); the engine enforces it at runtime. Aggregate session
  budgets (modules, artifact size, claims, active instances, declared memory)
  gate admission with DECLINE and mutate nothing on rejection
- adversarial fixtures: unreachable trap, metered infinite loop, memory-growth
  bomb, admission limits, quarantine replay — plus the M1 rejection matrix
- pj-wasm-embed-manifest installed CLI (embed/verify) wrapping the shared
  section codec; pj_add_parser_module(... TARGETS native wasm) builds both
  artifacts from one source with post-link audit, dogfooded on the toy module
- fix: release the metering middleware on the adapter-failure path
- VERSION 0.24.0, CHANGELOG entry, CI wasmer job (metering-symbol check)

Tests: 80/80 Debug+ASAN with both toolchain roots; graceful skip verified for
each root independently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_017U391nLF7Motf4FehVRiXz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

parser_functional: gate extension advertisement on the bound schema, not on any registered handler

1 participant