From 1502a57381237b319bc426c9e8a1f392dd42f1ab Mon Sep 17 00:00:00 2001 From: Xiyuan Chen <52963600+GareArc@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:35:18 -0700 Subject: [PATCH] feat(api,cli): strict UUID validation for app-id and workspace-id (#37212) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/openapi/_models.py | 20 ++--------- api/controllers/openapi/apps.py | 2 +- api/controllers/openapi/auth/prepare.py | 4 +++ api/libs/helper.py | 12 +++++++ api/openapi/markdown/openapi-swagger.md | 2 -- .../controllers/openapi/auth/test_prepare.py | 17 ++++++--- .../openapi/test_app_list_query.py | 36 ++++++++++--------- cli/src/api/app-meta.test.ts | 20 +++++------ cli/src/api/app-meta.ts | 4 +-- cli/src/api/apps.test.ts | 10 +++--- cli/src/api/apps.ts | 5 +-- cli/src/commands/describe/app/index.ts | 13 ++++--- cli/src/commands/describe/app/run.ts | 6 +--- cli/src/commands/get/app/run.ts | 2 +- cli/src/commands/resume/app/run.ts | 8 ++--- cli/src/commands/run/app/run.ts | 10 ++---- cli/src/workspace/resolver.ts | 16 +++++++-- .../generated/api/openapi/types.gen.ts | 2 -- .../generated/api/openapi/zod.gen.ts | 2 -- 19 files changed, 99 insertions(+), 92 deletions(-) diff --git a/api/controllers/openapi/_models.py b/api/controllers/openapi/_models.py index d661f5640f..d01f023cbf 100644 --- a/api/controllers/openapi/_models.py +++ b/api/controllers/openapi/_models.py @@ -6,7 +6,7 @@ from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, field_validator -from libs.helper import EmailStr, UUIDStrOrEmpty, uuid_value +from libs.helper import EmailStr, UUIDStr, UUIDStrOrEmpty, uuid_value from models.model import AppMode # Server-side cap on `limit` query param for /openapi/v1/* list endpoints. @@ -262,22 +262,6 @@ class AppDescribeQuery(BaseModel): model_config = ConfigDict(extra="forbid") fields: set[str] | None = Field(default=None, json_schema_extra=_csv_string_query_schema) - workspace_id: str | None = None - - @field_validator("workspace_id", mode="before") - @classmethod - def _validate_workspace_id(cls, v: object) -> str | None: - if v is None or v == "": - return None - if not isinstance(v, str): - raise ValueError("workspace_id must be a string") - try: - import uuid as _uuid - - _uuid.UUID(v) - except ValueError: - raise ValueError("workspace_id must be a valid UUID") - return v @field_validator("fields", mode="before") @classmethod @@ -297,7 +281,7 @@ class AppDescribeQuery(BaseModel): class AppListQuery(BaseModel): """mode is a closed enum.""" - workspace_id: str + workspace_id: UUIDStr page: int = Field(1, ge=1) limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT) mode: AppMode | None = None diff --git a/api/controllers/openapi/apps.py b/api/controllers/openapi/apps.py index cd44014faf..9520d6b097 100644 --- a/api/controllers/openapi/apps.py +++ b/api/controllers/openapi/apps.py @@ -97,7 +97,7 @@ class AppDescribeApi(AppReadResource): except ValidationError as exc: raise UnprocessableEntity(exc.json()) - app = self._load(app_id, workspace_id=query.workspace_id) + app = self._load(app_id) requested = query.fields want_info = requested is None or "info" in requested diff --git a/api/controllers/openapi/auth/prepare.py b/api/controllers/openapi/auth/prepare.py index 5ad8d0647b..3826f2c33c 100644 --- a/api/controllers/openapi/auth/prepare.py +++ b/api/controllers/openapi/auth/prepare.py @@ -19,6 +19,10 @@ def load_app(data: AuthData) -> None: if data.app is not None: return app_id = data.path_params["app_id"] + try: + uuid.UUID(app_id) + except ValueError: + raise NotFound("app not found") app = AppService.get_app_by_id(db.session, app_id) if not app or app.status != "normal": raise NotFound("app not found") diff --git a/api/libs/helper.py b/api/libs/helper.py index a31b546624..3f27eac516 100644 --- a/api/libs/helper.py +++ b/api/libs/helper.py @@ -273,6 +273,18 @@ def parse_uuid_str_or_none(value: str | None) -> str | None: UUIDStrOrEmpty = Annotated[str, AfterValidator(normalize_uuid)] +def _strict_uuid(value: str | UUID) -> str: + if not value: + raise ValueError("must be a non-empty valid UUID") + try: + return uuid_value(value) + except ValueError as exc: + raise ValueError("must be a valid UUID") from exc + + +UUIDStr = Annotated[str, AfterValidator(_strict_uuid)] + + def alphanumeric(value: str): # check if the value is alphanumeric and underlined if re.match(r"^[a-zA-Z0-9_]+$", value): diff --git a/api/openapi/markdown/openapi-swagger.md b/api/openapi/markdown/openapi-swagger.md index 150237b3c5..bbeb9f7f2a 100644 --- a/api/openapi/markdown/openapi-swagger.md +++ b/api/openapi/markdown/openapi-swagger.md @@ -112,7 +112,6 @@ User-scoped operations | ---- | ---------- | ----------- | -------- | ------ | | app_id | path | | Yes | string | | fields | query | | No | string | -| workspace_id | query | | No | string | ##### Responses @@ -454,7 +453,6 @@ Empty / omitted → all blocks. Unknown member → ValidationError → 422. | Name | Type | Description | Required | | ---- | ---- | ----------- | -------- | | fields | string | | No | -| workspace_id | string | | No | #### AppDescribeResponse diff --git a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py index dd061ebf48..093a53ba0d 100644 --- a/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py +++ b/api/tests/unit_tests/controllers/openapi/auth/test_prepare.py @@ -32,18 +32,27 @@ def _make_auth_data(**kwargs) -> AuthData: return data +_VALID_APP_UUID = "00000000-0000-0000-0000-000000000001" + + def test_load_app_writes_app_to_data(): app = MagicMock() app.status = "normal" app.enable_api = True - data = _make_auth_data(path_params={"app_id": "abc"}) + data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID}) with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app): load_app(data) assert data.app is app +def test_load_app_raises_not_found_for_non_uuid_app_id(): + data = _make_auth_data(path_params={"app_id": "not-a-uuid"}) + with pytest.raises(NotFound): + load_app(data) + + def test_load_app_raises_not_found_when_missing(): - data = _make_auth_data(path_params={"app_id": "missing"}) + data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID}) with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=None): with pytest.raises(NotFound): load_app(data) @@ -52,7 +61,7 @@ def test_load_app_raises_not_found_when_missing(): def test_load_app_raises_not_found_when_not_normal(): app = MagicMock() app.status = "archived" - data = _make_auth_data(path_params={"app_id": "abc"}) + data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID}) with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app): with pytest.raises(NotFound): load_app(data) @@ -62,7 +71,7 @@ def test_load_app_stashes_app_even_when_api_disabled(): app = MagicMock() app.status = "normal" app.enable_api = False - data = _make_auth_data(path_params={"app_id": "abc"}) + data = _make_auth_data(path_params={"app_id": _VALID_APP_UUID}) with patch("controllers.openapi.auth.prepare.AppService.get_app_by_id", return_value=app): load_app(data) assert data.app is app diff --git a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py index f7e8e9c73a..9d207b1930 100644 --- a/api/tests/unit_tests/controllers/openapi/test_app_list_query.py +++ b/api/tests/unit_tests/controllers/openapi/test_app_list_query.py @@ -18,8 +18,8 @@ from controllers.openapi.apps import AppListQuery def test_defaults(): - q = AppListQuery.model_validate({"workspace_id": "ws-1"}) - assert q.workspace_id == "ws-1" + q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001"}) + assert q.workspace_id == "00000000-0000-0000-0000-000000000001" assert q.page == 1 assert q.limit == 20 assert q.mode is None @@ -34,61 +34,63 @@ def test_workspace_id_required(): def test_page_must_be_positive(): with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "page": 0}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": 0}) with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "page": -1}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": -1}) def test_page_rejects_non_integer_string(): with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "page": "abc"}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": "abc"}) def test_limit_must_be_positive(): with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "limit": 0}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": 0}) with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "limit": -1}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": -1}) def test_limit_caps_at_max_page_limit(): # Boundary accepts. - q = AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT}) + q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT}) assert q.limit == MAX_PAGE_LIMIT # Just over rejects. with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "limit": MAX_PAGE_LIMIT + 1}) + AppListQuery.model_validate( + {"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT + 1} + ) def test_mode_whitelisted_against_app_mode(): # Valid mode passes. - q = AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "chat"}) + q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "chat"}) assert q.mode is not None assert q.mode.value == "chat" # Invalid mode rejects. with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "mode": "not-a-mode"}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "not-a-mode"}) def test_name_length_capped(): - AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 200}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 200}) with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "name": "x" * 201}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201}) def test_tag_length_capped(): - AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 100}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 100}) with pytest.raises(ValidationError): - AppListQuery.model_validate({"workspace_id": "ws-1", "tag": "x" * 101}) + AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "tag": "x" * 101}) def test_all_fields_accept_valid_values(): """Pin the happy-path acceptance for every field in one place.""" q = AppListQuery.model_validate( { - "workspace_id": "ws-1", + "workspace_id": "00000000-0000-0000-0000-000000000001", "page": 5, "limit": 50, "mode": "workflow", @@ -96,7 +98,7 @@ def test_all_fields_accept_valid_values(): "tag": "prod", } ) - assert q.workspace_id == "ws-1" + assert q.workspace_id == "00000000-0000-0000-0000-000000000001" assert q.page == 5 assert q.limit == 50 assert q.mode is not None diff --git a/cli/src/api/app-meta.test.ts b/cli/src/api/app-meta.test.ts index ff11741f05..ab7ab8a6ae 100644 --- a/cli/src/api/app-meta.test.ts +++ b/cli/src/api/app-meta.test.ts @@ -37,11 +37,11 @@ describe('AppMetaClient', () => { const spy = vi.spyOn(apps, 'describe') const client = new AppMetaClient({ apps, host: mock.url, cache }) - const m1 = await client.get('app-1', 'ws-1', [FieldInfo]) + const m1 = await client.get('app-1', [FieldInfo]) expect(m1.info?.id).toBe('app-1') expect(spy).toHaveBeenCalledTimes(1) - const m2 = await client.get('app-1', 'ws-1', [FieldInfo]) + const m2 = await client.get('app-1', [FieldInfo]) expect(m2.info?.id).toBe('app-1') expect(spy).toHaveBeenCalledTimes(1) }) @@ -52,10 +52,10 @@ describe('AppMetaClient', () => { const spy = vi.spyOn(apps, 'describe') const client = new AppMetaClient({ apps, host: mock.url, cache }) - await client.get('app-1', 'ws-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(1) - const full = await client.get('app-1', 'ws-1', [FieldInfo, FieldParameters]) + const full = await client.get('app-1', [FieldInfo, FieldParameters]) expect(spy).toHaveBeenCalledTimes(2) expect(full.coveredFields.has(FieldParameters)).toBe(true) }) @@ -66,11 +66,11 @@ describe('AppMetaClient', () => { const spy = vi.spyOn(apps, 'describe') const client = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:00Z') }) - await client.get('app-1', 'ws-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(1) const client2 = new AppMetaClient({ apps, host: mock.url, cache, now: () => new Date('2026-05-09T00:00:01Z') }) - await client2.get('app-1', 'ws-1', [FieldInfo]) + await client2.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(2) }) @@ -80,11 +80,11 @@ describe('AppMetaClient', () => { const spy = vi.spyOn(apps, 'describe') const client = new AppMetaClient({ apps, host: mock.url, cache }) - await client.get('app-1', 'ws-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(1) await client.invalidate('app-1') - await client.get('app-1', 'ws-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(2) }) @@ -93,8 +93,8 @@ describe('AppMetaClient', () => { const spy = vi.spyOn(apps, 'describe') const client = new AppMetaClient({ apps, host: mock.url }) - await client.get('app-1', 'ws-1', [FieldInfo]) - await client.get('app-1', 'ws-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) + await client.get('app-1', [FieldInfo]) expect(spy).toHaveBeenCalledTimes(2) }) }) diff --git a/cli/src/api/app-meta.ts b/cli/src/api/app-meta.ts index 854c9b0eac..c2ca9a5aa9 100644 --- a/cli/src/api/app-meta.ts +++ b/cli/src/api/app-meta.ts @@ -23,12 +23,12 @@ export class AppMetaClient { this.now = opts.now ?? (() => new Date()) } - async get(appId: string, workspaceId: string, fields: readonly AppMetaFieldKey[] = []): Promise { + async get(appId: string, fields: readonly AppMetaFieldKey[] = []): Promise { const cached = this.cache?.get(this.host, appId) if (cached !== undefined && this.cache?.isFresh(cached, this.now()) === true && covers(cached.meta, fields)) return cached.meta - const resp = await this.apps.describe(appId, workspaceId, fields.length === 0 ? undefined : fields) + const resp = await this.apps.describe(appId, fields.length === 0 ? undefined : fields) const fresh = fromDescribe(resp, fields) const merged = cached !== undefined && this.cache?.isFresh(cached, this.now()) === true ? mergeMeta(cached.meta, fresh) diff --git a/cli/src/api/apps.test.ts b/cli/src/api/apps.test.ts index e7d4da93e1..68e7bcc86a 100644 --- a/cli/src/api/apps.test.ts +++ b/cli/src/api/apps.test.ts @@ -86,14 +86,14 @@ describe('AppsClient.describe', () => { await stub?.stop() }) - it('hits /apps//describe, sends workspace_id, omits fields when none given', async () => { + it('hits /apps//describe, omits workspace_id and fields when not given', async () => { stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap)) - const res = await makeClient(stub.url).describe('app-1', 'ws-1') + const res = await makeClient(stub.url).describe('app-1') expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app-1/describe') const q = queryOf(stub.captured.url) - expect(q.get('workspace_id')).toBe('ws-1') + expect(q.has('workspace_id')).toBe(false) expect(q.has('fields')).toBe(false) expect(res.info?.id).toBe('app-1') }) @@ -101,7 +101,7 @@ describe('AppsClient.describe', () => { it('joins fields with commas', async () => { stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap)) - await makeClient(stub.url).describe('app-1', 'ws-1', ['parameters', 'input_schema']) + await makeClient(stub.url).describe('app-1', ['parameters', 'input_schema']) expect(queryOf(stub.captured.url).get('fields')).toBe('parameters,input_schema') }) @@ -109,7 +109,7 @@ describe('AppsClient.describe', () => { it('URL-encodes the app id', async () => { stub = await startStubServer(cap => jsonResponder(200, DESCRIBE_BODY, cap)) - await makeClient(stub.url).describe('app/with space', 'ws-1') + await makeClient(stub.url).describe('app/with space') expect(stub.captured.url?.split('?')[0]).toBe('/openapi/v1/apps/app%2Fwith%20space/describe') }) diff --git a/cli/src/api/apps.ts b/cli/src/api/apps.ts index 5932dd936b..40bb5c8005 100644 --- a/cli/src/api/apps.ts +++ b/cli/src/api/apps.ts @@ -32,13 +32,10 @@ export class AppsClient { }) } - async describe(appId: string, workspaceId: string, fields?: readonly string[]): Promise { + async describe(appId: string, fields?: readonly string[]): Promise { return this.orpc.apps.byAppId.describe.get({ params: { app_id: appId }, query: { - workspace_id: workspaceId, - // The backend parses a comma-separated string (validator splits on ','); the contract - // types `fields` as a string accordingly, so join here rather than send an array. fields: fields !== undefined && fields.length > 0 ? fields.join(',') : undefined, }, }) diff --git a/cli/src/commands/describe/app/index.ts b/cli/src/commands/describe/app/index.ts index 49ac206f7c..1a5beb386b 100644 --- a/cli/src/commands/describe/app/index.ts +++ b/cli/src/commands/describe/app/index.ts @@ -1,7 +1,10 @@ import { DifyCommand } from '@/commands/_shared/dify-command' import { httpRetryFlag } from '@/commands/_shared/global-flags' +import { BaseError } from '@/errors/base' +import { ErrorCode } from '@/errors/codes' import { Args, Flags } from '@/framework/flags' import { formatted, OutputFormat } from '@/framework/output' +import { isValidUuid } from '@/workspace/resolver' import { agentGuide } from './guide' import { runDescribeApp } from './run' @@ -9,13 +12,13 @@ export default class DescribeApp extends DifyCommand { static override description = 'Describe a single app (kubectl-describe-style)' static override examples = [ - '<%= config.bin %> describe app app-1', - '<%= config.bin %> describe app app-1 -o json', - '<%= config.bin %> describe app app-1 --refresh', + '<%= config.bin %> describe app ', + '<%= config.bin %> describe app -o json', + '<%= config.bin %> describe app --refresh', ] static override args = { - id: Args.string({ description: 'app id', required: true }), + id: Args.string({ description: 'app UUID', required: true }), } static override flags = { @@ -27,6 +30,8 @@ export default class DescribeApp extends DifyCommand { async run(argv: string[]) { const { args, flags } = this.parse(DescribeApp, argv) + if (!isValidUuid(args.id)) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `${JSON.stringify(args.id)} is not a valid app UUID` }) const format = flags.output const ctx = await this.authedCtx({ retryFlag: flags['http-retry'], withCache: true, format }) return formatted({ diff --git a/cli/src/commands/describe/app/run.ts b/cli/src/commands/describe/app/run.ts index 7eebf63cbf..8af2a5dc44 100644 --- a/cli/src/commands/describe/app/run.ts +++ b/cli/src/commands/describe/app/run.ts @@ -4,11 +4,9 @@ import type { HttpClient } from '@/http/types' import type { IOStreams } from '@/sys/io/streams' import { AppMetaClient } from '@/api/app-meta' import { AppsClient } from '@/api/apps' -import { getEnv } from '@/sys/index' import { runWithSpinner } from '@/sys/io/spinner' import { nullStreams } from '@/sys/io/streams' import { FieldInfo, FieldInputSchema, FieldParameters } from '@/types/app-meta' -import { resolveWorkspaceId } from '@/workspace/resolver' import { AppDescribeOutput } from './handlers.js' export type DescribeAppOptions = { @@ -28,8 +26,6 @@ export type DescribeAppDeps = { } export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeAppDeps): Promise { - const env = deps.envLookup ?? getEnv - const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) const apps = new AppsClient(deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) const io = deps.io ?? nullStreams() @@ -38,7 +34,7 @@ export async function runDescribeApp(opts: DescribeAppOptions, deps: DescribeApp async () => { if (opts.refresh === true) await meta.invalidate(opts.appId) - return meta.get(opts.appId, wsId, [FieldInfo, FieldParameters, FieldInputSchema]) + return meta.get(opts.appId, [FieldInfo, FieldParameters, FieldInputSchema]) }, ) return new AppDescribeOutput(result) diff --git a/cli/src/commands/get/app/run.ts b/cli/src/commands/get/app/run.ts index ba27c55f14..940f0ac44e 100644 --- a/cli/src/commands/get/app/run.ts +++ b/cli/src/commands/get/app/run.ts @@ -59,7 +59,7 @@ export async function runGetApp(opts: GetAppOptions, deps: GetAppDeps): Promise< if (opts.appId !== undefined && opts.appId !== '') { const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) const wsName = workspaceNameForId(deps.active, wsId) - const desc = await apps.describe(opts.appId, wsId, ['info']) + const desc = await apps.describe(opts.appId, ['info']) return describeToEnvelope(desc, wsId, wsName) } const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) diff --git a/cli/src/commands/resume/app/run.ts b/cli/src/commands/resume/app/run.ts index 5385d50f77..81eb4e01f0 100644 --- a/cli/src/commands/resume/app/run.ts +++ b/cli/src/commands/resume/app/run.ts @@ -8,10 +8,9 @@ import { AppRunClient } from '@/api/app-run' import { AppsClient } from '@/api/apps' import { pickStrategy } from '@/commands/run/app/_strategies/index' import { RUN_MODES } from '@/commands/run/app/handlers' -import { getEnv, processExit } from '@/sys/index' +import { processExit } from '@/sys/index' import { colorEnabled, colorScheme } from '@/sys/io/color' import { FieldInfo } from '@/types/app-meta' -import { resolveWorkspaceId } from '@/workspace/resolver' export type ResumeAppOptions = { readonly appId: string @@ -76,12 +75,9 @@ async function resolveInputs( } export async function resumeApp(opts: ResumeAppOptions, deps: ResumeAppDeps): Promise { - const env = deps.envLookup ?? getEnv - const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) - const apps = new AppsClient(deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) - const m = await meta.get(opts.appId, wsId, [FieldInfo]) + const m = await meta.get(opts.appId, [FieldInfo]) const mode = m.info?.mode ?? RUN_MODES.Workflow const runClient = new AppRunClient(deps.http) diff --git a/cli/src/commands/run/app/run.ts b/cli/src/commands/run/app/run.ts index bb06a6bdf3..ada582b48b 100644 --- a/cli/src/commands/run/app/run.ts +++ b/cli/src/commands/run/app/run.ts @@ -9,9 +9,8 @@ import { FileUploadClient } from '@/api/file-upload' import { pickStrategy } from '@/commands/run/app/_strategies/index' import { BaseError, HttpClientError } from '@/errors/base' import { ErrorCode } from '@/errors/codes' -import { getEnv, processExit } from '@/sys/index' +import { processExit } from '@/sys/index' import { FieldInfo } from '@/types/app-meta' -import { resolveWorkspaceId } from '@/workspace/resolver' import { resolveFileInputs } from './file-flags.js' import { RUN_MODES } from './handlers.js' @@ -78,13 +77,11 @@ async function resolveInputs( } export async function runApp(opts: RunAppOptions, deps: RunAppDeps): Promise { - const env = deps.envLookup ?? getEnv - const wsId = resolveWorkspaceId({ flag: opts.workspace, env: env('DIFY_WORKSPACE_ID'), active: deps.active }) const apps = new AppsClient(deps.http) const meta = new AppMetaClient({ apps, host: deps.host, cache: deps.cache }) try { - await executeRun(opts, deps, meta, wsId) + await executeRun(opts, deps, meta) } catch (err) { if (err instanceof HttpClientError && err.httpStatus === 422) { @@ -99,9 +96,8 @@ async function executeRun( opts: RunAppOptions, deps: RunAppDeps, meta: AppMetaClient, - wsId: string, ): Promise { - const m = await meta.get(opts.appId, wsId, [FieldInfo]) + const m = await meta.get(opts.appId, [FieldInfo]) const mode = m.info?.mode ?? '' if (mode === '') throw new Error(`app ${opts.appId}: mode missing from /describe`) diff --git a/cli/src/workspace/resolver.ts b/cli/src/workspace/resolver.ts index 2f2ad7a212..40af50b38b 100644 --- a/cli/src/workspace/resolver.ts +++ b/cli/src/workspace/resolver.ts @@ -8,11 +8,23 @@ export type WorkspaceResolveInputs = { readonly active?: ActiveContext } +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + +export function isValidUuid(v: string): boolean { + return UUID_RE.test(v) +} + export function resolveWorkspaceId(inputs: WorkspaceResolveInputs): string { - if (truthy(inputs.flag)) + if (truthy(inputs.flag)) { + if (!isValidUuid(inputs.flag)) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `--workspace value ${JSON.stringify(inputs.flag)} is not a valid UUID` }) return inputs.flag - if (truthy(inputs.env)) + } + if (truthy(inputs.env)) { + if (!isValidUuid(inputs.env)) + throw new BaseError({ code: ErrorCode.UsageInvalidFlag, message: `DIFY_WORKSPACE_ID value ${JSON.stringify(inputs.env)} is not a valid UUID` }) return inputs.env + } const ctx = inputs.active?.ctx if (ctx !== undefined) { if (truthy(ctx.workspace?.id)) diff --git a/packages/contracts/generated/api/openapi/types.gen.ts b/packages/contracts/generated/api/openapi/types.gen.ts index be36ccafd1..ccf98cdf52 100644 --- a/packages/contracts/generated/api/openapi/types.gen.ts +++ b/packages/contracts/generated/api/openapi/types.gen.ts @@ -33,7 +33,6 @@ export type AppDescribeInfo = { export type AppDescribeQuery = { fields?: string - workspace_id?: string | null } export type AppDescribeResponse = { @@ -445,7 +444,6 @@ export type GetAppsByAppIdDescribeData = { } query?: { fields?: string - workspace_id?: string } url: '/apps/{app_id}/describe' } diff --git a/packages/contracts/generated/api/openapi/zod.gen.ts b/packages/contracts/generated/api/openapi/zod.gen.ts index ca09b116dd..f34d6f4bb8 100644 --- a/packages/contracts/generated/api/openapi/zod.gen.ts +++ b/packages/contracts/generated/api/openapi/zod.gen.ts @@ -20,7 +20,6 @@ export const zAccountPayload = z.object({ */ export const zAppDescribeQuery = z.object({ fields: z.string().optional(), - workspace_id: z.string().nullish(), }) /** @@ -533,7 +532,6 @@ export const zGetAppsByAppIdDescribePath = z.object({ export const zGetAppsByAppIdDescribeQuery = z.object({ fields: z.string().optional(), - workspace_id: z.string().optional(), }) /**