Skip to content

fix(multiuser): redact other users' current-item identifiers from queue status events#135

Open
lstein wants to merge 12 commits intomainfrom
fix/queue-status-cross-user-leak
Open

fix(multiuser): redact other users' current-item identifiers from queue status events#135
lstein wants to merge 12 commits intomainfrom
fix/queue-status-cross-user-leak

Conversation

@lstein
Copy link
Copy Markdown
Owner

@lstein lstein commented Apr 25, 2026

Summary

QueueItemStatusChangedEvent embeds a SessionQueueStatus that includes the
currently-running item's item_id, session_id, and batch_id. The full event
ships to user:{owner} and admin rooms. When user A's item changed status while
user B's item was the one in progress, owner A's frontend received the event with
B's identifiers exposed in the embedded queue_status.

This was identified as out-of-scope in the security audit on #127.

Fix

In _set_queue_item_status, after building queue_status, scrub item_id /
session_id / batch_id when the in-progress item belongs to a different user
than the changed item. Aggregate counts stay global (not user-sensitive). The
frontend never reads those fields off the event payload (only batch_status.batch_id,
which is the changed item's own batch — no leak), so no UI behavior changes.

Test plan

  • New regression test test_event_redacts_other_users_current_item_identifiers
    verifies that when user B's item is in_progress and user A's item is canceled,
    A's emitted event has queue_status.item_id/session_id/batch_id == None.
  • test_event_preserves_owner_current_item_identifiers confirms no over-redaction
    when there's no in-progress item.
  • test_event_preserves_identifiers_when_current_item_is_the_changed_item confirms
    identifiers ARE exposed when the in-progress item is the changed item itself.
  • Verified the regression test fails without the fix and passes with it.
  • All 16 session-queue + sanitization tests pass.

🤖 Generated with Claude Code

…ue status events

QueueItemStatusChangedEvent embeds the SessionQueueStatus, which includes the
currently-running item's item_id, session_id, and batch_id. The event ships to
user:{owner} and admin rooms. When user A's item changed status while user B's
item was the one in progress, owner A's frontend received the event with B's
identifiers exposed.

In _set_queue_item_status, scrub item_id/session_id/batch_id from the embedded
queue_status when the in-progress item belongs to a different user than the
changed item. Aggregate counts remain global (not user-sensitive).

Identified out-of-scope in the security audit of #127.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pfannkuchensack and others added 11 commits April 26, 2026 19:29
…e-ai#8971)

* feat: Add virtual boards that dynamically group images by date

Virtual boards are computed on-the-fly via backend queries, not stored in the database.
The first virtual board type groups images by creation date into sub-boards per day.
The feature is togglable via the board settings popover and the collapse state persists across sessions.

* add missing Redux state migration

* Chore ruff check

* docs(gallery): document virtual boards by date

Add a "Virtual Boards by Date" section to the gallery feature docs
explaining how to enable the new By Date section, what each entry
shows, and that virtual boards are a read-only view over existing
images.

* fix(ui): invalidate VirtualBoards tag on image generation

Optimistic updates in onInvocationComplete cover ImageList and
BoardImagesTotal but not VirtualBoards, so date-grouped counts and
cover thumbnails would only refresh on the next mutation. Trigger an
explicit invalidation when at least one non-intermediate image was
added.
…es (invoke-ai#8899)

* Add LLM-powered prompt expansion and image-to-prompt features

Adds two new buttons to the positive prompt area:
- "Expand Prompt" uses a local TextLLM model (AutoModelForCausalLM) to expand brief prompts into detailed image generation prompts
- "Image to Prompt" uses an existing LLaVA OneVision model to generate descriptive prompts from uploaded images

Backend: new TextLLM model type with config, loader, pipeline wrapper, workflow node, and two new API endpoints (expand-prompt, image-to-prompt). Also fixes HuggingFace metadata fetch assertion error when file size is None.

Frontend: ExpandPromptButton and ImageToPromptButton components with model picker popovers, RTK Query mutations, and model type hooks. Buttons only appear when compatible models are installed.

* chore fix windows paths

* Fix device mismatch for LLM inference and add CPU-only toggle for Text LLM models

Derive the execution device from the loaded model parameters instead of
the global TorchDevice chooser so that cpu_only models no longer receive
GPU-bound inputs. Also expose the existing cpu_only setting in the
frontend Model Manager for Text LLM models.

* Harden LLM endpoints and add tests

- Bound max_tokens to 1-2048 on ExpandPromptRequest to prevent OOM
- Replace asserts with explicit type checks and proper HTTP status codes
  (404 for unknown models, 422 for wrong model type, 500 for unexpected)
- Use float32 dtype for cpu_only TextLLM models instead of global fp16
- Add 16 tests for TextLLMPipeline and API request validation

* Add Ctrl+Z undo for LLM prompt changes

Saves the previous prompt before LLM overwrites it (Expand Prompt and
Image to Prompt). Pressing Ctrl+Z in the prompt textarea restores
the original prompt. Undo state auto-expires after 30 seconds and
is cleared when the user types manually.

* Add documentation and What's New entry for LLM prompt tools

- Add docs/features/prompt-tools.md covering Expand Prompt, Image to
  Prompt, compatible models, Ctrl+Z undo, and the workflow node
- Register new doc page in mkdocs.yml under Features
- Add What's New item in en.json for the LLM Prompt Tools feature

* fix: resolve merge conflict in mkdocs.yml nav

* feat(ui): allow dragging gallery images onto prompt box for Image to Prompt

Add drop target on the positive prompt textarea so users can drag images
from the gallery directly into the prompt area. When dropped, the Image
to Prompt popover opens automatically with the image pre-loaded, ready
for description generation.

* chore typegen

* Fix typo in Z-Image Turbo diversity description

* Fix three bugs in LLM/VLM utility endpoints

Move torch.no_grad() from async endpoint into worker functions where
inference actually runs, since the context manager does not carry across
the thread boundary used by asyncio.to_thread().

Add threading.Lock around load_model() calls to serialize access to the
thread-unsafe model loader, preventing race conditions from concurrent
HTTP requests.

Catch ImageFileNotFoundException in image_to_prompt and return 404
instead of letting it fall through to the blanket 500 handler.

* Fix tokenizer validation, drag-drop dead end, and i18n for LLM prompt tools

Validate tokenizer files at model probe time instead of deferring to runtime.
Guard image drag-drop on the prompt textarea behind LLaVA model availability.
Add missing modelManager.textLLM i18n key and replace all hardcoded strings
in ImageToPromptButton and ExpandPromptButton with translation calls.

* Add unit tests for promptUndo module

* Fix typo in Z-Image Turbo diversity description

* Chore fix typegen

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
…metadata recall (invoke-ai#8995)

* Fix(Flux2): Correct guidance_embed, add guidance support for Klein 9B Base, and fix metadata recall

Klein 4B and 9B (distilled) have guidance_embeds=False, while Klein 9B Base
(undistilled) has guidance_embeds=True. This commit:
- Sets guidance_embed=False for Klein 4B/9B and adds Klein9BBase with True
- Adds guidance parameter to Flux2DenoiseInvocation (used by Klein 9B Base)
- Passes real guidance value instead of hardcoded 1.0 in flux2/denoise.py
- Hides guidance slider for distilled Klein models, shows it for Klein 9B Base
- Shows Flux scheduler dropdown for all Flux2 Klein models
- Passes scheduler to Flux2 denoise node and saves it in metadata
- Adds KleinVAEModel and KleinQwen3EncoderModel to recall parameters panel

* test(flux2): cover Klein guidance gating, scheduler metadata, and recall dedupe

Add a mock-based harness for buildFLUXGraph that locks in the FLUX.2
orchestration: guidance is written to metadata and the flux2_denoise
node only for klein_9b_base, distilled variants (klein_9b, klein_4b)
omit it, the FLUX scheduler is persisted into both metadata and the
denoise node, and separately selected Klein VAE / Qwen3 encoder land
in metadata.

Add parsing tests for the metadata recall handlers: KleinVAEModel and
KleinQwen3EncoderModel only fire when the current main model is FLUX.2,
and the generic VAEModel handler now bails out for flux2 / z-image so
the metadata viewer no longer renders duplicate VAE rows next to the
dedicated Klein / Z-Image handlers.

* Chore pnpm fix

* Update version to 1.5.0 in flux2_denoise.py

* Update condition for rendering ParamFluxScheduler

* feat(flux2): add Klein4BBase variant for FLUX.2 Klein Base 4B models

Recognize FLUX.2-klein-base-4B on import via filename heuristic.
The variant shares Klein4B's architecture (Qwen3-4B encoder,
context_in_dim=7680) and reports guidance_embeds=False in its HF
config, consistent with Klein 9B Base. UI behavior stays identical
to distilled Klein4B until CFG support is wired up in a follow-up.

* Change Wrong Comment

* refactor(flux2): remove inert guidance UI/metadata for FLUX.2 Klein

All current FLUX.2 Klein variants (4B, 4B Base, 9B, 9B Base) report
guidance_embeds=false in their HF transformer config (or have zeroed
projection weights), so the guidance scalar has no effect on output.
The linear UI previously exposed a guidance slider for klein_9b_base
and wrote the value into metadata, which misled users into thinking
it was steering generation.

* Chore typegen

* fix test

* fix(flux2): skip Guidance metadata recall for legacy FLUX.2 images

The generic Guidance metadata handler unconditionally parsed
`metadata.guidance` and dispatched `setGuidance(value)` into the
shared params slice. For images generated before the Klein guidance
cleanup, this still fired — silently writing a stale guidance value
into the global state, which then leaked back into FLUX.1 on model
switch.

Gate the handler on `metadata.model.base`: reject parsing when the
image was generated with a FLUX.2 model. The handler is then skipped
for both display and recall on legacy FLUX.2 metadata, matching the
"silently ignored" contract stated in the PR.

- parsing.tsx: check metadata.model.base in Guidance.parse()
- parsing.test.tsx: three new cases covering FLUX.2 gating,
  FLUX.1 pass-through, and back-compat for metadata without a
  model field

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
…me (invoke-ai#9079)

* Fix lazy If branch pruning and skipped-parent handling in graph runtime

* Tighten lazy If runtime edge-case handling

* Polish lazy If runtime diagnostics and idempotency

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
)

* feat(ui): add canvas snapshot save/restore functionality

Add ability to save and restore canvas state snapshots, allowing users
to preserve their canvas layout at any point and restore it later.
This is useful when the canvas freezes or resets unexpectedly.

Backend:
- Add get_keys_by_prefix and delete_by_key to client_state persistence
- Add corresponding API endpoints

Frontend:
- Add canvasSnapshotRestored reducer to canvasSlice
- Add useCanvasSnapshots hook for snapshot CRUD operations
- Add CanvasToolbarSnapshotMenuButton with save/restore UI
- Add i18n keys for snapshot feature
- Regenerate API schema types

Tests:
- Add tests for new client_state endpoints (prefix search, key deletion)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ui): address review feedback for canvas snapshot feature

- Preserve current modelBase on snapshot restore to prevent bbox desync
  with the active model (mirrors resetState pattern)
- Exclude snapshot restore from undo history so it cannot be accidentally
  undone
- Migrate manual fetch calls to RTKQ endpoints (clientState.ts) so
  snapshots go through the shared API transport layer with proper auth,
  session-expiry handling and sliding-window token refresh
- Validate referenced images on restore and warn when some are missing
- Detect incompatible (schema-changed) snapshots and show a specific
  error message instead of a generic failure toast
- Disable snapshot restore while the canvas is staging to prevent entity
  ID conflicts with in-progress generations
- Sort snapshot list by updated_at instead of rowid so re-saved
  snapshots appear at the top
- Add pre-flight backend reachability check before image validation to
  avoid false "missing images" warnings when offline

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor(ui): consolidate collectImageNames to shared canvasProjectFile utility

Remove the local collectImageNames from useCanvasSnapshots and reuse
the shared, more comprehensive version from canvasProjectFile.ts that
was introduced by the canvas project save/load feature (invoke-ai#8917).

Snapshots don't include global ref images, so an empty array is passed
for that parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(canvas-snapshots): escape LIKE wildcards, warn on overwrite, fix default name chars

- Escape %, _, \ in client_state prefix query to prevent accidental wildcard matching
- Confirm before overwriting an existing snapshot instead of silently replacing it
- Use - instead of / and : in the default snapshot name to avoid key separator clashes

* fix(canvas): align canvasProjectRecalled with snapshot restore pattern

Preserve modelBase, call syncScaledSize, and exclude from undo history
to avoid bbox/model desync on project load — same pattern already used
by canvasSnapshotRestored.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
…voke-ai#9045)

* feat(recall): support model-free reference images in recall API

The recall parameters API previously exposed only `loras`, `control_layers`,
and `ip_adapters`. This meant reference images used by architectures that
feed images directly into the main model — FLUX.2 Klein, FLUX Kontext, and
Qwen Image Edit — could not be sent through the recall endpoint at all:
they have no adapter model to resolve, so they could not ride in the
`ip_adapters` list.

This change adds a new `reference_images` field on RecallParameter that
carries only an `image_name`. The backend validates the file exists in
outputs/images and forwards the resolved metadata (width/height) in the
broadcast event. The frontend's recall handler picks the right config type
(`flux2_reference_image` / `flux_kontext_reference_image` / `ip_adapter`
fallback) via getDefaultRefImageConfig() based on the currently-selected
main model, matching the behavior of a manual drag-and-drop, and dispatches
`refImagesRecalled` with replace:false so these append rather than clobber
any adapters already applied in the same event.

Also consolidates the two existing docs under docs/contributing/RECALL_PARAMETERS/
(RECALL_PARAMETERS_API.md and RECALL_API_LORAS_CONTROLNETS_IMAGES.md) into
a single RECALL_PARAMETERS_API.md that documents the full request schema
including the new field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(recall): cover loras, control layers, and ip_adapters paths

The original recall_parameters router (PR invoke-ai#8758) shipped without any
unit tests for its three collection fields. This commit backfills that
coverage alongside the reference_images tests added in the previous
commit.

The resolver helpers (resolve_model_name_to_key, load_image_file,
process_controlnet_image) are monkey-patched via module-level attribute
replacement so each test can pin down a specific resolution outcome
without spinning up the model manager or an image-files service. Two
small factory helpers (make_name_to_key_stub / make_load_image_file_stub)
make that ergonomic.

New coverage:

* LoRAs — multi-entry resolution + weight/is_enabled pass-through,
  silent drop on unresolvable names, is_enabled default of True.
* Control layers — ControlNet resolution precedence, fall-through to
  T2I Adapter and Control LoRA in order, missing image gracefully
  warned-and-continued, processed_image attached when the processor
  returns data, unresolvable entries dropped.
* IP Adapters — IPAdapter-before-FluxRedux lookup order, method /
  image_influence pass-through, missing image gracefully warned-and-
  continued, unresolvable entries dropped.
* Combined happy path — full request with prompts + model + all four
  collection fields, verifying every resolved value reaches the
  broadcast payload.
* Main-model drop — an unresolvable main model is scrubbed from the
  broadcast so the frontend never receives a stale model name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(frontend): typegen

* chore: fix lint errors and typegen

* fix(test): patch ApiDependencies in auth_dependencies to fix recall tests

The patched_dependencies fixture only monkeypatched ApiDependencies in
the recall_parameters module, but the endpoint also resolves
CurrentUserOrDefault via auth_dependencies, which accesses
ApiDependencies.invoker independently. Patch both import sites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore(frontend): typegen

* feat(recall): add strict mode to clear unset parameters on recall

Add a `strict` query parameter (default false) to POST recall endpoint.
When true, parameters not in the request body are reset: list fields
(loras, control_layers, ip_adapters, reference_images) become [] and
scalar fields become null, so the frontend clears stale state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(frontend): eliminate ref image doubling from race between Promise.all chains

IP adapters and model-free reference images were dispatched via two
independent Promise.all chains — one with replace:true, the other with
replace:false.  When a previous recall's promises were still in-flight
they could resolve after the clear and re-append stale entries, doubling
the list.  Combine both into a single Promise.all with one replace:true
dispatch so the race is impossible.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
* feat: add Custom Node Manager for installing and managing node packs from the UI

Adds a new "Nodes" tab (circuit icon) to the sidebar with a two-panel layout:
- Left: list of installed custom node packs with reload and uninstall
- Right: tabbed install UI (Git URL / Scan Folder) with install log

Backend API endpoints (POST install, DELETE uninstall, POST reload, GET list)
handle git clone, pip dependency install, runtime node loading/unloading,
and automatic workflow import from node pack repositories. Workflows are
tagged with node-pack:<name> and removed on uninstall.

Includes user and developer documentation, plus 31 tests (21 backend, 10 frontend).

* Chore typegen

* Chore Typegen without Node

* feat(custom-nodes): detect deps instead of auto-installing

Auto-running pip on requirements.txt could pull incompatible
packages into the running InvokeAI env and break the app. The
installer now detects requirements.txt or pyproject.toml,
returns requires_dependencies + dependency_file, and the UI
shows a persistent warning toast pointing the user to the
node pack's documentation.

* Chore ruff

* custom nodes: require admin auth, share imported workflows, and localize UI

- Gate install/uninstall/reload routes on AdminUserOrDefault so they respect multiuser auth
- Import pack workflows under the installing admin with is_public=True so all users see them
- Replace hardcoded English strings in CustomNodesList and CustomNodesInstallLog with translations
- Reuse existing common/queue keys for clear/status, drop duplicates in en.json

* test(custom_nodes): update _import_workflows_from_pack tests for owner_user_id

Pass owner_user_id="admin" in all call sites and assert that user_id and
is_public=True are forwarded to workflow_records.create().

* custom nodes: track imported workflows via manifest and harden pack listing

- Record imported workflow IDs in .invokeai_pack_manifest.json inside the pack
  directory; uninstall reads the manifest before rmtree and deletes only those
  IDs, so user-authored workflows sharing the pack tag are preserved
- Gate GET /v2/custom_nodes/ with AdminUserOrDefault to match install/uninstall
  /reload and prevent unauthenticated disclosure of absolute node pack paths
- Extract getParentDirectory() helper that handles both POSIX and Windows
  separators so the nodes-directory label renders on all platforms
- Add regression tests for manifest roundtrip, colliding-tag preservation, and
  parent-directory extraction across separator styles

* Chore typegen

* ui: hide Custom Nodes tab for non-admin users in multiuser mode

Add useIsCustomNodesEnabled hook (mirrors useIsModelManagerEnabled) and
conditionally render the tab in VerticalNavBar. Backend routes already
reject non-admin callers; this prevents the UI from advertising controls
that would 403.

* ui: guard Custom Nodes tab content for non-admin persisted state and add auth regression tests

- Suppress CustomNodesTabAutoLayout render and redirect to generate via
  navigationApi.switchToTab when a non-admin user lands on a persisted
  customNodes tab
- Add TestCustomNodesAuthorization with 10 route-level tests verifying
  unauthenticated (401), non-admin (403), and admin (200) for list,
  install, uninstall, and reload endpoints
- Add decision-matrix test for useIsCustomNodesEnabled covering
  single-user, multiuser admin, multiuser non-admin, and unloaded user

* test: add shared helper for custom-nodes gate + admin happy-path tests

Extract getIsCustomNodesEnabled so test imports the real logic
instead of a local reimplementation. Add install/uninstall
admin-success tests with mocked filesystem/subprocess.

* fix(custom-nodes): return optimistic default while setup status loads

Prevents redirect away from persisted customNodes tab on startup
in single-user mode when RTK Query hasn't resolved yet.

* ui: split custom nodes permission into isKnown/isAllowed to close loading window

useIsCustomNodesEnabled now returns { isKnown, isAllowed } so the navbar
hides the tab conservatively (isAllowed=false while loading) while the
redirect only fires once the decision is definitive (isKnown && !isAllowed),
preventing both the non-admin flash and the single-user kickout.

* refactor(custom-nodes): extract permission derivation into shared helper

Tests now import deriveCustomNodesPermission directly instead
of mirroring the hook logic in a local simulateHook, so hook
and tests can never drift.

* Chore Knit fix

* fix(custom-nodes): purge full module subtree on uninstall

Only removing sys.modules[pack_name] left submodules cached, so
reinstall reused them and the @invocation decorators never re-
registered the nodes — the pack loaded with 0 nodes until a
full process restart. Now _purge_pack_modules strips the root
and every pack_name.* key.

* fix(custom-nodes): purge full module subtree on uninstall

Only removing sys.modules[pack_name] left submodules cached, so
reinstall reused them and the @invocation decorators never re-
registered the nodes — the pack loaded with 0 nodes until a
full process restart. Now _purge_pack_modules strips the root
and every pack_name.* key.

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
* feat: add editable source_url field to model config

Allow users to store a URL (e.g. download page or model page) on any
model, independent of the original installation source. The URL is
editable in the model edit form and displayed as a clickable link in
the model header view.

* fix(mm): validate source_url to prevent XSS via javascript: URI scheme

* fix(mm): harden source_url validator against non-string inputs

Guard the pydantic `mode="before"` validator with an isinstance check
so PATCH bodies like `{"source_url": 123}` surface as a 422 instead of
a 500 from AttributeError on `.startswith()`. Also set `type="url"` on
the model edit input for basic browser-level validation.

* Chore Typegen
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants