Skip to content

feat(cli,api): difyctl use workspace + member management#36360

Open
lin-snow wants to merge 4 commits into
langgenius:feat/clifrom
lin-snow:feat/cli-workspace
Open

feat(cli,api): difyctl use workspace + member management#36360
lin-snow wants to merge 4 commits into
langgenius:feat/clifrom
lin-snow:feat/cli-workspace

Conversation

@lin-snow
Copy link
Copy Markdown
Contributor

Important

  1. Make sure you have read our contribution guidelines
  2. Ensure there is an associated issue and you have been assigned to it
  3. Use the correct syntax to link this PR: Fixes #<issue number>.

Summary

Adds server-side workspace switching and full member management to difyctl, on parity with the console's /workspaces/current/members endpoints but bearer-authed and gated on the path's workspace_id.

Backend — five new /openapi/v1/ endpoints, all double-gated by @accept_subjects(ACCOUNT) + @require_workspace_role(...):

Method Path Min role
POST /workspaces/<id>/switch any member
GET /workspaces/<id>/members any member
POST /workspaces/<id>/members admin / owner
DELETE /workspaces/<id>/members/<member_id> admin / owner
PUT /workspaces/<id>/members/<member_id>/role admin / owner

The new require_workspace_role decorator returns 404 for non-members (matching the existing GET /workspaces/<id> convention to avoid tenant-id existence leaks) and 403 for members with insufficient role. Domain logic is reused as-is from TenantService / RegisterService; invites still go through invite_new_member so the Celery activation email fires for newly-invited addresses. Owner is intentionally not assignable through invite or role-update — ownership transfer remains console-only.

CLI — five new commands:

difyctl use workspace <id>            # server-side switch + refresh hosts.yml
difyctl get member [-w <id>] [-o ...]
difyctl create member --email <e> --role <r> [-w <id>]
difyctl delete member <member-id> [-w <id>]
difyctl set member <member-id> --role <r> [-w <id>]

use workspace strictly orders POST /switch → GET /workspaces → saveHosts; any failure aborts with no local mutation so hosts.yml never diverges from the server. get member flags the calling account row with * (matched via hosts.yml.bundle.account.id). --role is client-enum-validated to normal | admin before any HTTP call.

The old difyctl auth use (a pure-local workspace picker) is deleted — its semantics are incompatible with the new server-side switch, so keeping it would only confuse. The "no workspace selected" hint now points at difyctl use workspace <id>.

Tests: 11 cases for the role-gate decorator, 26 for the new endpoints + payload validation, 12 for MembersClient + WorkspacesClient.switch, 21 across the five CLI command runners (including the no-fallback-on-switch-failure invariant). Local: 229/229 backend openapi tests, 681/681 CLI tests, pnpm type-check and pnpm build clean.

Screenshots

N/A — CLI-only.

Checklist

  • This change requires a documentation update, included: Dify Document
  • I understand that this PR may be closed in case there was no previous discussion or issues. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.
  • I ran make lint && make type-check (backend) and cd web && pnpm exec vp staged (frontend) to appease the lint gods

@lin-snow lin-snow requested review from a team, QuantumGhost and laipz8200 as code owners May 18, 2026 16:32
@dosubot dosubot Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label May 18, 2026
@lin-snow lin-snow self-assigned this May 18, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

Pyrefly Diff

base → PR
--- /tmp/pyrefly_base.txt	2026-05-19 11:02:39.922857722 +0000
+++ /tmp/pyrefly_pr.txt	2026-05-19 11:02:30.357853380 +0000
@@ -2182,6 +2182,26 @@
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:49:12
 ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
   --> tests/unit_tests/controllers/openapi/test_workspaces.py:50:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:114:12
+ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:115:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:120:12
+ERROR `in` is not supported between `Literal['GET']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:121:12
+ERROR `in` is not supported between `Literal['POST']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:122:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:127:12
+ERROR `in` is not supported between `Literal['DELETE']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:128:12
+ERROR Object of class `FunctionType` has no attribute `view_class` [missing-attribute]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:133:12
+ERROR `in` is not supported between `Literal['PUT']` and `None` [not-iterable]
+   --> tests/unit_tests/controllers/openapi/test_workspaces_members.py:134:12
+ERROR Object of class `NoneType` has no attribute `json`
+ERROR Object of class `NoneType` has no attribute `json`
 ERROR Cannot index into `Iterable[bytes]` [bad-index]
    --> tests/unit_tests/controllers/service_api/app/test_audio.py:190:16
 ERROR Cannot index into `Response` [bad-index]

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 0.00% 44.40% +44.40%
Strict coverage 0.00% 43.92% +43.92%
Typed symbols 0 22,960 +22,960
Untyped symbols 0 29,064 +29,064
Modules 0 2646 +2,646

lin-snow added 3 commits May 19, 2026 17:02
…ted openapi

Adds five bearer-authed endpoints under /openapi/v1/workspaces/<id>/
(switch, members CRUD, role update) gated by a new
@require_workspace_role decorator that returns 404 for non-members
(matching the existing GET /workspaces/<id> convention so workspace
IDs don't leak across tenants) and 403 for insufficient role.
TenantService / RegisterService domain logic is reused as-is — invites
still go through invite_new_member so the Celery activation email
fires for newly-invited addresses. Owner is intentionally not
assignable through invite or role-update; ownership transfer remains
console-only.

CLI gains five commands:

  difyctl use workspace <id>
  difyctl get member [-w <id>] [-o ...]
  difyctl create member --email <e> --role <r> [-w <id>]
  difyctl delete member <member-id> [-w <id>]
  difyctl set member <member-id> --role <r> [-w <id>]

use workspace strictly orders POST /switch -> GET /workspaces ->
saveHosts; any failure aborts with no local mutation so hosts.yml
never diverges from the server. get member marks the calling account
row with '*' (matched via hosts.yml bundle.account.id). --role is
client-enum-validated to normal|admin before any HTTP call.

The old `difyctl auth use` (a pure-local workspace picker) is
removed — its semantics conflict with server-side switch and keeping
it would only confuse. The "no workspace selected" hint now points
at `difyctl use workspace <id>`.
Inline checks on POST /openapi/v1/workspaces/<id>/members for:
- SaaS subscription members.limit (members.limit_exceeded)
- EE license workspace_members cap (workspace_members.license_exceeded)

Envelope {code, message, hint} on the wire body so CLI error-mapper
can surface structured remediation guidance without edition awareness.
EE per-workspace allow_member_invite policy continues via service-layer
check_workspace_member_invite_permission inside invite_new_member.
Reruns pnpm gen-api-contract and pnpm tree:gen after rebasing onto
upstream/feat/cli (which migrated CLI types to @dify/contracts). Adds
the Member* types to the shared contract package and registers the
new CLI commands (use workspace, create/delete/get/set member) in
the build-time command tree.
@lin-snow lin-snow force-pushed the feat/cli-workspace branch from 508e575 to 0a425bc Compare May 19, 2026 09:16
@github-actions github-actions Bot added the web This relates to changes on the web. label May 19, 2026
…Workspace + simplify _member_response

- invite_url is always set server-side (always-non-null URL build path);
  drop the misleading Optional so generated CLI/SDK types stop forcing
  callers through pointless null checks.
- use/workspace: pickWorkspace was used in one of two adjacent shape
  conversions; inline both for symmetry.
- _member_response: TenantAccountRole and AccountStatus are StrEnums —
  the getattr + `if role else ""` defenses are unreachable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lin-snow lin-snow requested review from wylswz and removed request for a team, QuantumGhost, Yeuoly, crazywoola and laipz8200 May 19, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files. web This relates to changes on the web.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant