Files
odysseus/specs/gallery-editor-media.md
T
RaresKeYandStressTestor 7026cf40b5 docs: bootstrap specs ground truth (#5794)
* docs(specs): restore bootstrap after dev rewrite

* docs(specs): remove runtime inventory snapshot

* docs(specs): reconcile current dev truth

* docs(specs): document scheduled task actions as an owner-attribution source

Owner Attribution covered cookie, bearer-token and internal-loopback
requests. Scheduled task actions are a fourth source and behave
differently: _execute_action passes owner=task.owner off the stored
ScheduledTask row, so no request and no resolved principal are in
flight, and route-level require_user() never runs.

Webhook triggers are the sharp case. They are unauthenticated by
design with the token as the only credential and execute under the
stored task.owner.

Paths cite routes/task/task_routes.py, the canonical location after
the task subpackage move (#6081); routes/task_routes.py on current dev
is the backward-compat shim.

* docs(specs): add chained tasks to the trigger list, refresh dev stamp

Review feedback from RaresKeY on the previous commit.

"Every trigger path" was too broad: success-chained tasks are another
path into _execute_action. Added them with their own citation, and
noted that chaining additionally requires the target task to share
task.owner and rejects cycles, which is stricter than the trigger-side
checks. Softened the lead-in to "these trigger paths".

Line 56 still pointed at routes/task_routes.py for webhook credential
validation. That path is the backward-compat shim on current dev after
the task subpackage move (#6081); repointed to the canonical
routes/task/task_routes.py.

Stamp moved to dev@2a6b09b. Inspection backing that bump was scoped:
every file path cited in this spec was mechanically checked to resolve
on 2a6b09b, and every file:line in the Owner Attribution additions was
read against it. Behavioral claims elsewhere in the file were not
re-audited.

* docs(specs): correct SECURE_COOKIES description to match current behavior

Third of the stale details RaresKeY enumerated. The cookie section
described SECURE_COOKIES as purely opt-in, which stopped being true.

_secure_cookie() (routes/auth_routes.py:89) treats an explicit true or
false as authoritative and derives the Secure attribute from the
request otherwise, including when the variable is unset and when
docker-compose injects it present-but-empty. Either the connection
scheme or the first X-Forwarded-Proto hop being https is enough.

* docs(specs): refresh current dev truth

---------

Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
2026-08-25 14:18:44 +02:00

13 KiB

Gallery, Editor, And Media

Last updated: dev@e71f8ce | 2026-08-25

Scope

This spec covers media surfaces in:

  • app route registration and generated-file serving in app.py;
  • canonical models in core/database.py, with src.database as a compatibility import path;
  • canonical route package routes/gallery/gallery_routes.py and routes/gallery/gallery_helpers.py, with top-level routes/gallery_routes.py and routes/gallery_helpers.py compatibility shims;
  • generated-image writers in src/ai_interaction.py and mcp_servers/image_gen_server.py;
  • local MLX image compatibility server scripts/mlx_image_server.py;
  • image tool schemas/dispatch/implementations in src/tool_schemas.py, src/tool_execution.py, and src/tool_implementations.py;
  • routes/editor_draft_routes.py;
  • routes/signature_routes.py and document signature consumers in canonical routes/document/document_routes.py;
  • routes/emoji_routes.py;
  • routes/font_routes.py;
  • src/generated_images.py;
  • src/visual_report.py plus research image hide/unhide routes;
  • database models GalleryImage, GalleryAlbum, EditorDraft, and Signature;
  • generated files under data/generated_images;
  • frontend modules static/js/gallery.js, static/js/galleryEditor.js, static/js/editor/*, static/js/signature.js, static/js/emojiPicker.js, static/js/chatRenderer.js, static/js/document.js, static/js/markdown.js, and static/js/theme.js;
  • CLI surfaces scripts/odysseus-gallery and scripts/odysseus-signature;
  • tests covering gallery helpers/routes, generated-image serving, editor drafts, signatures, visual reports, fonts, upload limits, and image endpoint security.

Current Call Sites Include

  • gallery upload, library, album, tag, favorite, ZIP, delete, and saved-project views;
  • chat-generated image rendering/edit/delete bubbles;
  • agent generate_image and stale edit_image tool paths;
  • MCP image-generation rows/files;
  • image editor AI tools and model endpoint pickers;
  • document PDF signing with stored signatures;
  • visual-report hero/section image insertion and research hide/unhide controls;
  • emoji picker/markdown emoji SVG proxy calls;
  • theme custom-font loading;
  • local gallery/signature CLI inspection.

routes.gallery.gallery_routes owns gallery upload/import/library/editor transform behavior: upload dedupe, image/video extension handling, EXIF extraction for images, albums, favorites, tags, generated media metadata, search/filter/sort, owner filtering, ZIP downloads, soft delete, disk cleanup, and chat-history cleanup after image delete. Top-level routes.gallery_routes is a sys.modules compatibility shim to the canonical module.

Frontend gallery behavior includes upload progress, folder-drop album import, stale-while-revalidate cards, saved editor projects, detail actions, bulk delete/download, and cache-busted image refreshes.

Album assignment and gallery image detail/update endpoints enforce owner scope and fail closed when no authenticated owner is available instead of falling back to broad access.

Generated media provenance:

  • generated filenames are opaque hex-like media names, not trusted content hashes;
  • upload file_hash is a separate metadata field;
  • generated files live under data/generated_images;
  • chat image generation writes files and inserts GalleryImage rows through src.ai_interaction;
  • MCP image generation can create ownerless rows/files;
  • generated-but-not-yet-imported images can have no gallery row;
  • once a gallery row exists, owner checks decide visibility where the route enforces them.

app.py owns direct /api/generated-image/{filename} serving through src.generated_images.resolve_generated_image_path(). It validates hex-like image/video filenames, rejects path escape and missing files, serves rowless generated files, checks row owner when a row exists, allows null-owner compatibility rows, and uses immutable/nosniff cache headers. Gallery replace/rotate/save/delete/ZIP paths also resolve filenames through a shared generated-image path helper so database filenames cannot escape data/generated_images. Replace/rotate/save-over-original flows can mutate bytes under the same filename, so frontend cache busting matters.

Image Tools And Providers

Gallery/editor image transforms are split across:

  • /api/gallery/ai-upscale and /api/gallery/style-transfer;
  • /api/image/inpaint;
  • /api/image/harmonize;
  • /api/image/sharpen;
  • /api/image/denoise;
  • /api/image/upscale-local;
  • /api/image/remove-bg;
  • /api/image/enhance-face.

AI image endpoints mostly require image-generation privilege in the gallery route layer. The sharpen route is explicitly auth-gated; utility routes that live outside gallery still need their own route-level gate checks rather than assuming a shared decorator. The chat image-generation session path calls do_generate_image() separately and has its own privilege/tool-listing behavior.

Provider behavior:

  • OpenAI image edits use multipart /images/edits, mask conversion, size coercion, model restrictions, and source compositing where needed;
  • diffusion/self-hosted paths use JSON APIs such as inpaint, img2img, variations, harmonize, or A1111-compatible fallbacks;
  • client-supplied endpoint URLs on selected routes must pass outbound endpoint validation; DB-selected image endpoints should be resolved through owner-visible endpoint queries before decrypted headers/keys are used;
  • provider-returned image result URLs are validated with src.url_safety.check_outbound_url() before server-side download, with private-IP blocking controlled by image-route settings;
  • AI endpoint path suffixes are allowlisted before proxy/download use so arbitrary endpoint paths cannot be selected through gallery/editor requests;
  • editor model pickers load /api/model-endpoints and classify image-capable endpoints.

Optional dependency behavior:

  • Pillow-backed paths are effectively core for EXIF, rotate, sharpen, and image preparation;
  • Real-ESRGAN powers denoise/upscale when installed and otherwise returns install guidance; import-time torchvision compatibility patches run before Real-ESRGAN imports;
  • remove-bg tries rembg, then transformers-style fallback, then an error;
  • face enhancement falls back from GFPGAN/OpenCV toward PIL behavior;
  • video uploads intentionally skip EXIF/ffprobe metadata today.
  • grounding and mask model inputs cast only float64 tensors to float32 before transfer to Apple's MPS backend, because MPS rejects float64; integer/other tensors and non-tensor processor values preserve their normal device-transfer behavior.

Editor Drafts

routes.editor_draft_routes owns server-backed image editor project payloads. EditorDraft rows store title, payload JSON, thumbnail, source image, timestamps, and owner.

Frontend editor behavior is split across static/js/editor/* and static/js/galleryEditor.js: canvas state, layer panel, masks, history, snapping, stroke pipeline, inpaint/rembg/harmonize tools, AI tool runner, model pickers, an AI edit command box that routes natural-language edit requests into existing inpaint/remove/upscale/background/style actions where possible, import wiring, topbar controls, auto-save, resume by draft ID or source image, draft-only open, and cleanup after close. static/js/panels.js loads this module graph on first editor use, shares concurrent imports, retries failed loads, and static/sw.js keeps the lazy graph in a separate offline panel precache.

Draft compatibility behavior:

  • v2 server drafts store payloads and thumbnails server-side;
  • legacy/local raw payloads can still be restored by the frontend;
  • PUT 404 can recreate a missing draft row;
  • broken image drafts can fall back to the source image;
  • final close persist is best-effort.

Signatures, Emoji, Fonts

routes.signature_routes owns reusable signature/stamp rows. Signature image payloads are normalized to bounded PNG base64, encrypted at rest, and owner-filtered; SVG signature input is not preserved. Document PDF render/export paths owner-filter signature IDs before stamping.

routes.emoji_routes owns same-origin OpenMoji black SVG proxy/caching. It validates codepoint filenames, caches SVGs under data/emoji_cache, and returns transparent no-store SVGs for invalid, unknown, or unreachable codepoints. static/js/emojiPicker.js is a curated inline monochrome picker.

routes.font_routes owns deriving available custom font family names from static font files under static/fonts/custom.

Visual Reports

src.visual_report owns generated research/report HTML image behavior: HTTPS Open Graph image filtering, hero images, section images, icon/logo filtering, hide/reroll client controls, and inline JSON escaping for scripts.

Research routes and handler code own hidden-image persistence. Visual reports render model/source-influenced Markdown to HTML, so raw HTML/link/image sanitization remains security-sensitive.

Security Policy

Media routes are cookie/current-user surfaces unless they explicitly implement token owner/scope handling. Bearer-token callers that arrive as synthetic api users should not be treated as owner-scoped media API clients without explicit policy.

Known boundaries:

  • image-generation routes require can_generate_images;
  • image proxy/editor endpoints currently resolve client-selected, DB-selected, or fallback image model endpoints without full owner-scoped endpoint-key policy or uniform outbound revalidation;
  • generated-file serving allows rowless files and null-owner compatibility rows;
  • uploads are byte-limited and extension-gated, with content sniffing available through UploadHandler.detect_content_type() when python-magic/libmagic is installed;
  • several base64 JSON editor routes accept large decoded image payloads and need route-level size discipline;
  • gallery DB filenames should be joined through shared generated-media path helpers before filesystem operations;
  • editor draft source image IDs, payloads, and thumbnails are owner-scoped by draft owner but do not fully validate source-gallery ownership or payload size;
  • emoji proxy constrains codepoint filenames and degrades invalid, unknown, or unreachable SVGs to transparent no-store placeholders, but remote SVG content still deserves security review;
  • visual report Markdown HTML/link/image output needs continued sanitization coverage.
  • scripts/mlx_image_server.py pins generation/edit routing to the process-start model and ignores request-selected model names, preventing unauthenticated callers from selecting a local model directory/repository whose model-specific script or bridge would execute.

Degraded And Compatibility Behavior

  • Uploaded images record display dimensions with EXIF orientation when possible; EXIF failures warn/degrade.
  • Video uploads skip EXIF and have no metadata extraction yet.
  • Missing generated files are skipped in ZIP downloads; if all are missing, the route returns no files found.
  • Soft delete commits the gallery row state before removing the disk file, so a failed DB write does not orphan a missing image row.
  • AI tagging can fail when disk files are missing.
  • Static JS/CSS/HTML assets revalidate because there is no frontend build/versioning.
  • Gallery/editor frontend state includes stale-while-revalidate and listener cleanup to avoid stale handlers.
  • edit_image tool schema/implementation currently appears stale against implemented /api/image/* and /api/gallery/* routes.

Testing Coverage

Existing tests cover EXIF dimensions, owner-filter helper behavior, direct upload limits, image-generation privilege source shape, sharpen auth, gallery null-user denial, endpoint SSRF/source checks, editor draft payload validation, lazy editor loading/offline precache, MLX request-model pinning, font family derivation, visual-report helper behavior, gallery CLI previews, and selected security regressions.

Route-level coverage is thin for full gallery CRUD/album/tag/download/delete flows, generated-image serving, editor draft owner CRUD, signature owner CRUD, emoji proxy/cache behavior, image-tool degraded responses, optional dependency fallbacks, and frontend editor behavior.

Current Gaps

  • Owner-scoped endpoint-key resolution is needed for image proxy/editor routes.
  • Media routes need a clear API-token policy: reject token callers, or implement owner/scope handling.
  • Generated-image serving needs live route tests for invalid filenames, rowless files, owned rows, null-owner rows, MIME/cache headers, and cross-owner behavior.
  • Mutable generated filenames plus immutable cache headers need cache-busting tests for replace/save-over-original flows.
  • Base64 JSON editor payload size limits need hardening; upload content sniffing should keep native/Docker parity coverage as dependencies change.
  • MCP image generation needs an owner attribution decision or explicit admin-only documentation.
  • edit_image tool route mapping appears stale.
  • Emoji SVG proxy/cache and visual-report raw HTML/link sanitization need stronger tests.
  • Optional image dependency fallbacks are mostly untested.