feat: Per-user workflow libraries in multiuser mode#114
Conversation
lstein
left a comment
There was a problem hiding this comment.
The migration isn't working properly. On startup, I am seeing the following:
[2026-03-02 21:39:40,270]::[InvokeAI]::INFO --> Using torch device: NVIDIA Graphics Device
[2026-03-02 21:39:40,272]::[InvokeAI]::INFO --> cuDNN version: 90701
[2026-03-02 21:39:40,548]::[InvokeAI]::INFO --> Patchmatch initialized
[2026-03-02 21:39:41,923]::[InvokeAI]::INFO --> InvokeAI version 6.11.1.post1
[2026-03-02 21:39:41,924]::[InvokeAI]::INFO --> Root directory = /home/lstein/invokeai-lstein
[2026-03-02 21:39:41,924]::[InvokeAI]::INFO --> Initializing database at /home/lstein/invokeai-lstein/databases/invokeai.db
[2026-03-02 21:39:41,926]::[InvokeAI]::INFO --> Database update needed
[2026-03-02 21:39:41,926]::[InvokeAI]::INFO --> Backing up database to /home/lstein/invokeai-lstein/databases/invokeai_backup_20260302-213941.db
[2026-03-02 21:39:41,932]::[InvokeAI]::INFO --> Database updated successfully
[2026-03-02 21:39:41,932]::[InvokeAI]::INFO --> JWT secret loaded from database
[2026-03-02 21:39:42,018]::[ModelManagerService]::INFO --> [MODEL CACHE] Calculated model RAM cache size: 12786.88 MB. Heuristics applied: [1, 2].
[2026-03-02 21:39:42,020]::[ModelInstallService]::INFO --> Restoring incomplete installs
[2026-03-02 21:39:42,021]::[ModelInstallService]::INFO --> Finished restoring incomplete installs
[a bunch of lines deleted]
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/api_app.py", line 48, in lifespan
ApiDependencies.initialize(config=app_config, event_handler_id=event_handler_id, loop=loop, logger=logger)
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/api/dependencies.py", line 202, in initialize
ApiDependencies.invoker = Invoker(services)
^^^^^^^^^^^^^^^^^
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 14, in __init__
self._start()
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 31, in _start
self.__start_service(getattr(self.services, service))
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 20, in __start_service
start_op(self)
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 33, in start
self._sync_default_workflows()
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 472, in _sync_default_workflows
workflow_from_db = self.get(workflow_from_file.id).workflow
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 38, in get
cursor.execute(
sqlite3.OperationalError: no such column: user_id
[2026-03-02 21:39:42,023]::[uvicorn.error]::ERROR --> Application startup failed. Exiting.
Fixed in commit
|
lstein
left a comment
There was a problem hiding this comment.
Something is still wrong with the migration. The schema is not being updated with the user_id column. Here is the full stack trace:
[2026-03-02 22:30:03,509]::[InvokeAI]::INFO --> Using torch device: NVIDIA Graphics Device
[2026-03-02 22:30:03,511]::[InvokeAI]::INFO --> cuDNN version: 90701
[2026-03-02 22:30:03,787]::[InvokeAI]::INFO --> Patchmatch initialized
[2026-03-02 22:30:05,166]::[InvokeAI]::INFO --> InvokeAI version 6.11.1.post1
[2026-03-02 22:30:05,166]::[InvokeAI]::INFO --> Root directory = /home/lstein/invokeai-lstein
[2026-03-02 22:30:05,166]::[InvokeAI]::INFO --> Initializing database at /home/lstein/invokeai-lstein/databases/invokeai.db
[2026-03-02 22:30:05,167]::[InvokeAI]::INFO --> Database update needed
[2026-03-02 22:30:05,167]::[InvokeAI]::INFO --> Backing up database to /home/lstein/invokeai-lstein/databases/invokeai_backup_20260302-223005.db
[2026-03-02 22:30:05,173]::[InvokeAI]::INFO --> Database updated successfully
[2026-03-02 22:30:05,173]::[InvokeAI]::INFO --> JWT secret loaded from database
[2026-03-02 22:30:05,260]::[ModelManagerService]::INFO --> [MODEL CACHE] Calculated model RAM cache size: 12786.88 MB. Heuristics applied: [1, 2].
[2026-03-02 22:30:05,263]::[ModelInstallService]::INFO --> Restoring incomplete installs
[2026-03-02 22:30:05,263]::[ModelInstallService]::INFO --> Finished restoring incomplete installs
[2026-03-02 22:30:05,265]::[uvicorn.error]::ERROR --> Traceback (most recent call last):
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/starlette/routing.py", line 694, in lifespan
async with self.lifespan_context(app) as maybe_state:
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/contextlib.py", line 210, in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 209, in merged_lifespan
async with original_context(app) as maybe_original_state:
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/contextlib.py", line 210, in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 209, in merged_lifespan
async with original_context(app) as maybe_original_state:
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/contextlib.py", line 210, in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 209, in merged_lifespan
async with original_context(app) as maybe_original_state:
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 209, in merged_lifespan
async with original_context(app) as maybe_original_state:
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/contextlib.py", line 210, in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/invokeai-lstein/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 209, in merged_lifespan
async with original_context(app) as maybe_original_state:
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/contextlib.py", line 210, in __aenter__
return await anext(self.gen)
^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/api_app.py", line 48, in lifespan
ApiDependencies.initialize(config=app_config, event_handler_id=event_handler_id, loop=loop, logger=logger)
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/api/dependencies.py", line 202, in initialize
ApiDependencies.invoker = Invoker(services)
^^^^^^^^^^^^^^^^^
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 14, in __init__
self._start()
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 31, in _start
self.__start_service(getattr(self.services, service))
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/invoker.py", line 20, in __start_service
start_op(self)
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 33, in start
self._sync_default_workflows()
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 472, in _sync_default_workflows
workflow_from_db = self.get(workflow_from_file.id).workflow
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/lstein/Projects/InvokeAI-lstein/invokeai/app/services/workflow_records/workflow_records_sqlite.py", line 38, in get
cursor.execute(
sqlite3.OperationalError: no such column: user_id
[2026-03-02 22:30:05,265]::[uvicorn.error]::ERROR --> Application startup failed. Exiting.
Fixed in commit The root cause: Python's The fix switches the DDL to use |
6eb87a1 to
89ef06d
Compare
lstein
left a comment
There was a problem hiding this comment.
User isolation is working well. Now please implement workflow sharing. In the workflow "Details" panel, please add a "Shared workflow" checkbox. When this checkbox is selected, the workflow will be available in a read-only fashion to all users under the "Browse Workflows" section of the Workflow Library panel. Also auto-add the tag "shared" to these workflows so that they can be easily filtered on.
Only the owner of a workflow can edit or delete a workflow. The exception is the Administrator, who can edit or delete any user's workflow.
|
@copilot |
Done in commit
Here's how the Details panel looks with the new checkbox:
... Fixed in commit
All 13 tests now pass. |
lstein
left a comment
There was a problem hiding this comment.
The PR is working well now. I have just one UI request:
- When editing the workflow in the upper Details panel, please move the sharing checkbox up to the vertical position between the workflow Name text field and the "Workflow Thumbnail" label. Relabel the checkbox "Shared workflow" and make sure that the checkbox and its label are on the same line. Right now, the checkbox is on a line below its label.
Thank you!
Done in commit
The Details panel now looks like: |
lstein
left a comment
There was a problem hiding this comment.
Please move the "Shared workflow" checkbox combo in the Details section to the left of the label. This will match the appearance of the checkbox and label when the user does a "Save" or "Save As"
…r ownership checks, is_public endpoint, schema regeneration, frontend UI Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
…dit/delete, fix tests Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
c9d6c5e to
ef5b610
Compare
66ba32b
into
lstein/feature/workflow-isolation-in-multiuser-mode
…nvoke-ai#9018) * feat: Per-user workflow libraries in multiuser mode (#114) * Add per-user workflow isolation: migration 28, service updates, router ownership checks, is_public endpoint, schema regeneration, frontend UI Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * feat: add shared workflow checkbox to Details panel, auto-tag, gate edit/delete, fix tests Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Restrict model sync to admin users only (#118) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * feat: distinct splash screens for admin/non-admin users in multiuser mode (#116) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Disable Save when editing another user's shared workflow in multiuser mode (#120) * Disable Save when editing another user's shared workflow in multiuser mode Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * chore(app): ruff * Add board visibility (private/shared/public) feature with tests and UI Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Enforce read-only access for non-owners of shared/public boards in UI Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * Fix remaining board access enforcement: invoke icon, drag-out, change-board filter, archive Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * fix: allow drag from shared boards to non-board targets (viewer, ref image, etc.) Previously, images in shared boards owned by another user could not be dragged at all — the draggable setup was completely skipped in GalleryImage.tsx when canWriteImages was false. This blocked ALL drop targets including the viewer, reference image pane, and canvas. Now images are always draggable. The board-move restriction is enforced in the dnd target isValid functions instead: - addImageToBoardDndTarget: rejects moves from shared boards the user doesn't own (unless admin or board is public) - removeImageFromBoardDndTarget: same check Other drop targets (viewer, reference images, canvas, comparison, etc.) remain fully functional for shared board images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): add auth requirement to all sensitive routes in multimodal mode * chore(backend): ruff * fix (backend): improve user isolation for session queue and recall parameters - Sanitize session queue information of all cross-user fields except for the timestamps and status. - Recall parameters are now user-scoped. - Queue status endpoints now report user-scoped activity rather than global activity - Tests added: TestSessionQueueSanitization (4 tests): 1. test_owner_sees_all_fields - Owner sees complete queue item data 2. test_admin_sees_all_fields - Admin sees complete queue item data 3. test_non_owner_sees_only_status_timestamps_errors - Non-owner sees only item_id, queue_id, status, and timestamps; everything else is redacted 4. test_sanitization_does_not_mutate_original - Sanitization doesn't modify the original object TestRecallParametersIsolation (2 tests): 5. test_user1_write_does_not_leak_to_user2 - User1's recall params are not visible in user2's client state 6. test_two_users_independent_state - Both users can write recall params independently without overwriting each other fix(backend): queue status endpoints report user-scoped stats rather than global stats * fix(workflow): do not filter default workflows in multiuser mode Problem: When categories=['user', 'default'] (or no category filter) and user_id was set for multiuser scoping, the SQL query became WHERE category IN ('user', 'default') AND user_id = ?, which excluded default workflows (owned by "system"). Fix: Changed user_id = ? to (user_id = ? OR category = 'default') in all 6 occurrences across workflow_records_sqlite.py — in get_many, counts_by_category, counts_by_tag, and get_all_tags. Default workflows are now always visible regardless of user scoping. Tests added (2): - test_default_workflows_visible_when_listing_user_and_default — categories=['user','default'] includes both - test_default_workflows_visible_when_no_category_filter — no filter still shows defaults * fix(multiuser): scope queue/recall/intermediates endpoints to current user Several read-only and event-emitting endpoints were leaking aggregate cross-user activity in multiuser mode: - recall_parameters_updated event was broadcast to every queue subscriber. Added user_id to the event and routed it to the owner + admin rooms only. - get_queue_status, get_batch_status, counts_by_destination and get_intermediates_count now scope counts to the calling user (admins still see global state). Removed the now-redundant user_pending/user_in_progress fields and simplified QueueCountBadge. - get_queue_status hides current item_id/session_id/batch_id when the current item belongs to another user. Also fixes test_session_queue_sanitization assertions that lagged behind the recently expanded redaction set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(backend): ruff * fix(multiuser): reject anonymous websockets and scope queue item events Close three cross-user leaks in the websocket layer: - _handle_connect() now rejects connections without a valid JWT in multiuser mode (previously fell through to user_id="system"), so anonymous clients can no longer subscribe to queue rooms and observe other users' activity. In single-user mode it still accepts as system admin. - _handle_sub_queue() no longer silently falls back to the system user for an unknown sid in multiuser mode; it refuses the subscription. - QueueItemStatusChangedEvent and BatchEnqueuedEvent are now routed to user:{user_id} + admin rooms instead of the full queue room. Both events carry unsanitized user_id, batch_id, origin, destination, session_id, and error metadata and must not be broadcast. - BatchEnqueuedEvent gains a user_id field; emit_batch_enqueued and enqueue_batch thread it through. New TestWebSocketAuth suite covers connect accept/reject for both modes, sub_queue refusal, and private routing of the queue item and batch events (plus a QueueClearedEvent sanity check). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): verify user record on websocket connect A deleted or deactivated user with an unexpired JWT could still open a websocket and subscribe to queue rooms. Now _handle_connect() checks the backing user record (exists + is_active) in multiuser mode, mirroring the REST auth path in auth_dependencies.py. Fails closed if the user service is unavailable. Tests: added deleted-user and inactive-user rejection tests; updated valid-token test to create the user in the database first. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): close bulk download cross-user exfiltration path Backend: - POST /download now validates image read access (per-image) and board read access (per-board) before queuing the download. - GET /download/{name} is intentionally unauthenticated because the browser triggers it via <a download> which cannot carry Authorization headers. Access control relies on POST-time checks, UUID filename unguessability, private socket event routing, and single-fetch deletion. - Added _assert_board_read_access() helper to images router. - Threaded user_id through bulk download handler, base class, event emission, and BulkDownloadEventBase so events carry the initiator. - Bulk download service now tracks download ownership via _download_owners dict (cleaned up on delete). - Socket bulk_download room subscription restricted to authenticated sockets in multiuser mode. - Added error-catching in FastAPIEventService._dispatch_from_queue to prevent silent event dispatch failures. Frontend: - Fixed pre-existing race condition where the "Preparing Download" toast from the POST response overwrote the "Ready to Download" toast from the socket event (background task completes in ~17ms, so the socket event can arrive before Redux processes the HTTP response). Toast IDs are now distinct: "preparing:{name}" vs "{name}". - bulk_download_complete/error handlers now dismiss the preparing toast. Tests (8 new): - Bulk download by image names rejected for non-owner (403) - Bulk download by image names allowed for owner (202) - Bulk download from private board rejected (403) - Bulk download from shared board allowed (202) - Admin can bulk download any images (202) - Bulk download events carry user_id - Bulk download event emitted to download room - GET /download unauthenticated returns 404 for unknown files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): enforce board visibility on image listing endpoints GET /api/v1/images?board_id=... and GET /api/v1/images/names?board_id=... passed board_id directly to the SQL layer without checking board visibility. The SQL only applied user_id filtering for board_id="none" (uncategorized images), so any authenticated user who knew a private board ID could enumerate its images. Both endpoints now call _assert_board_read_access() before querying, returning 403 unless the caller is the board owner, an admin, or the board is Shared/Public. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(backend): ruff * fix(multiuser): require image ownership when adding images to boards add_image_to_board and add_images_to_board only checked write access to the destination board, never verifying that the caller owned the source image. An attacker could add a victim's image to their own board, then exploit the board-ownership fallback in _assert_image_owner to gain delete/patch/star/unstar rights on the image. Both endpoints now call _assert_image_direct_owner which requires direct image ownership (image_records.user_id) or admin — board ownership is intentionally not sufficient, preventing the escalation chain. Also fixed a pre-existing bug where HTTPException from the inner loop in add_images_to_board was caught by the outer except-Exception and returned as 500 instead of propagating the correct status code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(backend): ruff * fix(multiuser): validate image access in recall parameter resolution The recall endpoint loaded image files and ran ControlNet preprocessors on any image_name supplied in control_layers or ip_adapters without checking that the caller could read the image. An attacker who knew another user's image UUID could extract dimensions and, for supported preprocessors, mint a derived processed image they could then fetch. Added _assert_recall_image_access() which validates read access for every image referenced in the request before any resolution or processing occurs. Access is granted to the image owner, admins, or when the image sits on a Shared/Public board. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): require admin auth on model install job endpoints list_model_installs, get_model_install_job, pause, resume, restart_failed, and restart_file were unauthenticated — any caller who could reach the API could view sensitive install job fields (source, local_path, error_traceback) and interfere with installation state. All six endpoints now require AdminUserOrDefault, consistent with the neighboring cancel and prune routes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): close bulk download exfiltration and additional review findings Bulk download capability token exfiltration: - Socket events now route to user:{user_id} + admin rooms instead of the shared 'default' room (the earlier toast race that blocked this approach was fixed in a prior commit). - GET /download/{name} re-requires CurrentUserOrDefault and enforces ownership via get_owner(). - Frontend download handler replaced <a download> (which cannot carry auth headers) with fetch() + Authorization header + programmatic blob download. Additional fixes from reviewer tests: - Public boards now grant write access in _assert_board_write_access and mutation rights in _assert_image_owner (BoardVisibility.Public). - Uncategorized image listing (GET /boards/none/image_names) now filters to the caller's images only, preventing cross-user enumeration. - board_images router uses board_image_records.get_board_for_image() instead of images.get_dto() to avoid dependency on image_files service. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): add user_id scoping to workflow SQL mutations Defense-in-depth: the route layer already checks ownership before calling update/delete/update_is_public/update_opened_at, but the SQL statements did not include AND user_id = ?, so a bypass of the route check would allow cross-user mutations. All four methods now accept an optional user_id parameter. When provided, the SQL WHERE clause is scoped to that user. The route layer passes current_user.user_id for non-admin callers and None for admins. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(multiuser): allow non-owner uploads to public boards upload_image() blocked non-owner uploads even to public boards. The board write check now allows uploads when board_visibility is Public, consistent with the public-board semantics in _assert_board_write_access and _assert_image_owner. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Summary
In multiuser mode, all users shared a single workflow library. This PR isolates workflows per-user, adds a "Shared Workflows" section for publicly shared workflows, and provides controls to share/unshare workflows.
Backend
user_id(DEFAULT'system') andis_public(DEFAULTFALSE) columns + indexes toworkflow_library. Usesexecutescript()for the DDL statements so that transaction management is handled explicitly (executescript always issues COMMIT first), which avoids edge-cases in Python'ssqlite3implicit transaction handling for DDL on tables that contain VIRTUAL generated columns. A post-check raises a clearRuntimeErrorif the columns were not actually added, preventing silent failures. No cross-module imports in the migration callback.WORKFLOW_LIBRARY_DEFAULT_USER_IDconstant added toworkflow_records_common.pyto avoid magic strings across service and base layersworkflow_records_*: All query methods (create,get_many,counts_by_*,get_all_tags) acceptuser_idandis_publicfilters; newupdate_is_public()method — which also automatically adds the"shared"tag whenis_public=trueand removes it whenis_public=falseworkflows.pyrouter:CurrentUserOrDefaultlist_workflows/counts_by_*/get_all_tagsautomatically scopeusercategory results to the current user in multiuser mode (bypassed whenis_public=trueis explicitly requested)GET(non-owner blocked unless public/default/admin),PATCH,DELETE, and thumbnail endpointsPATCH /api/v1/workflows/i/{workflow_id}/is_publicendpointFrontend
openapi.jsonupdated;schema.tsregenerated viamake frontend-typegenWorkflowRecordOrderBygainsis_public;WorkflowLibraryViewgains'shared'WorkflowLibrarySideNav;WorkflowListroutes'shared'view tois_public=truequeryWorkflowGeneralTab(Details panel): newShareWorkflowCheckboxcomponent positioned between the workflow Name field and the Workflow Thumbnail section — visible to the workflow owner and admins in multiuser mode; label and checkbox are on the same horizontal line; togglesis_publicvia the APIWorkflowListItem: owners see aShareWorkflowToggleswitch; public workflows display a "Shared" badge;EditWorkflowandDeleteWorkflowbuttons are now gated behindisOwner || is_adminso non-owners cannot edit or delete others' workflowsSaveWorkflowAsDialog: "Share workflow" checkbox — marks new workflow public immediately after creationWorkflowSortControl+ sort options updated to includeis_publicsharedWorkflows,shareWorkflow("Shared workflow") added toen.jsonSingle-user mode behavior is completely unchanged — no user filtering is applied when
multiuserisfalse.Related Issues / Discussions
QA Instructions
Multiuser mode (
multiuser: truein config):"shared"tag is auto-added to the workflowSingle-user mode: verify all existing workflow operations behave identically to before.
Merge Plan
Migration 28 is additive (new nullable/defaulted columns); safe to deploy without data loss. Existing workflows are assigned
user_id = 'system'andis_public = false.The
workflow_librarytable in real databases has accumulated schema changes from earlier migrations (VIRTUAL generated columns added viaALTER TABLE). Migration 28 usesexecutescript()rather thancursor.execute()for its DDL to ensure deterministic commit behaviour on such tables regardless of Python version or SQLite version.Checklist
What's Newcopy (if doing a release after this PR)Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.