Camera permission and review fixes (openpanel C3, C4)

C4: a device's `viewers` (superusers by default, or members) covers the
image and video messages its ports publish. Anyone else gets none of their
values on the socket, in the catalogue or a flow's state, 403 for their
history, for the frames they hold (the last 8 included) and for TURN
credentials, no bytes when asking for them, and `bye: forbidden` for a live
view. A device credential stays bounded by its dashboards. The device page
sets it in its Camera section, and a tile the viewer may not see says
"No access to this camera" and asks the camera for nothing.

C3:
- A volatile upload is held to MAX_FRAME_BYTES (4 MiB, 413 above it, read
  without extra copies), and so is a frame on a device's socket.
- A frame the engine holds in memory is written to the ring directory
  before a local worker is handed it, so load_artifact finds it.
- A socket the bus dropped events for catches up with a snapshot, demand
  included, and a device's credential is checked again per frame, cached by
  the flow store revision and its nonce, so a lost event never keeps an
  unpaired device's frames coming.
- A report is at most 64 KB once its named config_schemas are put in place.
- A device away for 60 s has its true bool messages set to false.
- Refused frames are logged by fixed keys, a frame is named after its
  message, `fluksio serve` waits 5 s for open connections, a tile with a
  stream URL creates no demand, the 8-name cap keeps the newest names.
