Derive the OpenAPI document from the code instead of declaring it alongside - #591
Open
bburda wants to merge 18 commits into
Open
Derive the OpenAPI document from the code instead of declaring it alongside#591bburda wants to merge 18 commits into
bburda wants to merge 18 commits into
Conversation
bburda
force-pushed
the
feat/openapi-derivation
branch
from
August 5, 2026 07:55
5e5b36e to
9429c5c
Compare
bburda
marked this pull request as ready for review
August 24, 2026 09:29
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes the served OpenAPI 3.1 document a derived artifact of the gateway’s typed route registry and handler signatures, reducing drift between documentation and real runtime behavior. It also adds contract-style tests and a few targeted runtime fixes uncovered while validating the document against live gateways.
Changes:
- Derive per-route success statuses, declared errors, headers, and auth
securityrequirements from typed handler return types and route registrations (including plugin route descriptions where available). - Normalize/centralize URI construction (notably
Locationon 201/202 responses) and tighten several request/response correctness edges found during live-spec probing. - Add/expand unit + integration + contract tests that mechanically enforce OpenAPI/document consistency (status surface, schema reachability, error coverage, RBAC contract, etc.).
Reviewed changes
Copilot reviewed 152 out of 172 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/ros2_medkit_serialization/include/ros2_medkit_serialization/type_cache.hpp | Explicitly deletes move ops; documents lifetime. |
| src/ros2_medkit_plugins/ros2_medkit_graph_provider/README.md | Clarifies plugin-only links and docs contribution. |
| src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt | Enables compile_commands.json export for tooling. |
| src/ros2_medkit_msgs/README.md | Updates fault_code contract documentation. |
| src/ros2_medkit_log_bridge/src/log_bridge_node.cpp | Clamping/parameter parsing tweaks; signature polish. |
| src/ros2_medkit_log_bridge/include/ros2_medkit_log_bridge/log_bridge_node.hpp | Adjusts cooldown_allows signature to const-ref. |
| src/ros2_medkit_integration_tests/test/scenarios/test_scenario_discovery_manifest.test.py | Pins Area capability advertising of /components. |
| src/ros2_medkit_integration_tests/test/scenarios/test_scenario_bulk_data_download.test.py | Asserts download media type is explicitly declared. |
| src/ros2_medkit_integration_tests/test/features/test_updates.test.py | Adds malformed-body vs gate precedence + 501 declaration checks. |
| src/ros2_medkit_integration_tests/test/features/test_scripts_api.test.py | Pins collection-correct URLs and trailing-slash Location behavior. |
| src/ros2_medkit_integration_tests/test/features/test_openapi_response_drift.test.py | Adds write-route status drift checks (201/202 + empty body). |
| src/ros2_medkit_integration_tests/test/features/test_locking.test.py | Verifies 201 Location resolves (incl trailing slash). |
| src/ros2_medkit_integration_tests/test/features/test_health.test.py | Ensures endpoint list has no duplicates; refines schema checks. |
| src/ros2_medkit_integration_tests/test/features/test_graph_provider_plugin.test.py | Pins plugin-emitted top-level x-medkit-graph link. |
| src/ros2_medkit_integration_tests/test/features/test_docs_endpoint.test.py | Ensures nested-entity sub-docs describe nothing (empty paths). |
| src/ros2_medkit_integration_tests/test/features/test_configuration_api.test.py | Adds config_id bound enforcement + unconvertible value is 400. |
| src/ros2_medkit_integration_tests/test/features/test_auth.test.py | Exercises per-operation security filtering with plugin route. |
| src/ros2_medkit_integration_tests/ros2_medkit_test_utils/launch_helpers.py | Adds reusable gateway parameter presets (plugin/full-feature). |
| src/ros2_medkit_integration_tests/ros2_medkit_test_utils/gateway_test_case.py | Adds assert_declared_media_type helper. |
| src/ros2_medkit_integration_tests/CMakeLists.txt | Raises timeout for heavy auth-policy contract test. |
| src/ros2_medkit_gateway/test/test_update_manager.cpp | Adds lifetime tests for shutdown draining async tasks. |
| src/ros2_medkit_gateway/test/test_trigger_manager.cpp | Pins SSE trigger-event frame schema shape. |
| src/ros2_medkit_gateway/test/test_sse_transport_provider.cpp | Pins vendor-error sentinel pairing and success/error frame shapes. |
| src/ros2_medkit_gateway/test/test_script_handlers.cpp | Updates tests for Created/Accepted wrappers and exact Locations. |
| src/ros2_medkit_gateway/test/test_schema_builder.cpp | Clarifies schema rationale for execution capability field. |
| src/ros2_medkit_gateway/test/test_route_descriptions.cpp | Adds tests for operation identity + security semantics. |
| src/ros2_medkit_gateway/test/test_operation_handlers.cpp | Updates for Accepted<> wrapper and Location behavior. |
| src/ros2_medkit_gateway/test/test_openapi_spec_builder.cpp | Verifies middleware-owned responses are always present. |
| src/ros2_medkit_gateway/test/test_lock_manager.cpp | Pins extend/release status surface (no 409; includes 403). |
| src/ros2_medkit_gateway/test/test_lifecycle_handlers.cpp | Updates to Accepted<> wrapper; removes runtime status override. |
| src/ros2_medkit_gateway/test/test_fault_manager.cpp | Pins typed fault failure classification from transport layer. |
| src/ros2_medkit_gateway/test/test_fault_handlers.cpp | Pins 404-vs-503 split based on typed failure, not message text. |
| src/ros2_medkit_gateway/test/test_dto_contract.cpp | Adds vendor_code schema/wire contract; improves schema shape checks. |
| src/ros2_medkit_gateway/test/test_docs_handlers.cpp | Converts /docs root to typed text response; byte-equality checks. |
| src/ros2_medkit_gateway/test/test_discovery_models.cpp | Fixes capability key naming to match served routes. |
| src/ros2_medkit_gateway/test/test_discovery_handlers.cpp | Ensures graph link omitted when plugin not loaded. |
| src/ros2_medkit_gateway/src/ros2/transports/ros2_parameter_transport.cpp | Maps JSON conversion throws to INVALID_VALUE (400), not 500. |
| src/ros2_medkit_gateway/src/ros2/transports/ros2_fault_service_transport.cpp | Sets FaultFailure on no-response paths. |
| src/ros2_medkit_gateway/src/openapi/schema_builder.hpp | Removes OpenAPI 3.0-style binary schema helper. |
| src/ros2_medkit_gateway/src/openapi/schema_builder.cpp | Removes format: binary schema emission. |
| src/ros2_medkit_gateway/src/openapi/openapi_spec_builder.hpp | Allows schemes without global document-level requirement. |
| src/ros2_medkit_gateway/src/http/handlers/update_handlers.cpp | Migrates to Created/Accepted wrappers + Location helper. |
| src/ros2_medkit_gateway/src/http/handlers/trigger_handlers.cpp | Migrates POST to Created<> + Location normalization. |
| src/ros2_medkit_gateway/src/http/handlers/sse_transport_provider.cpp | Adds vendor_code to vendor-error SSE frames. |
| src/ros2_medkit_gateway/src/http/handlers/lock_handlers.cpp | Migrates POST to Created<> + canonicalized child Location. |
| src/ros2_medkit_gateway/src/http/handlers/lifecycle_handlers.cpp | Uses api_path + Accepted<> for transitions. |
| src/ros2_medkit_gateway/src/http/handlers/health_handlers.cpp | Includes plugin-mounted endpoints in root endpoint list. |
| src/ros2_medkit_gateway/src/http/handlers/fault_handlers.cpp | Replaces substring error classification with typed failure flag. |
| src/ros2_medkit_gateway/src/http/handlers/docs_handlers.cpp | Serves cached pre-serialized JSON text for docs. |
| src/ros2_medkit_gateway/src/http/handlers/cyclic_subscription_handlers.cpp | Migrates POST to Created<> + child Location. |
| src/ros2_medkit_gateway/src/http/handlers/config_handlers.cpp | Enforces config_id length bound on DELETE. |
| src/ros2_medkit_gateway/src/http/handlers/bulkdata_handlers.cpp | Adds finite+wildcard download media type set; Created<> upload. |
| src/ros2_medkit_gateway/src/gateway_node.cpp | Drains UpdateManager before notifier shutdown to avoid UAF. |
| src/ros2_medkit_gateway/src/entity_freeze_frame_capture.cpp | Minor callback signature tightening. |
| src/ros2_medkit_gateway/src/core/openapi/path_resolver.cpp | Documents known live gaps in <entity-path>/docs resolution. |
| src/ros2_medkit_gateway/src/core/models/entity_types.cpp | Adds resource-collection string mappings (data-groups, triggers, etc.). |
| src/ros2_medkit_gateway/src/core/managers/update_manager.cpp | Makes shutdown explicit/idempotent; locks stop/start race window. |
| src/ros2_medkit_gateway/src/core/http/parameter_error_classification.cpp | Derives possible statuses from enum; adds COUNT sentinel guard. |
| src/ros2_medkit_gateway/src/core/http/handlers/capability_builder.cpp | Types capability array as DTOs; fixes naming to served routes. |
| src/ros2_medkit_gateway/src/core/http/detail/primitives.cpp | Adds writer for already-serialized JSON text. |
| src/ros2_medkit_gateway/src/core/auth/auth_manager.cpp | Moves RBAC table to runtime-populated permission set. |
| src/ros2_medkit_gateway/README.md | Updates docs route semantics + graph provider endpoint details. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/typed_router.hpp | Clarifies TypedRequest::path() semantics (prefix included). |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/handler_support.hpp | Adds optional status recorder instrumentation for tests. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/fault_handlers.hpp | Declares classify_fault_failure API + rationale. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handlers/cyclic_subscription_handlers.hpp | Documents Created<> contract for POST. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/handler_result.hpp | Introduces Created/Accepted wrappers + with_location. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/detail/primitives.hpp | Adds write_json_text API + clarifies legacy raw routes. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/http/alternate_status.hpp | Adds wrapper alternate-status + payload unwrap helpers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/x_medkit.hpp | Types embedded capabilities as EntityCapability list. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/updates.hpp | Improves schema fidelity for SOVD update DTOs. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/scripts.hpp | Removes unreachable schema; adds ScriptExecutionRequest DTO. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/operations.hpp | Adds field descriptions; improves opaque result schema docs. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/logs.hpp | Clarifies semantics; adds constraints/docs for fields. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/locks.hpp | Adds detailed field docs + minimum constraint. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/health.hpp | Documents timestamp semantics (wall-clock, non-monotonic). |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/enums.hpp | Adds fault-trigger operator/severity vocab tables. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/entity_capability.hpp | New typed DTO for capability list items. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/data.hpp | Removes unreachable schema; models plugin-vs-core list via anyOf. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/cyclic_subscriptions.hpp | Adds docs + minimum constraints for create/update. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/contract.hpp | Adds FieldConstraints for schema-level bounds. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/dto/auth.hpp | Adds OAuth2Error DTO for RFC6749 error bodies. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/openapi/document_checks.hpp | Adds schema reachability/ref-walk utilities. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/models/entity_types.hpp | Expands/clarifies ResourceCollection contract + comments. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/models/entity_capabilities.hpp | Clarifies served-matrix intent vs spec-permitted superset. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/managers/update_manager.hpp | Documents notifier lifetime + adds shutdown() API. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/rest_server.hpp | Adds reporting hook for route metadata completeness issues. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/parameter_error_classification.hpp | Exposes derived status surface for parameter routes. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/http_utils.hpp | Adds canonical path + child-resource Location helpers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/update_handlers.hpp | Updates signatures to Created/Accepted wrappers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/trigger_handlers.hpp | Updates signatures/docs for Created<> POST. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/script_handlers.hpp | Updates signatures for Created/Accepted wrappers. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/operation_handlers.hpp | Makes 202 Accepted explicit in return type. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/lock_handlers.hpp | Updates POST signature/docs for Created<> + Location. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/lifecycle_handlers.hpp | Updates transition signature to Accepted<>. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/docs_handlers.hpp | Makes /docs root typed; keeps scoped docs raw. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/capability_builder.hpp | Types capability builder output; documents enumerator contract. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/handlers/bulkdata_handlers.hpp | Adds download_media_types() API; Created<> upload signature. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/http/error_codes.hpp | Adds x-medkit-resource-sample-failed code constant. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/faults/fault_types.hpp | Adds typed FaultFailure + defaults for safe classification. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/models/component.hpp | Fixes relationship key to /hosts. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/discovery/models/area.hpp | Fixes relationship key to /components. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/configuration/parameter_types.hpp | Adds COUNT sentinel for enum completeness enforcement. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/auth/auth_manager.hpp | Adds permission merge API + exposes auth policy accessor. |
| src/ros2_medkit_gateway/include/ros2_medkit_gateway/core/auth/auth_config.hpp | Defines residual permissions + RoutePermissions type. |
| src/ros2_medkit_gateway/design/lifecycle.rst | Updates docs for Accepted<>-declared 202. |
| src/ros2_medkit_gateway/design/index.rst | Adds OpenAPI derivation doc + teardown contract note. |
| src/ros2_medkit_gateway/design/aggregation.rst | Clarifies fan-out header handling + known global-faults loop risk. |
| src/ros2_medkit_gateway/config/gateway_params.yaml | Documents updates backend gating as 501, not unregistered routes. |
| src/ros2_medkit_gateway/CMakeLists.txt | Adds status-recorder define (test-only) + new linter/contract tests. |
| src/ros2_medkit_fault_manager/test/test_rosbag_capture.cpp | Adds NAME_MAX budget tests; reserves vector capacity. |
| src/ros2_medkit_fault_manager/src/sqlite_fault_storage.cpp | Deletes move ops for stmt wrapper; documents intent. |
| src/ros2_medkit_fault_manager/src/fault_manager_node.cpp | Raises max fault_code length to 256; documents rationale. |
| src/ros2_medkit_fault_manager/src/fault_audit_log.cpp | Deletes move ops for stmt wrapper; documents intent. |
| src/ros2_medkit_fault_manager/include/ros2_medkit_fault_manager/rosbag_capture.hpp | Adds bounded+digest bag directory naming contract. |
| docs/tutorials/snapshots.rst | Documents bag directory name truncation + digest behavior. |
| docs/tutorials/plugin-system.rst | Documents describe_plugin_routes for OpenAPI inclusion. |
| docs/tutorials/graph-provider.rst | Clarifies plugin-conditional graph link behavior. |
| docs/tutorials/authentication.rst | Updates RBAC explanation + per-deployment doc behavior. |
| docs/config/server.rst | Updates updates.enabled semantics (501 gating, always registered). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
bburda
force-pushed
the
feat/openapi-derivation
branch
4 times, most recently
from
August 25, 2026 12:36
07cf1d7 to
61ce60f
Compare
bburda
marked this pull request as draft
August 25, 2026 14:56
bburda
force-pushed
the
feat/openapi-derivation
branch
2 times, most recently
from
August 26, 2026 16:23
3d7aafc to
9b68f5b
Compare
bburda
marked this pull request as ready for review
August 26, 2026 16:36
bburda
force-pushed
the
feat/openapi-derivation
branch
from
August 27, 2026 08:05
9b68f5b to
48e5b42
Compare
Document-wide invariants no single handler owns: unique operation identity, declared tags, resolvable refs, no malformed path keys. A violation here is a client-visible defect even when every endpoint behaves correctly.
…turn type 45 operations declared a 2xx they cannot return, because the status was attached by hand at the registration while the handler's return type said otherwise. Created<T> and Accepted<T> carry the status in the type, so the registry reads it from the signature and the document cannot drift from the wire. status_payload_t unwraps to the schema, the serializer and the static assertions. A 202 labelled "No content" is now unrepresentable rather than merely corrected.
28 routes could answer 501 and none of them said so, so a client met a refusal the document never mentioned. gated_on() ties the guard and the declaration to one expression: a route that can be gated off publishes its 501, and a gate with an empty predicate now fails closed instead of segfaulting.
Twelve sites attached a header no operation declared. response_header() records it where it is set; declare_derived_response static_asserts that a 201 or 202 carries its Location, so publishing the header without sending it stops compiling. validate_completeness() runs at startup and is asserted by the suite, so a missing declaration is reported rather than silently dropped.
…non-JSON route returns lock_guarded() marks the writes that answer 409 to a client without the lock, and the expected set is pinned so dropping a marker turns the suite red. Binary downloads stop emitting format: binary, which is not valid in OpenAPI 3.1, and the schema-less exemption now requires a non-JSON media type. The 416 cpp-httplib answers before routing is declared where it is reachable.
A created resource is answered with its Location, a fault-trigger route that cannot serve says 501 rather than a bare error, and a parameter conversion failure answers 400 instead of a status that told the caller nothing.
bburda
force-pushed
the
feat/openapi-derivation
branch
3 times, most recently
from
August 27, 2026 17:02
0440c7e to
c2b5e65
Compare
get_tracked_goal was scoped to no entity, so an execution started on one app was reachable from another, and list_executions resolved only two of the four entity types. Both now resolve the entity that owns the execution. Kept as its own commit: this changes behaviour, and reverting it must not take the documentation work with it.
…keep Location resolvable Script handlers guessed the collection from the request path instead of reading the segment the router matched. Separately, to_regex_path appends "/?$", so a request with a trailing slash routes successfully and req.path() + "/" + id then yields a double slash whose Location answers 404. Nine sites built a URI that way; canonical_request_path and child_resource_path are now the single place that knows about the anchor.
15 schemas were unreachable from any operation and 9 operations published a body with no content at all. Responses, request bodies, error bodies, SSE frames and the update payloads SOVD already defines are now bound to the types the handlers use, with success_schema() narrowing only the declared schema where a handler must keep returning free-form JSON so peer vendor keys survive. Two wire defects surfaced doing it: the vendor-error sentinel was emitted without its vendor_code on one path, so a client could not identify the error; and the error-code table claimed completeness while its check matched any mention anywhere in the guide. The check now reads the table itself, and the emitter scan covers headers and in-tree plugins.
…a client must send The document advertised entity resource collections the gateway does not serve and omitted several it does; the correspondence is now exact in both directions for all five types. FieldConstraints publishes the bounds the handlers enforce, every operation carries a description, and non-trivial request bodies carry an example. A published bound is only added where every route carrying it rejects an over-long value unconditionally - DELETE on a configuration did not, and now does.
…osing bags silently The document published a maximum of 256 while FaultManagerNode enforced 128, so a code in between produced a server error on a value the document calls valid. The bound is now 256 on both sides; every fault_code column is unconstrained TEXT and every IDL field an unbounded string, so nothing downstream caps it. Raising it exposed silent loss: fault_<code>_<timestamp> pushed the rosbag directory name past NAME_MAX, the exception was caught and only logged, and the bag was never written. The name is bounded and carries a digest of the whole code, so two codes sharing every kept byte still get separate directories. Separately, get_fault decided 404 against 503 by substring-matching the store's message, so a validation refusal from the fault manager reached the client as a server error. The transport now records whether it got a response at all, and the client-error side is the default so a 503 must be asked for.
…ion table, project the sub-documents The /docs routes and the graph provider's routes were served but undocumented. Folding them in needed OperationDesc to carry tag, operationId and role, and a gateway-stamped marker for the coverage sweep, whose recorder attaches at the registry mount point and structurally cannot see a plugin route. The RBAC table is now generated from the route registrations rather than maintained as a literal, with a short residual list for what is mounted outside the registry. Enforcement did not move: check_authorization still fails closed where it always did, because docs, Swagger UI and plugin routes would otherwise lose their rule. The <entity-path>/docs sub-documents are projected from the real document instead of rebuilt by hand. The hand-written producers had been shipping dangling $refs - twelve in a component's document - so a client generating code from one got references to nothing. The cache now stores the serialized form rather than a parsed tree, cutting resident growth by about 40% and the per-hit copy by 70%. Also documents the derivation rule and the tier each mechanism reaches, and adds a check resolving every test the design docs cite against the test tree.
…implicit Pre-existing clang-tidy findings in packages this work does not otherwise touch. The gate is mandatory, and "pre-existing" is not a reason to leave it red.
A whole-branch pass found what reviewing one slice at a time cannot: three surfaces sit outside RouteRegistry::to_openapi_paths() and each published something the gateway does not honour. Per-operation security and the 401/403 declarations were keyed on auth.enabled while enforcement asks the policy, so under require_auth_for: write the document told a client to hold a token for 136 GETs that admit everyone, and under none it asserted an RBAC posture the gateway does not have. Both now key on the policy. Plugin-served operations omitted the 401/403 the middleware does emit on them - the same pre-routing argument the code already made for 416. With locking off, 44 operations still advertised locks while the same gateway's root said locking: false. GET /api/v1 and /docs disagreed in both directions, and nothing compared them. A hand-written array's completeness is now a compiler check rather than a claim the design docs made for it. The cache-derived items in the sub-documents published a request body the handler rejects and a 200 body the gateway never returns; they now inherit both from the templated sibling that names the same route, and are discarded rather than published raw when that sibling cannot be found.
…inted The package set no CMAKE_EXPORT_COMPILE_COMMANDS, so it produced no compile_commands.json and clang-tidy run standalone could not resolve the gateway headers graph_provider_plugin_exports.cpp includes - reporting them as missing on a file that compiles clean. Nine other packages already set it; this one was the gap, and it only surfaced now because this work touches that file.
UpdateManager runs prepare/execute on their own std::async threads and each calls ResourceChangeNotifier::notify() once the backend returns, so a task can still be inside the backend when the gateway tears down. ~UpdateManager is what joins those tasks, but update_mgr_ is declared before resource_change_notifier_ and C++ destroys members in reverse declaration order, so the notifier was freed first. A task waking up afterwards wrote to freed memory - ASan reported heap-use-after-free on the fetch_add that opens notify(), from run_prepare() on a std::async thread. The counter is the function's own lifetime guard, so it cannot help here: it only protects a call already in progress, not one that starts after the object is gone. Give UpdateManager an explicit shutdown() that stops accepting work and joins every in-flight task, and call it from ~GatewayNode before the notifier is shut down. Draining at a defined point makes the ordering independent of member declaration order. The destructor delegates to shutdown() so standalone use keeps the previous join behaviour, and moving the futures out makes repeat calls a no-op. The stopped_ check in the three start_*() entry points moves under mutex_. Previously it was read before the lock, leaving a window where a call could pass the check and then launch after shutdown() had already snapshotted the futures, escaping the drain. Covered by test_openapi_response_drift under ASan, which is what first drove this path, plus unit tests pinning that shutdown() blocks while a task is in flight and is safe to call twice.
A service and an action may share a short name and a ROS path: an action puts its own services under `<path>/_action/...`, so `<path>` stays free. Neither form of the item half separates them, so both collapse to one id, and the document wrote a path item for it twice - the second replacing the first, under an id `refuse_if_ambiguous` answers 400 for. operation_item_ids() now owns the rule and reports how many operations each id names, so the collection and the document cannot drift apart. An id that names more than one operation gets no concrete path item; the templated route still describes the request.
`dto_fields` describes members one at a time, so "at least one of data or value" could not come from the descriptor: both were optional and an empty object validated against a body handle_write_configuration answers 400 for. A generated client would compile that call. The properties still come from the descriptor; only the cross-field rule is added, so a member renamed there cannot leave the rule stale without changing the key it names.
bburda
force-pushed
the
feat/openapi-derivation
branch
from
August 27, 2026 18:40
c2b5e65 to
70a9244
Compare
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.
Pull Request
Summary
The generated OpenAPI document asserted a great deal the gateway does not honour, and omitted a great deal it does. The common cause was that facts about a route were declared separately from the handler implementing them, so the two drifted with nothing checking.
This derives what can be derived, and where it cannot be derived, declares it once at a seam that also does the work.
The governing rule, now stated in
design/openapi_derivation.rst: if a fact about a route can be derived from the handler, it must not be declared separately; where it cannot, the declaration lives at a seam that also does the work.What now derives itself
Created<T>,Accepted<T>,status_payload_t) rather than a hand-attached number.check_authorizationstill fails closed where it always did, because docs, Swagger UI and plugin routes are registered outside the registry and would lose their rule.<entity-path>/docssub-documents are projected from the real document instead of rebuilt by hand.securityis published only where the deployment actually checks it, keyed on the auth policy rather than onauth.enabled.Measured against a running gateway
format: binary(invalid in OpenAPI 3.1)$refin/docssub-documentsVerification probed all 238 documented operations against live gateways in five configurations - feature gates on and off, auth off and all three
require_auth_forvalues, plugin loaded and not - and found no status the gateway emits that the document does not declare.Defects found and fixed along the way
These were not documentation problems:
503, because the classification matched a substring of the error message. Replaced with a two-state outcome set at the transport's no-response paths, defaulting to the client-error side so a server error must be asked for.fault_codewas published with a maximum of 256 while the fault manager enforced 128; values in between produced a server error. Bounds reconciled.NAME_MAX, the exception was caught and only logged, and the bag was never written. The name is now bounded and carries a digest so truncated codes stay distinguishable.401/403/429answered before the request body was read stranded the payload and poisoned the keep-alive connection, so the next request on it received a spurious400.DELETE /{entity}/configurations/{config_id}did not enforce the length bound its siblings enforce and the document published./docssub-documents published a request body the handler rejects, and a 200 body the gateway never returns.{}, which the handler answers 400 for. The rule spans two members, so the descriptor could not carry it; it is now stated once next to them.Enforcement, so this does not come back
$refs, no phantom 2xx, every advertised collection served, every operation described, no role published where none is enforced.Issue
Type
Testing
Gateway unit suite and the integration suite, plus contract suites added here:
test_openapi_contract,test_openapi_callability,test_openapi_error_coverage,test_openapi_response_drift,test_rbac_contract,test_auth_policy_contract,test_locking_disabled_contract.Reviewers can reproduce the central claim directly:
Re-verified after rebasing onto current
main(25 commits had landed since thiswork started): gateway builds with zero warnings, 2678 unit tests and
137 contract cases pass, no orphaned processes left behind. The one conflict
was the per-target coverage list, which
mainreplaced with a directory-scopemodule that instruments every target automatically.
Two tests are red and are not caused by this branch:
test_external_app_fault_rollupfails roughly two runs in three on an idle machine. Diagnosed here:RosbagCapture::start()runs from theFaultManagerNodeconstructor while the node's graph cache is still empty, so nothing is buffered for the first 500 ms and a fault confirmed in that window gets no bag. Written up on [BUG] A fault confirmed right after a post-fault recording window gets no rosbag #574, which is a different entry path into the same sink; left unfixed by owner decision.test_opcua_securedlaunches the fault manager with no parameters, so storage defaults to/var/lib/ros2_medkit, which the test runner cannot create;create_storage()rethrows and the node aborts before its service appears. One launch parameter fixes it; out of scope here.Known limitations
Stated rather than left to be discovered:
<entity-path>/docscarry no payload schema on any gateway: all fourTopicDataconstruction sites push an empty type, so the schema-generating branch never runs in production. The items still carry the SOVD extensions and the correct write envelope./docsis silently unreachable - route registration order puts the docs catch-all first, with no warning at registration./docssub-document cache bounds entries rather than bytes; caching the serialized form rather than the parsed tree cut resident growth by about 40% and the per-hit copy by about 70%, but the entry bound is still the wrong unit.<entity-path>/docsanswers 404 for four collections that are served and advertised (locks,scripts,fault-triggers,status), and 200 with an empty document for two nested paths that are not served.A tooling gap this surfaced
The plugin packages do not set
CMAKE_EXPORT_COMPILE_COMMANDS, so they produceno
compile_commands.jsonand the pre-push clang-tidy hook cannot resolve thegateway headers they include - reporting them as missing on files that compile
clean. Nine other packages set it. It bites whoever next touches a file in
ros2_medkit_graph_providerorros2_medkit_graph_watchdog; both are outsidethis change's subject, so it is reported rather than fixed here.
Checklist