Skip to content

feat(drivers): let a read-only driver sign in before it reads - #743

Open
HuggeK wants to merge 2 commits into
srcfl:masterfrom
HuggeK:feat/read-only-driver-auth-post
Open

feat(drivers): let a read-only driver sign in before it reads#743
HuggeK wants to merge 2 commits into
srcfl:masterfrom
HuggeK:feat/read-only-driver-auth-post

Conversation

@HuggeK

@HuggeK HuggeK commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

A driver that reads a vendor cloud cannot read anything until it has exchanged
a token, and it exchanges one with a POST issued from driver_init or
driver_poll. allowWrite refuses exactly those phases:

if h.writePhase != "command" && h.writePhase != "default" {
    return fmt.Errorf("%s: write is not allowed during init, poll, or cleanup", permission)
}

That is right for actuation — nothing in those phases carries a command lease —
but it means a cloud telemetry driver cannot be published read-only at all.
The only way to make one work is to publish it control-capable.

That is what srcfl/device-drivers does for myuplink today. Its catalog entry
claims a control path, while its command entrypoint has always been:

function driver_command(_action, _power_w, _cmd)
    -- Read-only: no actuation in Step 1.
    return false
end

So the driver is published write-capable to preserve a token refresh, and an
owner reading the catalog is told it can control their heat pump.

What this changes

Authenticating is a precondition for reading rather than a write to the device,
so it is admitted separately from the write scope. RuntimePolicy gains
AuthPostPath, carried from the signed manifest's driver metadata, and
host.http_post skips allowWrite only for a URL whose path equals it.

Why the exemption cannot widen

allowAuthPost requires all of:

  • a RuntimePolicy (a bundled or local driver has none and is unaffected);
  • ReadOnly — a controlling driver has no exemption to take;
  • a non-empty AuthPostPath, which only the signed manifest can set;
  • http.post still granted in Permissions;
  • url.Parse(...).Path equal to the declared path — not a prefix or suffix.

A driver that declares nothing behaves exactly as before, which is every driver
in the channel today except myuplink.

Why it does not consume the write budget

A token refresh is driven by expiry, not by a caller. Charging it to
maxWrites would let a driver's own token lifetime exhaust the budget and
leave it unable to read — the failure this change exists to prevent.

Scope: signed channel only

This touches the signed-channel path (runtimePolicyForInstalledSignedDriver),
which is what srcfl/device-drivers publishes. The package-v1 path is
deliberately untouched: validateSourcefulReadOnlyContract still rejects any
write-capable permission on a read-only package. A read-only cloud driver
shipped that way would need the field in the package schema first, and no such
package exists today.

Companion change

srcfl/device-drivers declares auth_post_path in the driver's DRIVER table
and confines the same POST inside the generated artifact guard, so the
restriction holds in the Lua as well as in the host. That side must not merge
first
— a read-only myuplink reaching a host without this change would lose
its token refresh.

Testing

go/internal/drivers/read_only_auth_post_test.go covers the exemption applying
outside a write scope, staying off the write budget, being confined to the
declared path (including /oauth/token/../device, a bare /oauth, a foreign
host, and a malformed URL), and being refused when undeclared, when the driver
controls, when http.post was never granted, and when there is no policy.

go build ./... and go vet are clean. On this machine
./internal/drivers and ./internal/driverrepo fail an identical set of tests
with and without this change — 12 in drivers from a shallow clone lacking
bundled driver assets ("not in catalog"), and
TestPublicationRejectsDangerousOutputDirectories in driverrepo from a
Windows path separator. Nothing is introduced; CI on Linux should be the
authority.

🤖 Generated with Claude Code

A driver that reads a vendor cloud cannot read anything until it has
exchanged a token, and it exchanges one with a POST issued from
driver_init or driver_poll. allowWrite refuses those phases -- correctly,
since nothing there carries a command lease -- so a cloud telemetry
driver could not be published read-only at all. The only way to make it
work was to publish it control-capable, which is what srcfl/device-drivers
does for myuplink today: its catalog entry claims a control path, while
its driver_command has always been

    function driver_command(_action, _power_w, _cmd)
        -- Read-only: no actuation in Step 1.
        return false
    end

Authenticating is a precondition for reading rather than a write to the
device, so it is admitted separately: RuntimePolicy gains AuthPostPath,
carried from the signed manifest's driver metadata, and host.http_post
skips the write scope only for a URL whose path equals it.

The exemption is narrow by construction. It requires a policy that is
read-only, a non-empty declared path, http.post granted, and an exact
path match -- so it cannot become a general write channel, and a driver
that declares nothing behaves exactly as before. It does not consume the
write budget: a token refresh is driven by expiry rather than by a
caller, and spending the budget on it would leave the driver unable to
read once its token aged out.

Scope: the signed-channel path only, which is what srcfl/device-drivers
publishes. The package-v1 path is untouched and still rejects a write
permission on a read-only package; a read-only cloud driver shipped that
way would need the field in the package schema first.

Companion to srcfl/device-drivers, which declares auth_post_path in the
driver's DRIVER table and confines the same POST in the generated
artifact guard.
@HuggeK

HuggeK commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@frahlg flagging you for review.

This is the host half of a two-repo change. The other half is srcfl/device-drivers#64 and its follow-up, which found that five drivers — myuplink, nibe_local, pixii_pv, solis_string, tibber — are badged Control in the catalog while their driver_command refuses every command.

Four of the five can simply be relabelled. myuplink cannot: making it read-only takes away the POST it signs in with, because allowWrite refuses init and poll. That is what this PR addresses.

Merge order matters — this must land and ship before device-drivers publishes a read-only myuplink, or the driver loses its token refresh on every host.

I could not request you as a reviewer directly (no permission on this repo from a fork), hence the mention.

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