Skip to content

feat(runtime): publish minEditorVersion at /api/capabilities (DOPE-448) - #163

Merged
marconetsf merged 1 commit into
developmentfrom
feature/DOPE-448-version-compatibility
Aug 7, 2026
Merged

feat(runtime): publish minEditorVersion at /api/capabilities (DOPE-448)#163
marconetsf merged 1 commit into
developmentfrom
feature/DOPE-448-version-compatibility

Conversation

@marconetsf

@marconetsf marconetsf commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

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_file 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" — one of the explicit requirements on the card — was not expressible.

The runtime advertises; it does not enforce

The editor compares minEditorVersion against its own version and refuses to upload. Nothing on the upload path enforces it. That is deliberate:

  • shipping a runtime release can never lock out an editor already installed in the field;
  • there is exactly one place to debug when a push is refused, with both version strings in hand.

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/version and /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_VERSION already leaves this process three ways: the X-OpenPLC-Runtime-Version header 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 in tests/pytest/restapi passing. Pins that the endpoint stays reachable without a token even once users exist, that it never disagrees with /api/version about the runtime version, and that the published floor stays a bare x.y.z (a tag-style v4.1.0 or a partial 4.1 would make the editor's comparison ambiguous).

Note for the reviewer

docs/EDITOR_INTEGRATION.md gains 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 .gitattributes protection, so core.autocrlf=true gives them CRLF on checkout and docker build dies with ./install.sh: not found (exit 127 — a \r in the shebang). Hit during testing. Not fixed here; worth its own card, as it affects every Windows contributor.

🤖 Generated with Claude Code

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>
@marconetsf
marconetsf marked this pull request as ready for review August 6, 2026 09:43
@JulioSergioFS
JulioSergioFS self-requested a review August 7, 2026 16:16

@JulioSergioFS JulioSergioFS left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" in webserver/version.py with a strong
    comment on when to raise it.
  • Documents the endpoint in EDITOR_INTEGRATION.md and 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, /capabilities and /version never disagree on the
    version, payload shape, the X-OpenPLC-Runtime-Version header covers the new
    route, and minEditorVersion parses as bare x.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/capabilities reachable 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).

@marconetsf
marconetsf merged commit 1ab588c into development Aug 7, 2026
@marconetsf
marconetsf deleted the feature/DOPE-448-version-compatibility branch August 7, 2026 17:41
@thiagoralves thiagoralves mentioned this pull request Aug 11, 2026
3 tasks
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.

2 participants