fix(app): derive get-app --mode whitelist from listable app types (#37761)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Xiyuan Chen 2026-06-22 21:40:05 -07:00 committed by GitHub
parent 26639e0923
commit b3e5f29421
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 246 additions and 118 deletions

View File

@ -2,7 +2,8 @@
from __future__ import annotations
from typing import Any, Literal
from enum import StrEnum
from typing import Any, Final, Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
@ -13,6 +14,30 @@ from models.model import AppMode
MAX_PAGE_LIMIT = 200
class SupportedAppType(StrEnum):
"""App types the ``app`` usage face (``get app``) lists and filters.
A curated subset of :class:`AppMode`: the real, user-facing app categories.
Excludes runtime-only mode tags that are not standalone apps
(``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the
roster-owned ``agent`` type (surfaced through the roster, not this list).
Members reference ``AppMode.*.value`` so the subset relationship is
type-checked: dropping a member from ``AppMode`` breaks this at import.
This is the single source for the listable set params, filters, and the
generated CLI whitelist all derive from it.
"""
COMPLETION = AppMode.COMPLETION.value
CHAT = AppMode.CHAT.value
ADVANCED_CHAT = AppMode.ADVANCED_CHAT.value
WORKFLOW = AppMode.WORKFLOW.value
AGENT_CHAT = AppMode.AGENT_CHAT.value
SUPPORTED_APP_TYPES: Final[tuple[AppMode, ...]] = tuple(AppMode(t.value) for t in SupportedAppType)
class UsageInfo(BaseModel):
prompt_tokens: int = 0
completion_tokens: int = 0
@ -279,12 +304,12 @@ class AppDescribeQuery(BaseModel):
class AppListQuery(BaseModel):
"""mode is a closed enum."""
"""mode is a closed enum of listable app types."""
workspace_id: UUIDStr
page: int = Field(1, ge=1)
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
mode: AppMode | None = None
mode: SupportedAppType | None = None
name: str | None = Field(None, max_length=200)
@ -335,7 +360,7 @@ class PermittedExternalAppsListQuery(BaseModel):
page: int = Field(1, ge=1)
limit: int = Field(20, ge=1, le=MAX_PAGE_LIMIT)
mode: AppMode | None = None
mode: SupportedAppType | None = None
name: str | None = Field(None, max_length=200)

View File

@ -16,6 +16,7 @@ from controllers.openapi import openapi_ns
from controllers.openapi._contract import accepts, returns
from controllers.openapi._input_schema import EMPTY_INPUT_SCHEMA, build_input_schema, resolve_app_config
from controllers.openapi._models import (
SUPPORTED_APP_TYPES,
AppDescribeInfo,
AppDescribeQuery,
AppDescribeResponse,
@ -37,6 +38,11 @@ from services.app_service import AppListParams, AppService
_ALLOWED_DESCRIBE_FIELDS: frozenset[str] = frozenset({"info", "parameters", "input_schema"})
def _is_listable(app: App) -> bool:
"""Whether the openapi app face exposes this app (curated, listable types only)."""
return app.mode in SUPPORTED_APP_TYPES
_EMPTY_PARAMETERS: dict[str, Any] = {
"opening_statement": None,
"suggested_questions": [],
@ -171,6 +177,8 @@ class AppListApi(Resource):
app: App | None = AppService.get_visible_app_by_id(db.session, str(parsed_uuid))
if app is None or str(app.tenant_id) != workspace_id:
return empty
if not _is_listable(app):
return empty
# Apply RBAC visibility to the UUID fast-path the same way the service
# layer does for paginated queries (id in accessible set OR own app).
if apply_rbac_filter and not access_filter.is_app_accessible(
@ -223,6 +231,7 @@ class AppListApi(Resource):
workspace_name=tenant_name,
)
for r in pagination.items
if _is_listable(r)
]
env = AppListResponse(

View File

@ -80,7 +80,7 @@ User-scoped operations
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| limit | query | | No | integer, <br>**Default:** 20 |
| mode | query | | No | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" |
| mode | query | App types the ``app`` usage face (``get app``) lists and filters. A curated subset of :class:`AppMode`: the real, user-facing app categories. Excludes runtime-only mode tags that are not standalone apps (``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the roster-owned ``agent`` type (surfaced through the roster, not this list). Members reference ``AppMode.*.value`` so the subset relationship is type-checked: dropping a member from ``AppMode`` breaks this at import. This is the single source for the listable set — params, filters, and the generated CLI whitelist all derive from it. | No | string, <br>**Available values:** "advanced-chat", "agent-chat", "chat", "completion", "workflow" |
| name | query | | No | string |
| page | query | | No | integer, <br>**Default:** 1 |
| workspace_id | query | | Yes | string |
@ -318,7 +318,7 @@ Upload a file to use as an input variable when running the app
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| limit | query | | No | integer, <br>**Default:** 20 |
| mode | query | | No | string, <br>**Available values:** "advanced-chat", "agent", "agent-chat", "channel", "chat", "completion", "rag-pipeline", "workflow" |
| mode | query | App types the ``app`` usage face (``get app``) lists and filters. A curated subset of :class:`AppMode`: the real, user-facing app categories. Excludes runtime-only mode tags that are not standalone apps (``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the roster-owned ``agent`` type (surfaced through the roster, not this list). Members reference ``AppMode.*.value`` so the subset relationship is type-checked: dropping a member from ``AppMode`` breaks this at import. This is the single source for the listable set — params, filters, and the generated CLI whitelist all derive from it. | No | string, <br>**Available values:** "advanced-chat", "agent-chat", "chat", "completion", "workflow" |
| name | query | | No | string |
| page | query | | No | integer, <br>**Default:** 1 |
@ -592,12 +592,12 @@ Request body for POST /workspaces/<workspace_id>/apps/imports.
#### AppListQuery
mode is a closed enum.
mode is a closed enum of listable app types.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| limit | integer, <br>**Default:** 20 | | No |
| mode | [AppMode](#appmode) | | No |
| mode | [SupportedAppType](#supportedapptype) | | No |
| name | string | | No |
| page | integer, <br>**Default:** 1 | | No |
| workspace_id | string | | Yes |
@ -922,7 +922,7 @@ Strict (extra='forbid').
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| limit | integer, <br>**Default:** 20 | | No |
| mode | [AppMode](#appmode) | | No |
| mode | [SupportedAppType](#supportedapptype) | | No |
| name | string | | No |
| page | integer, <br>**Default:** 1 | | No |
@ -990,6 +990,24 @@ Pagination for GET /account/sessions. Strict (extra='forbid').
| last_used_at | string | | No |
| prefix | string | | Yes |
#### SupportedAppType
App types the ``app`` usage face (``get app``) lists and filters.
A curated subset of :class:`AppMode`: the real, user-facing app categories.
Excludes runtime-only mode tags that are not standalone apps
(``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the
roster-owned ``agent`` type (surfaced through the roster, not this list).
Members reference ``AppMode.*.value`` so the subset relationship is
type-checked: dropping a member from ``AppMode`` breaks this at import.
This is the single source for the listable set — params, filters, and the
generated CLI whitelist all derive from it.
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| SupportedAppType | string | App types the ``app`` usage face (``get app``) lists and filters. A curated subset of :class:`AppMode`: the real, user-facing app categories. Excludes runtime-only mode tags that are not standalone apps (``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the roster-owned ``agent`` type (surfaced through the roster, not this list). Members reference ``AppMode.*.value`` so the subset relationship is type-checked: dropping a member from ``AppMode`` breaks this at import. This is the single source for the listable set — params, filters, and the generated CLI whitelist all derive from it. | |
#### TaskStopResponse
200 body for POST /apps/<id>/tasks/<task_id>/stop. The handler always returns

View File

@ -0,0 +1,10 @@
"""Shared mode lists for the openapi app-list query tests.
Single source so adding/removing a listable app type is a one-line change
across every query-validator test.
"""
from __future__ import annotations
LISTABLE_MODES = ["completion", "chat", "advanced-chat", "workflow", "agent-chat"]
NON_LISTABLE_MODES = ["rag-pipeline", "channel", "agent"]

View File

@ -4,7 +4,7 @@ Runs against the model directly, not the HTTP layer. Pins:
- defaults match the plan (page=1, limit=20).
- workspace_id is required.
- numeric bounds enforced (page >= 1, limit in [1, MAX_PAGE_LIMIT]).
- mode validates against the AppMode enum.
- mode validates against the SupportedAppType enum (listable app types only).
- name has a length cap.
"""
@ -16,10 +16,14 @@ from pydantic import ValidationError
from controllers.openapi._models import MAX_PAGE_LIMIT
from controllers.openapi.apps import AppListQuery
from ._mode_constants import LISTABLE_MODES, NON_LISTABLE_MODES
WS_ID = "00000000-0000-0000-0000-000000000001"
def test_defaults():
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001"})
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
q = AppListQuery.model_validate({"workspace_id": WS_ID})
assert q.workspace_id == WS_ID
assert q.page == 1
assert q.limit == 20
assert q.mode is None
@ -33,64 +37,71 @@ def test_workspace_id_required():
def test_page_must_be_positive():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": 0})
AppListQuery.model_validate({"workspace_id": WS_ID, "page": 0})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": -1})
AppListQuery.model_validate({"workspace_id": WS_ID, "page": -1})
def test_page_rejects_non_integer_string():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "page": "abc"})
AppListQuery.model_validate({"workspace_id": WS_ID, "page": "abc"})
def test_limit_must_be_positive():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": 0})
AppListQuery.model_validate({"workspace_id": WS_ID, "limit": 0})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": -1})
AppListQuery.model_validate({"workspace_id": WS_ID, "limit": -1})
def test_limit_caps_at_max_page_limit():
# Boundary accepts.
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT})
q = AppListQuery.model_validate({"workspace_id": WS_ID, "limit": MAX_PAGE_LIMIT})
assert q.limit == MAX_PAGE_LIMIT
# Just over rejects.
with pytest.raises(ValidationError):
AppListQuery.model_validate(
{"workspace_id": "00000000-0000-0000-0000-000000000001", "limit": MAX_PAGE_LIMIT + 1}
)
AppListQuery.model_validate({"workspace_id": WS_ID, "limit": MAX_PAGE_LIMIT + 1})
def test_mode_whitelisted_against_app_mode():
# Valid mode passes.
q = AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "chat"})
@pytest.mark.parametrize("mode", LISTABLE_MODES)
def test_mode_accepts_listable_app_types(mode: str):
q = AppListQuery.model_validate({"workspace_id": WS_ID, "mode": mode})
assert q.mode is not None
assert q.mode.value == "chat"
assert q.mode.value == mode
# Invalid mode rejects.
def test_mode_rejects_unknown_value():
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "mode": "not-a-mode"})
AppListQuery.model_validate({"workspace_id": WS_ID, "mode": "not-a-mode"})
@pytest.mark.parametrize("mode", NON_LISTABLE_MODES)
def test_mode_rejects_non_listable_app_modes(mode: str):
"""rag-pipeline (a knowledge Pipeline), channel (unused) and agent (roster-owned)
are AppMode members but not standalone listable apps the `app` face rejects them."""
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": WS_ID, "mode": mode})
def test_name_length_capped():
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 200})
AppListQuery.model_validate({"workspace_id": WS_ID, "name": "x" * 200})
with pytest.raises(ValidationError):
AppListQuery.model_validate({"workspace_id": "00000000-0000-0000-0000-000000000001", "name": "x" * 201})
AppListQuery.model_validate({"workspace_id": WS_ID, "name": "x" * 201})
def test_all_fields_accept_valid_values():
"""Pin the happy-path acceptance for every field in one place."""
q = AppListQuery.model_validate(
{
"workspace_id": "00000000-0000-0000-0000-000000000001",
"workspace_id": WS_ID,
"page": 5,
"limit": 50,
"mode": "workflow",
"name": "search",
}
)
assert q.workspace_id == "00000000-0000-0000-0000-000000000001"
assert q.workspace_id == WS_ID
assert q.page == 5
assert q.limit == 50
assert q.mode is not None

View File

@ -10,9 +10,11 @@ import pytest
from controllers.openapi.apps import ( # pyright: ignore[reportPrivateUsage]
_EMPTY_PARAMETERS,
_is_listable,
parameters_payload,
)
from controllers.service_api.app.error import AppUnavailableError
from models.model import AppMode
def _fake_app(**overrides):
@ -53,3 +55,16 @@ def test_empty_parameters_constant_matches_describe_fallback_shape():
assert _EMPTY_PARAMETERS["opening_statement"] is None
assert _EMPTY_PARAMETERS["file_upload"] is None
assert _EMPTY_PARAMETERS["system_parameters"] == {}
@pytest.mark.parametrize(
"mode",
[AppMode.COMPLETION, AppMode.CHAT, AppMode.ADVANCED_CHAT, AppMode.WORKFLOW, AppMode.AGENT_CHAT],
)
def test_is_listable_accepts_supported_app_types(mode):
assert _is_listable(_fake_app(mode=mode)) is True
@pytest.mark.parametrize("mode", [AppMode.AGENT, AppMode.CHANNEL, AppMode.RAG_PIPELINE])
def test_is_listable_hides_non_app_modes(mode):
assert _is_listable(_fake_app(mode=mode)) is False

View File

@ -13,6 +13,8 @@ from pydantic import ValidationError
from controllers.openapi.apps_permitted_external import PermittedExternalAppsListQuery
from ._mode_constants import NON_LISTABLE_MODES
def test_query_defaults_match_apps_list():
q = PermittedExternalAppsListQuery.model_validate({})
@ -36,11 +38,18 @@ def test_query_rejects_tag():
PermittedExternalAppsListQuery.model_validate({"tag": "prod"})
def test_query_validates_mode_against_app_mode():
def test_query_validates_mode_against_supported_app_type():
with pytest.raises(ValidationError):
PermittedExternalAppsListQuery.model_validate({"mode": "not-a-mode"})
@pytest.mark.parametrize("mode", NON_LISTABLE_MODES)
def test_query_rejects_non_listable_app_modes(mode: str):
"""Non-app runtime modes and roster-owned agent are not listable here."""
with pytest.raises(ValidationError):
PermittedExternalAppsListQuery.model_validate({"mode": mode})
def test_query_clamps_limit_at_max():
with pytest.raises(ValidationError):
PermittedExternalAppsListQuery.model_validate({"limit": 500})

View File

@ -0,0 +1,24 @@
"""Unit tests for SupportedAppType — the listable subset of AppMode that the
openapi `app` face (`get app`) exposes and the CLI `--mode` whitelist derives from.
"""
from __future__ import annotations
from controllers.openapi._models import SUPPORTED_APP_TYPES, SupportedAppType
from models.model import AppMode
def test_supported_app_type_is_the_listable_subset_of_app_mode():
"""SupportedAppType (and the derived SUPPORTED_APP_TYPES tuple) is exactly the
curated, listable subset of AppMode; non-app/runtime modes stay out."""
assert {t.value for t in SupportedAppType} == {
"completion",
"chat",
"advanced-chat",
"workflow",
"agent-chat",
}
assert set(SUPPORTED_APP_TYPES) <= set(AppMode)
assert AppMode.AGENT not in SUPPORTED_APP_TYPES
assert AppMode.RAG_PIPELINE not in SUPPORTED_APP_TYPES
assert AppMode.CHANNEL not in SUPPORTED_APP_TYPES

View File

@ -1,4 +1,4 @@
import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen'
import type { AppDescribeResponse, AppListResponse, SupportedAppType } from '@dify/contracts/api/openapi/types.gen'
import type { AppReader } from './app-reader'
import type { OpenApiClient } from '@/http/orpc'
import type { HttpClient } from '@/http/types'
@ -8,12 +8,12 @@ export type ListQuery = {
readonly workspaceId: string
readonly page?: number
readonly limit?: number
readonly mode?: AppMode | ''
readonly mode?: SupportedAppType | ''
readonly name?: string
}
// An absent or empty mode filter means "any mode" — collapse both to undefined for the query.
export function normalizeMode(mode: AppMode | '' | undefined): AppMode | undefined {
export function normalizeMode(mode: SupportedAppType | '' | undefined): SupportedAppType | undefined {
return mode !== undefined && mode !== '' ? mode : undefined
}

View File

@ -1,4 +1,5 @@
import type { AppMode } from '@dify/contracts/api/openapi/types.gen'
import type { SupportedAppType } from '@dify/contracts/api/openapi/types.gen'
import { zSupportedAppType } from '@dify/contracts/api/openapi/zod.gen'
import { DifyCommand } from '@/commands/_shared/dify-command'
import { httpRetryFlag } from '@/commands/_shared/global-flags'
import { Args, Flags } from '@/framework/flags'
@ -6,16 +7,9 @@ import { OutputFormat, table } from '@/framework/output'
import { agentGuide } from './guide'
import { runGetApp } from './run'
const APP_MODE_VALUES: readonly AppMode[] = [
'advanced-chat',
'agent',
'agent-chat',
'channel',
'chat',
'completion',
'rag-pipeline',
'workflow',
]
// Single source: derived from the backend's listable app types (openapi codegen).
// Adding/removing a listable type is a backend-only change that flows here on regen.
const APP_MODE_VALUES: readonly SupportedAppType[] = zSupportedAppType.options
export default class GetApp extends DifyCommand {
static override description = 'List apps or describe one app\'s basic info'
@ -56,7 +50,7 @@ export default class GetApp extends DifyCommand {
allWorkspaces: flags['all-workspaces'],
page: flags.page,
limitRaw: flags.limit,
mode: flags.mode as AppMode | undefined,
mode: flags.mode as SupportedAppType | undefined,
name: flags.name,
format,
}, { active: ctx.active, http: ctx.http, io: ctx.io })

View File

@ -0,0 +1,13 @@
import { zSupportedAppType } from '@dify/contracts/api/openapi/zod.gen'
import { describe, expect, it } from 'vitest'
// The `get app --mode` whitelist is derived from this generated enum (see index.ts).
// These pins guard the original bug: the CLI must not advertise modes the backend
// rejects (rag-pipeline, channel) or modes that aren't listable here (agent).
describe('get app --mode whitelist', () => {
it('is exactly the listable app types', () => {
expect([...zSupportedAppType.options].sort()).toEqual(
['advanced-chat', 'agent-chat', 'chat', 'completion', 'workflow'],
)
})
})

View File

@ -1,4 +1,4 @@
import type { AppDescribeResponse, AppListResponse, AppMode } from '@dify/contracts/api/openapi/types.gen'
import type { AppDescribeResponse, AppListResponse, AppMode, SupportedAppType } from '@dify/contracts/api/openapi/types.gen'
import type { AppReader } from '@/api/app-reader'
import type { ActiveContext } from '@/auth/hosts'
import type { HttpClient } from '@/http/types'
@ -20,7 +20,7 @@ export type GetAppOptions = {
readonly allWorkspaces?: boolean
readonly page?: number
readonly limitRaw?: string
readonly mode?: AppMode
readonly mode?: SupportedAppType
readonly name?: string
readonly format?: string
}

View File

@ -177,6 +177,19 @@ describe('E2E / difyctl get app (list)', () => {
expect(result.exitCode, '--mode chatbot should cause non-zero exit').not.toBe(0)
})
// Regression: rag-pipeline (a knowledge Pipeline), channel (unused) and agent
// (roster-owned) are AppMode members but not listable app types. The old CLI
// whitelist advertised rag-pipeline/channel, so the CLI forwarded them and the
// server replied 400. The whitelist now derives from SupportedAppType, so the
// CLI rejects them before any HTTP call.
it.each(['rag-pipeline', 'channel', 'agent'])(
'[P0] non-listable mode %s is intercepted client-side',
async (mode) => {
const result = await fx.r(['get', 'app', '--mode', mode])
expect(result.exitCode, `--mode ${mode} should be rejected client-side`).not.toBe(0)
},
)
// ── workspace override ────────────────────────────────────────────────────
it('[P0] -w overrides the default workspace', async () => {

View File

@ -73,7 +73,7 @@ export type AppInfo = {
export type AppListQuery = {
limit?: number
mode?: AppMode | null
mode?: SupportedAppType | null
name?: string | null
page?: number
workspace_id: string
@ -354,7 +354,7 @@ export type Package = {
export type PermittedExternalAppsListQuery = {
limit?: number
mode?: AppMode | null
mode?: SupportedAppType | null
name?: string | null
page?: number
}
@ -405,6 +405,8 @@ export type SessionRow = {
prefix: string
}
export type SupportedAppType = 'advanced-chat' | 'agent-chat' | 'chat' | 'completion' | 'workflow'
export type TaskStopResponse = {
result: 'success'
}
@ -589,15 +591,7 @@ export type GetAppsData = {
path?: never
query: {
limit?: number
mode?:
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'channel'
| 'chat'
| 'completion'
| 'rag-pipeline'
| 'workflow'
mode?: 'advanced-chat' | 'agent-chat' | 'chat' | 'completion' | 'workflow'
name?: string
page?: number
workspace_id: string
@ -905,15 +899,7 @@ export type GetPermittedExternalAppsData = {
path?: never
query?: {
limit?: number
mode?:
| 'advanced-chat'
| 'agent'
| 'agent-chat'
| 'channel'
| 'chat'
| 'completion'
| 'rag-pipeline'
| 'workflow'
mode?: 'advanced-chat' | 'agent-chat' | 'chat' | 'completion' | 'workflow'
name?: string
page?: number
}

View File

@ -104,19 +104,6 @@ export const zAppMode = z.enum([
'workflow',
])
/**
* AppListQuery
*
* mode is a closed enum.
*/
export const zAppListQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: zAppMode.nullish(),
name: z.string().max(200).nullish(),
page: z.int().gte(1).optional().default(1),
workspace_id: z.string(),
})
/**
* AppListRow
*/
@ -452,18 +439,6 @@ export const zPackage = z.object({
version: z.string().nullish(),
})
/**
* PermittedExternalAppsListQuery
*
* Strict (extra='forbid').
*/
export const zPermittedExternalAppsListQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: zAppMode.nullish(),
name: z.string().max(200).nullish(),
page: z.int().gte(1).optional().default(1),
})
/**
* PermittedExternalAppsListResponse
*/
@ -526,6 +501,54 @@ export const zSessionListResponse = z.object({
total: z.int(),
})
/**
* SupportedAppType
*
* App types the ``app`` usage face (``get app``) lists and filters.
*
* A curated subset of :class:`AppMode`: the real, user-facing app categories.
* Excludes runtime-only mode tags that are not standalone apps
* (``rag-pipeline`` is a knowledge ``Pipeline``; ``channel`` is unused) and the
* roster-owned ``agent`` type (surfaced through the roster, not this list).
*
* Members reference ``AppMode.*.value`` so the subset relationship is
* type-checked: dropping a member from ``AppMode`` breaks this at import.
* This is the single source for the listable set params, filters, and the
* generated CLI whitelist all derive from it.
*/
export const zSupportedAppType = z.enum([
'advanced-chat',
'agent-chat',
'chat',
'completion',
'workflow',
])
/**
* AppListQuery
*
* mode is a closed enum of listable app types.
*/
export const zAppListQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: zSupportedAppType.nullish(),
name: z.string().max(200).nullish(),
page: z.int().gte(1).optional().default(1),
workspace_id: z.string(),
})
/**
* PermittedExternalAppsListQuery
*
* Strict (extra='forbid').
*/
export const zPermittedExternalAppsListQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: zSupportedAppType.nullish(),
name: z.string().max(200).nullish(),
page: z.int().gte(1).optional().default(1),
})
/**
* TaskStopResponse
*
@ -698,18 +721,7 @@ export const zDeleteAccountSessionsBySessionIdResponse = zRevokeResponse
export const zGetAppsQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: z
.enum([
'advanced-chat',
'agent',
'agent-chat',
'channel',
'chat',
'completion',
'rag-pipeline',
'workflow',
])
.optional(),
mode: z.enum(['advanced-chat', 'agent-chat', 'chat', 'completion', 'workflow']).optional(),
name: z.string().max(200).optional(),
page: z.int().gte(1).optional().default(1),
workspace_id: z.string(),
@ -862,18 +874,7 @@ export const zPostOauthDeviceTokenResponse = zDeviceTokenResponse
export const zGetPermittedExternalAppsQuery = z.object({
limit: z.int().gte(1).lte(200).optional().default(20),
mode: z
.enum([
'advanced-chat',
'agent',
'agent-chat',
'channel',
'chat',
'completion',
'rag-pipeline',
'workflow',
])
.optional(),
mode: z.enum(['advanced-chat', 'agent-chat', 'chat', 'completion', 'workflow']).optional(),
name: z.string().max(200).optional(),
page: z.int().gte(1).optional().default(1),
})