- Every container's log rotates (json-file, 3 x 10 MB).
This commit is contained in:
Melvin Strobl
2026-10-17 09:20:04 +02:00
parent 6a98dcd23f
commit 72c1d73e96
36 changed files with 1268 additions and 152 deletions
+28 -15
View File
@@ -174,7 +174,10 @@ curl "$FLUKSIO/artifacts/<digest>" \
The upload returns a reference with the content digest. Identical bytes are
stored once. Node code uses `save_artifact` and `load_artifact` instead.
`?volatile=true` holds the bytes in the engine's memory for a live frame, where
newer frames push them out, instead of storing them.
newer frames push them out, instead of storing them. Such a frame is at most
`MAX_FRAME_BYTES` (4 MB), and a larger one is refused with 413. A camera only
superusers may see answers 403 to anyone else for the frames its messages
hold, the last 8 included (see [Who may watch a camera](../interface/dashboards.md#who-may-watch-a-camera)).
A superuser can remove stored bytes with `DELETE /artifacts/<digest>`. While
a run, a current message or a flow input's initial value still references the
@@ -200,8 +203,8 @@ on its own, so this is for reclaiming space sooner.
| `GET` | `/devices/pair/{code}/device` | what is waiting on a code, before it is approved |
| `POST` | `/devices/{id}/pair` | adopt by `{code}`, or by `{request}` from the pending list for a request from your network |
| `POST` | `/devices/{id}/unpair` | revoke the device credential, one the portal issued included |
| `PUT` | `/devices/{id}/report` | what the device has, from its own credential only; at most 16 KB, replaces the last report, read leniently (see [What a device reports](../interface/dashboards.md#what-a-device-reports)) |
| `GET` | `/devices/{id}/ice` | STUN and TURN servers for a live view of the device, as `{ice_servers: [{urls, username?, credential?}], ttl}`, with TURN credentials of the caller's own; not for a device credential |
| `PUT` | `/devices/{id}/report` | what the device has, from its own credential only; at most 16 KB, and 64 KB once its named `config_schemas` are put in place (413 above either), replaces the last report, read leniently (see [What a device reports](../interface/dashboards.md#what-a-device-reports)) |
| `GET` | `/devices/{id}/ice` | STUN and TURN servers for a live view of the device, as `{ice_servers: [{urls, username?, credential?}], ttl}`, with TURN credentials of the caller's own; 403 for whoever may not watch its camera, and for a device credential |
`/panels/pair`, `/panels/pair/{code}` and `GET /panels/{id}` answer as their
`/devices/` counterparts, for firmware that has not updated yet.
@@ -254,7 +257,7 @@ The socket sends a snapshot followed by node, value, health, and run events.
A client asks for the bytes of the media it shows with
`{"type": "media", "names": [...]}`, which replaces what it asked for before,
for at most 8 messages. Each frame held in memory then arrives as a binary
for at most 8 messages, the last 8 it names when it names more. Each frame held in memory then arrives as a binary
message before the value naming it: four bytes of header length (big endian),
a JSON header `{"type": "media", "name", "digest", "media_type", "ts"}`, then
the bytes. Messages are sent without per-message compression.
@@ -264,17 +267,26 @@ A device's socket is bounded to its own messages. Its snapshot carries
`"uplink"`, `"binary"` on its own socket and `"http"` through the portal. When
that set changes it is sent `{"type": "demand", "ports": [...]}`. A port has
demand while a socket asks for the media of the message it is wired to, or a
node of a running flow reads that message.
node of a running flow reads that message. A socket that falls so far behind
that events are dropped for it is sent a fresh snapshot, demand included, and
is closed with 1008 if it was unpaired meanwhile.
A person who may not watch a camera (see
[Who may watch a camera](../interface/dashboards.md#who-may-watch-a-camera))
gets none of its values in the snapshot or after it, and no frames when asking
for them. Switching the camera's setting sends that person's open sockets a
fresh snapshot.
With a `binary` uplink the device sends a camera frame as a binary message in
the same layout, with the header `{"type": "frame", "name": "<message>",
"media_type": "image/jpeg", "ts": <device ms>}`. The message has to be one of
its `in` ports wired as `image`. The frame is held in memory and published
in one step, credited to its device node. A frame that does not qualify is
logged and dropped and the socket stays open. With `http` it uploads with
`PUT /artifacts?volatile=true&name=<message>` and publishes with
`POST /messages/<message>`. A device that is unpaired or deleted while
connected is closed with 1008.
its `in` ports wired as `image`, and the whole message at most
`MAX_FRAME_BYTES`. The frame is held in memory and published in one step,
named after the message and credited to its device node. A frame that does not
qualify is dropped, the reason logged once per socket, and the socket stays
open. With `http` it uploads with `PUT /artifacts?volatile=true&name=<message>`
and publishes with `POST /messages/<message>`. A device that is unpaired or
deleted while connected is closed with 1008, at the latest with its next frame.
### Live view
@@ -294,10 +306,11 @@ the viewer. In order:
| 4 | sends and hears `candidate`, the bare `candidate:...` line | hears and sends `candidate` |
| 5 | sends `bye`, or hears `bye` or `busy` with a `reason` | hears `bye` with a `reason`, or sends `bye` or `busy` |
Any signed-in person may watch any device. A device speaks only for the
sessions it was handed on that socket. The server answers `busy` itself once a
device has 2 sessions, and `bye` with `offline` or `unsupported` when the
device is not connected or reports no `live` port. A session the viewer has not
A superuser may watch any device, anyone else a device whose camera is open
to members, and `start` is answered `bye` with `forbidden` otherwise. A device
speaks only for the sessions it was handed on that socket. The server answers
`busy` itself once a device has 2 sessions, and `bye` with `offline` or
`unsupported` when the device is not connected or reports no `live` port. A session the viewer has not
answered within 10 seconds ends with `timeout`, and either socket closing ends
its sessions with `closed` or `offline`. A socket that falls 64 signalling
messages behind ends the session with `overflow` instead of losing one.
+33 -2
View File
@@ -552,12 +552,20 @@ lights up on the canvas. `out` messages are read only. The node is shown
degraded, *Offline*, in amber while the device is not connected: a warning on
Home, not a fault, since a panel switched off or asleep breaks nothing.
A `bool` the device publishes, such as a camera's `person`, says something is
so right now, and a device that went away cannot say it no longer is. Once the
device has been away for 60 seconds, the same wait as before its node shows
*Offline*, each of its `bool` messages that reads true is set to false by its
device node. A device back within that wait keeps its values, so a restart of
either side changes nothing. Its other values keep the last one it sent.
### What a device reports
A paired device says what it has with `PUT /devices/{id}/report`, from its own
credential only. openpanel sends it after pairing and once per boot. The report
replaces the one before and is at most 16 KB. It is read leniently, so a newer
firmware still reports to an older instance:
replaces the one before and is at most 16 KB, and 64 KB as kept, once named
`config_schemas` are put in place. It is read leniently, so a newer firmware
still reports to an older instance:
- keys this version does not know are dropped
- a value it does not know for a display `shape` or a port `direction` reads as
@@ -635,6 +643,29 @@ A device may fetch the last 8 artifacts each of its messages pointed at, not
only the current one, so a screen fetching a camera frame by frame through the
portal is not refused a frame the camera has just replaced.
#### Who may watch a camera
**Who may watch** in the Camera section of the device's page decides who sees
what its camera sends, its `image` and `video` messages: **Superusers**, the
default, or **All members**. A member of the portal is a member here. For
anyone it leaves out:
- the messages' values are left out of the live socket, `GET /messages/` and a
flow's state, and their history answers 403
- the frames those messages hold, the last 8 included, answer 403, and a
socket asking for their bytes gets none
- a live view is refused with `forbidden`, and so are TURN credentials
- a dashboard tile or the device's preview says *No access to this camera*
A wall panel shows the camera when one of its dashboards carries it, whoever
watches the wall: a superuser assigned it the dashboards.
The setting covers the camera's own messages, not what a flow makes of them.
Anyone who edits flows can wire a node that passes a picture on under another
name, and node code reads whatever it is handed. How long a kept `snapshot`
stays and a light on the device that shows the camera is on are not part of
it yet.
#### Live view
A media tile showing a device's `camera`, and the preview on the device's
+6 -2
View File
@@ -169,14 +169,18 @@ MIG entries are passed on as written.
| `ARTIFACT_GC_GRACE_S` | `3600` | minimum age before cleanup |
| `ARTIFACT_VOLATILE_DIR` | auto-detected | where local workers write live frames, under `/dev/shm` or the temporary directory |
| `ARTIFACT_VOLATILE_BYTES` | `50331648` (48 MiB) | live-media ring size, in the engine's memory and again in that directory; `0` stores volatile saves durably |
| `MAX_FRAME_BYTES` | `4194304` (4 MiB) | the largest live frame: a volatile upload past it gets 413, a device's frame on its socket is dropped; keep it under 16 MiB, the websocket's limit |
Cleanup removes durable artifacts no longer referenced by recorded runs or
current messages, and pauses while a run is active. Volatile artifacts live in
a bounded ring in the engine's memory, and the oldest disappear as it fills.
Recorded run outputs are copied into durable storage. A worker in the engine's
container writes its frames to `ARTIFACT_VOLATILE_DIR` instead, which the
engine trims to the same size every few seconds. If workers publish frames
under Docker, raise `shm_size` with the ring size.
engine trims to the same size every few seconds. A frame the engine holds is
written there too before a node in this container is handed it, so
`load_artifact` finds it. If workers publish or read frames under Docker,
raise `shm_size` with the ring size. The ring never holds more than its size
plus one frame.
## Agents