Skip to content

BLOCKED on srcfl/ftw#743 — make the signed artifact agree that myuplink is read-only - #65

Open
HuggeK wants to merge 2 commits into
srcfl:mainfrom
HuggeK:fix/myuplink-read-only-artifact
Open

BLOCKED on srcfl/ftw#743 — make the signed artifact agree that myuplink is read-only#65
HuggeK wants to merge 2 commits into
srcfl:mainfrom
HuggeK:fix/myuplink-read-only-artifact

Conversation

@HuggeK

@HuggeK HuggeK commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Warning

Do not merge before srcfl/ftw#743 has landed and shipped.
A read-only myuplink reaching a host without that change loses its token
refresh and stops reading. CI here is also expected red — see the last
section.

This is the second half of #64. #64 corrects the catalog label for five
drivers that are badged Control while refusing every command. This corrects
the signed artifact for the one of the five where that is not just a label.

The channel does not read the manifest's control field. It infers control
from the presence of a driver_command entrypoint:

controls = has_driver_command and not declares_read_only

myuplink's entrypoint exists and returns false unconditionally, so the
channel publishes it control_enabled: true, read_only: false, with a write
permission. This makes it declare read_only = true in its own DRIVER table —
the mechanism the channel already documents as authoritative.

The problem that makes this more than a one-line change

The read-only guard replaces host.http_post with a function that raises, and
myuplink POSTs to /oauth/token for the token it reads with. So it declares
auth_post_path, and the generated guard confines the POST rather than
trusting it:

host.http_post = function(url, ...)
    local path = type(url) == "string" and url:match("^[^?]*") or ""
    if path:sub(-#__sourceful_ftw_auth_path) == __sourceful_ftw_auth_path then
        return __sourceful_ftw_http_post(url, ...)
    end
    error("this driver declares itself read-only: POST is allowed only for authentication")
end

A path rather than a URL because base_url is config-overridable — a hardcoded
URL would break any site pointing the driver elsewhere.

Every other read-only driver was checked, and none is affected

Rebuilding the channel and cross-referencing each read-only driver's source
against the functions the guard replaces: only myuplink calls one, and
only http_post. deye, pixii and solaredge call modbus_write but
publish control-capable, so they are never guarded.

The exemption is opt-in and asserted to stay that way:
test_signing_in_is_declared_or_it_does_not_happen fails if any driver other
than myuplink loses the blanket denial, and checks that every other read-only
driver still has neither auth_post_path nor http.post.

Why CI is red here, which is the second thing needing a decision

drivers/tests/conftest.py exempts a driver byte-identical to baselines/ftw
from catalog conventions, and says a driver "stops being exempt the moment
someone edits it"
. Adding two metadata fields to the DRIVER table costs
myuplink that exemption and lights up 7 convention checks it was never
written to meet — among them should use 'http://' scheme and reference config.port for an HTTPS cloud API, and should call host.emit() when
emitting metrics only is the driver's stated design.

So this needs a maintainer's call: mark those checks as not applying to a
cloud driver, or accept myuplink diverging from FTW's copy. I did not want to
pick either inside a labelling fix.

Version moved 1.1.1 → 1.2.0: the Lua changed, so the artifact bytes moved.
The signed channel accepts this tree check confirms the bump covers it.

🤖 Generated with Claude Code

claude and others added 2 commits July 31, 2026 13:01
…y command

myuplink, nibe_local, pixii_pv, solis_string and tibber declared
control: true. All five have a driver_command that refuses
unconditionally, and four say so in a comment on the line above the
return false -- "Read-only: no actuation", "Tibber Pulse is a read-only
meter; no control surface".

The Control badge is the first thing an owner reads when deciding
whether a driver can run their heat pump or their battery, and for these
five it promised something the code refuses. They are now control:
false, which is what their sources have said all along.

No Lua changed and no artifact bytes moved: control is not among the
fields the channel copies into the signed artifact, so no version had to
move. Verified by rebuilding the channel from main and comparing all 80
digests.

tests/test_control_claims.py compares the claim against the code, which
nothing did before. A driver_command that calls nothing cannot reach a
write however it is invoked. Reverting any of the five fails it by name.
It also asserts at least 20 drivers still read as controlling, because a
guard that stops recognising anything passes exactly as loudly as one
that works -- which its own unit test caught while it was being written,
when the call regex counted `function driver_command(` and so found
every driver busy.

Signed-off-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Not ready to merge. Two blockers found while building it, both external
to this diff -- recorded here so the work is not lost.

What this does: myuplink declares read_only = true and
auth_post_path = "/oauth/token" in its DRIVER table. The channel then
publishes it read_only with control_enabled false, and the generated
guard permits host.http_post only for a URL whose path ends in the
declared path, refusing it everywhere else. So the flag enforces "this
POST is authentication" rather than asserting it. A path rather than a
URL because base_url is config-overridable.

Blocker 1 -- FTW rejects it. go/internal/driverrepo/sourceful.go,
validateSourcefulReadOnlyContract, allows a read-only package only
http.get, modbus.read, mqtt.subscribe, serial.read and returns
"read-only package requests write-capable permission" for anything else.
Publishing this without an FTW change first makes every host refuse
myuplink at install.

Blocker 2 -- editing the Lua forfeits the FTW-baseline exemption.
drivers/tests/conftest.py exempts a driver byte-identical to
baselines/ftw and says so explicitly: it "stops being exempt the moment
someone edits it". Adding two metadata fields costs the exemption and
lights up 7 catalog-convention checks this driver was never written to
meet -- among them "should use 'http://' scheme" and "reference
config.port" for an HTTPS cloud API, and "should call host.emit()" when
emitting only metrics is the driver's stated design.

The catalog half is already shippable and separate: see the branch
fix/myuplink-read-only-catalog (srcfl#64), which is green.

Signed-off-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com>
@HuggeK
HuggeK requested a review from frahlg July 31, 2026 12:17
@HuggeK
HuggeK marked this pull request as ready for review July 31, 2026 12:26
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