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/:

  1. Cache (generated-media-cache.ts) — cacheGeneratedImageForPhone(url) pulls each result onto hub disk at ~/.hivemindos/cache/generated-media/<sha256 of source>.<ext>. Accepts hub-local generated-media paths, 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.
  2. 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/sig shapes outright.
  • Secret: HIVEMINDOS_DASHBOARD_AUTH_SECRET when 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:

  1. Reads the request (media-rail.ts). A prompt is required. A model picked in the phone’s menu arrives as selection, the same shape the dashboard’s composer sends; the older appId / model fields 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.
  2. 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.
  3. Runs the same orchestration as the dashboard’s /api/chat/image-generation and /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.
  4. Caches the result onto hub disk (piece 1 above): every image, or one clip.
  5. 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.

Expanded image Scroll to pan · Esc to close
100%