HivemindOS manual
Generated Media Signing
How generated images and videos reach phone clients privately: hub-side caching plus short-lived signed URLs, so media only ever travels hub → client over the tailnet.
Why this exists
Connected image apps (ComfyUI, Image Studio, Z-Image, LocalAI, …) usually
answer with URLs that only resolve from the hub machine itself
(http://127.0.0.1:7860/…), or with inline data: payloads. The dashboard
browser runs on the hub, so it can fetch those directly. A phone cannot — and
native image loaders on the phone carry neither the dashboard session cookie
nor the device-token header, so they also can’t pass normal dashboard auth.
The answer is two pieces, both under src/lib/services/chat/:
- Cache (
generated-media-cache.ts) —cacheGeneratedImageForPhone(url)pulls each result onto hub disk at~/.hivemindos/cache/generated-media/<sha256 of source>.<ext>. Accepts hub-localgenerated-mediapaths,data:image/…payloads, and HTTP(S) URLs (fetched by the hub, which can reach them). Validates by magic number, caps at 64 MB.cacheGeneratedVideoForPhone(url)does the same for a clip, with two differences that follow from its size: it is streamed to disk under a 1 GB cap rather than held in memory (a partial download is never servable and is removed on failure), and the container is checked here, from the file’s first bytes (MP4/M4V/MOV or WebM), so a file that is only named like a video is never signed for a phone. - Signing (
generated-media-signing.ts) — mints and verifies path-scoped, expiring HMAC capabilities for/api/chat/generated-media.
The URL contract
/api/chat/generated-media?path=<absolute path>&exp=<epoch ms>&sig=<hex hmac>
sig = HMAC-SHA256(secret, "generated-media.v1\n<path>\n<exp>")
- Scope: one absolute path and one expiry. A leaked URL grants exactly one
already-generated file, until
exp(default TTL 1 hour,DEFAULT_SIGNED_MEDIA_TTL_MS). - Verification is constant-time and rejects malformed
exp/sigshapes outright. - Secret:
HIVEMINDOS_DASHBOARD_AUTH_SECRETwhen configured — rotating it invalidates every outstanding URL. Hubs without dashboard auth get a per-install random secret at~/.hivemindos/generated-media-signing.secret(created 0600 on first use).
/api/chat/generated-media accepts a valid signature or normal dashboard
auth (session cookie / device token) — the signature path is an alternative
for native loaders, never a replacement. Either way the route keeps its own
safety checks: image and video extensions only, and a size cap for each
(64 MB for an image, 1 GB for a video). An image’s magic number is validated
when it is served. A video is streamed by its extension, with byte-range
support, and its bytes are not inspected by the route; that is why the
phone cache checks a clip’s container before anything is signed. The route
authenticates itself, so it belongs to the self-authenticating allowlist in
src/proxy.ts.
Phone generation flow
POST /api/phone with action: "image-generation" or
action: "video-generation" (used by HivemindOS Mobile’s /image-gen command
and its Generate menu). Both live in src/lib/services/phone/media-generation.ts:
- Reads the request (
media-rail.ts). A prompt is required. A model picked in the phone’s menu arrives asselection, the same shape the dashboard’s composer sends; the olderappId/modelfields still work by themselves. Anything present but unreadable is refused, never dropped: a selection that does not parse would otherwise run the automatic path, which is a place nobody picked. A start picture is accepted inline only, never as a file path. - Resolves the selection with the same resolver the dashboard routes use, so Automatic is decided against the live model list, and a model picked for a published privacy claim is refused if that claim is gone.
- Runs the same orchestration as the dashboard’s
/api/chat/image-generationand/api/chat/video-generation(runChatImageGeneration()/runChatVideoGeneration()): connected-app discovery, the picked place, job polling, generation metrics. Spending rules are decided there and only there, so they are identical from a phone and from the dashboard. An agent’s own image or video tool runs under the agent as this hub knows it; the request names the agent and nothing else about it. - Caches the result onto hub disk (piece 1 above): every image, or one clip.
- Answers with signed relative URLs (piece 2), app/model/machine metadata, and an echo of the selection that ran.
What a hub’s rail accepts is published as phoneRail beside the model list
(GET /api/chat/media-catalog). A phone offers a hub’s models only when the
hub says its rail runs them. That is deliberate: a hub older than selections
does not refuse one, it ignores the field and chooses for itself, so trying
and failing is not a safe way to find out.
The client is expected to download each file once, promptly, and store it locally — signed URLs are transport, not storage. Expiry is deliberately short; history should render from the client’s own copy.
Net effect: the image or clip crosses exactly one client-visible network boundary — hub → phone, inside the WireGuard-encrypted tailnet. The phone never talks to the app that made it, and the result transits no third-party service on its way to the phone.