feat(runtime): publish minEditorVersion at /api/capabilities (DOPE-448) - #163
Conversation
Until now nothing let a runtime state what it needs from an editor: the upload endpoint accepts any ZIP that passes analyze_zip, and the bundle carries no editor identity at all. So "this runtime must not accept programs from an older editor" was not expressible. The new endpoint declares it. Unauthenticated, like /api/version — an editor calls it before login, because an editor too old to log in must still be able to find out why. The runtime only ADVERTISES this value; the editor compares it and refuses to upload. That is deliberate. Nothing on the upload path enforces it, so shipping a runtime release can never lock out an editor already installed in the field, and there is one place to debug when a push is refused. It is not a security control and should not be described as one. MIN_EDITOR_VERSION starts at 4.1.0 because that is where the STruC++ pipeline landed — 4.0.x editors emitted MatIEC artefacts this runtime cannot build at all. It is not a build counter: raising it for a release that merely changed something locks out working editors for no reason. Editors predating the endpoint get a 401 from the /<command> catch-all (not a 404) and fall back to /api/version, seeing no floor — exactly their previous behaviour. Verified against a real pre-change container. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
JulioSergioFS
left a comment
There was a problem hiding this comment.
PR Review — openplc-runtime #163
Verdict: Approve with minor comments
Small, well-scoped, well-tested, and unusually well-documented change. It adds one
unauthenticated read endpoint and a single constant. The design (advertise, don't
enforce) is sound and clearly justified. Nothing here blocks merge. The comments
below are one documentation inaccuracy and a couple of consistency nits.
What it does
- Adds
GET /api/capabilities→{ "runtimeVersion", "minEditorVersion" },
unauthenticated (same tier as/api/version). - Adds
MIN_EDITOR_VERSION = "4.1.0"inwebserver/version.pywith a strong
comment on when to raise it. - Documents the endpoint in
EDITOR_INTEGRATION.mdand adds
tests/pytest/restapi/test_capabilities.py(7 tests).
The runtime only advertises the floor; the editor compares and refuses to
upload. This is explicitly not an enforcement/security control, which is the
right call: a runtime release can never lock out a field-installed editor.
Strengths
- Design rationale is explicit and correct. "Advertise, not enforce",
"unauthenticated because an editor too old to log in must still learn why",
and the raise-it-only-when-a-bundle-would-mis-compile rule are all documented
at the point of definition. This is exactly where that reasoning belongs. - Tests pin the contract, not the implementation: reachable without a token
even once users exist,/capabilitiesand/versionnever disagree on the
version, payload shape, theX-OpenPLC-Runtime-Versionheader covers the new
route, andminEditorVersionparses as barex.y.z. - Swagger block is present and complete for the new route.
Findings
1. (Minor — doc accuracy) Docs say old runtimes return 404; the PR body says 401
EDITOR_INTEGRATION.md and the restapi_capabilities docstring both state that a
peer predating this endpoint returns 404 and the editor falls back to
/api/version. But the PR description says the opposite, with a test note:
"An editor predating this endpoint gets 401 from the
/<command>catch-all
(@jwt_required()), not a 404 … Verified against a real pre-change container."
So the shipped documentation contradicts the author's own verified behaviour. If
an old runtime routes /api/capabilities into the JWT-guarded /<command>
catch-all, it's 401, not 404. Please reconcile — either correct the docs to
say 401 (or "401/404 depending on the old runtime's routing"), and make sure the
editor's fallback treats both as "no capabilities endpoint here". A doc that
says 404 could lead someone to write a fallback that only triggers on 404 and
silently breaks against real old runtimes.
(Wording nit in the same sentence: "Editors that predate this endpoint" — the
endpoint lives in the runtime, so the subject should be "runtimes/peers that
predate this endpoint".)
2. (Minor — cross-component consistency) runtimeVersion and minEditorVersion have different formats in one payload
The body example is { "runtimeVersion": "v4.2.0", "minEditorVersion": "4.1.0" }
— runtimeVersion carries the v tag prefix (it's the GitHub release tag),
minEditorVersion is bare x.y.z (and the test enforces bare). Two version
fields in the same object with different lexical shapes is a foot-gun for the
consumer: whoever compares minRuntimeVersion (packages, bare) against this
runtimeVersion (tagged) must strip the v first. Worth either normalising
runtimeVersion to bare here, or documenting the asymmetry explicitly in the
Swagger schema so the editor/packages side can't assume a uniform format.
3. (Nit) No test on the exact JSON key names
test_capabilities_reports_runtime_version_and_editor_floor asserts the full
dict, which is good, but it compares against the constants — a rename of the JSON
keys (runtimeVersion/minEditorVersion) that the editor relies on would still
pass as long as both sides use the constants. Since these keys are a
cross-repo contract, consider one literal-key assertion (assert "minEditorVersion" in body) so a key rename can't slip through green.
Questions
- Is
/api/capabilitiesreachable at the same auth tier under every deploy
path (behind the reverse proxy / TLS), the same as/api/version? The tests
use the Flask test client; the "unauthenticated in production" guarantee is
worth a one-line confirmation.
Test assessment
Good. 7 focused behavioural tests, 39 total passing in tests/pytest/restapi.
Coverage matches the risks that matter for this endpoint (token-independence,
version agreement, header hook, parseable floor). The only gap is the literal
key-name contract (Finding 3).
Runtime half of DOPE-448. Companion: Autonomy-Logic/openplc-editor#993 (plus openplc-packages and openplc-web).
What this adds
GET /api/capabilities— unauthenticated, like/api/version:{ "runtimeVersion": "v4.2.0", "minEditorVersion": "4.1.0" }Until now nothing let a runtime state what it needs from an editor:
handle_upload_fileaccepts any ZIP that passesanalyze_zip, and the bundle carries no editor identity at all. So "this runtime must not accept programs from an older editor" — one of the explicit requirements on the card — was not expressible.The runtime advertises; it does not enforce
The editor compares
minEditorVersionagainst its own version and refuses to upload. Nothing on the upload path enforces it. That is deliberate:This is not a security control and should not be described as one. A client that skips the check can still upload. If enforcement is ever wanted, the additive follow-up is for the editor to send its version on upload and the runtime to compare.
Why unauthenticated
Same reason as
/api/versionand/api/get-users-info: an editor too old to log in must still be able to find out why. Information needed to decide how to authenticate cannot sit behind authentication.It exposes nothing new —
RUNTIME_VERSIONalready leaves this process three ways: theX-OpenPLC-Runtime-Versionheader on every response,/api/version, and the discovery broadcast.MIN_EDITOR_VERSION = "4.1.0"The floor is 4.1.0 because that is where the STruC++ pipeline landed; 4.0.x editors emitted MatIEC artefacts this runtime cannot build at all. So it locks out nobody who works today.
The comment on the constant states the rule for raising it: only when an older editor genuinely produces a bundle this runtime would mis-compile. It is not a build counter — bumping it for a release that merely "changed something" locks out working editors for no reason.
Older editors
An editor predating this endpoint gets 401 from the
/<command>catch-all (@jwt_required()), not a 404, and falls back to/api/version— exactly its previous behaviour. Verified against a real pre-change container during testing; the editor side has a regression test for both the 401 and 404 shapes.Tests
tests/pytest/restapi/test_capabilities.py— 7 new, 39 total intests/pytest/restapipassing. Pins that the endpoint stays reachable without a token even once users exist, that it never disagrees with/api/versionabout the runtime version, and that the published floor stays a barex.y.z(a tag-stylev4.1.0or a partial4.1would make the editor's comparison ambiguous).Note for the reviewer
docs/EDITOR_INTEGRATION.mdgains a "Version and compatibility" section and the JWT-exemption list is corrected — it omitted/api/version, which was already public.Shell scripts in this repo have no
.gitattributesprotection, socore.autocrlf=truegives them CRLF on checkout anddocker builddies with./install.sh: not found(exit 127 — a\rin the shebang). Hit during testing. Not fixed here; worth its own card, as it affects every Windows contributor.🤖 Generated with Claude Code