diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 8cec89499..085bbee05 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -165,9 +165,71 @@ } ] }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -198,6 +260,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -235,6 +306,130 @@ ], "type": "object" }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" + }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index ad1342d12..102baf2ff 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -10271,6 +10271,48 @@ }, "type": "object" }, + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppConfig": { "properties": { "disabled_reason": { @@ -10300,6 +10342,26 @@ "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -10330,6 +10392,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -10369,6 +10440,130 @@ "title": "AppListUpdatedNotification", "type": "object" }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/v2/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/v2/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" + }, "AppsConfig": { "type": "object" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json index 1638472c4..0813ed6f5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppListUpdatedNotification.json @@ -1,9 +1,71 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -34,6 +96,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -55,6 +126,130 @@ "name" ], "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" } }, "description": "EXPERIMENTAL - notification emitted when the app list changes.", diff --git a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json index cec3da5cc..4697b34e1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/AppsListResponse.json @@ -1,9 +1,71 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { + "AppBranding": { + "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", + "properties": { + "category": { + "type": [ + "string", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "isDiscoverableApp": { + "type": "boolean" + }, + "privacyPolicy": { + "type": [ + "string", + "null" + ] + }, + "termsOfService": { + "type": [ + "string", + "null" + ] + }, + "website": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "isDiscoverableApp" + ], + "type": "object" + }, "AppInfo": { "description": "EXPERIMENTAL - app metadata returned by app-list APIs.", "properties": { + "appMetadata": { + "anyOf": [ + { + "$ref": "#/definitions/AppMetadata" + }, + { + "type": "null" + } + ] + }, + "branding": { + "anyOf": [ + { + "$ref": "#/definitions/AppBranding" + }, + { + "type": "null" + } + ] + }, "description": { "type": [ "string", @@ -34,6 +96,15 @@ "description": "Whether this app is enabled in config.toml. Example: ```toml [apps.bad_app] enabled = false ```", "type": "boolean" }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": [ + "object", + "null" + ] + }, "logoUrl": { "type": [ "string", @@ -55,6 +126,130 @@ "name" ], "type": "object" + }, + "AppMetadata": { + "properties": { + "categories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "developer": { + "type": [ + "string", + "null" + ] + }, + "firstPartyRequiresInstall": { + "type": [ + "boolean", + "null" + ] + }, + "firstPartyType": { + "type": [ + "string", + "null" + ] + }, + "review": { + "anyOf": [ + { + "$ref": "#/definitions/AppReview" + }, + { + "type": "null" + } + ] + }, + "screenshots": { + "items": { + "$ref": "#/definitions/AppScreenshot" + }, + "type": [ + "array", + "null" + ] + }, + "seoDescription": { + "type": [ + "string", + "null" + ] + }, + "showInComposerWhenUnlinked": { + "type": [ + "boolean", + "null" + ] + }, + "subCategories": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "version": { + "type": [ + "string", + "null" + ] + }, + "versionId": { + "type": [ + "string", + "null" + ] + }, + "versionNotes": { + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "AppReview": { + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ], + "type": "object" + }, + "AppScreenshot": { + "properties": { + "fileId": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ] + }, + "userPrompt": { + "type": "string" + } + }, + "required": [ + "userPrompt" + ], + "type": "object" } }, "description": "EXPERIMENTAL - app list response.", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts new file mode 100644 index 000000000..873398db6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppBranding.ts @@ -0,0 +1,8 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * EXPERIMENTAL - app metadata returned by app-list APIs. + */ +export type AppBranding = { category: string | null, developer: string | null, website: string | null, privacyPolicy: string | null, termsOfService: string | null, isDiscoverableApp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts index 2f65c1832..0c9a13b12 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppInfo.ts @@ -1,11 +1,13 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppBranding } from "./AppBranding"; +import type { AppMetadata } from "./AppMetadata"; /** * EXPERIMENTAL - app metadata returned by app-list APIs. */ -export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, installUrl: string | null, isAccessible: boolean, +export type AppInfo = { id: string, name: string, description: string | null, logoUrl: string | null, logoUrlDark: string | null, distributionChannel: string | null, branding: AppBranding | null, appMetadata: AppMetadata | null, labels: { [key in string]?: string } | null, installUrl: string | null, isAccessible: boolean, /** * Whether this app is enabled in config.toml. * Example: diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts new file mode 100644 index 000000000..f1a5001eb --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppMetadata.ts @@ -0,0 +1,7 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AppReview } from "./AppReview"; +import type { AppScreenshot } from "./AppScreenshot"; + +export type AppMetadata = { review: AppReview | null, categories: Array | null, subCategories: Array | null, seoDescription: string | null, screenshots: Array | null, developer: string | null, version: string | null, versionId: string | null, versionNotes: string | null, firstPartyType: string | null, firstPartyRequiresInstall: boolean | null, showInComposerWhenUnlinked: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts new file mode 100644 index 000000000..10fd95f09 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppReview.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppReview = { status: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts new file mode 100644 index 000000000..0d264246f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AppScreenshot.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AppScreenshot = { url: string | null, fileId: string | null, userPrompt: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 8a1456f42..14a09f2f5 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -6,9 +6,13 @@ export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUp export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; export type { AgentMessageDeltaNotification } from "./AgentMessageDeltaNotification"; export type { AnalyticsConfig } from "./AnalyticsConfig"; +export type { AppBranding } from "./AppBranding"; export type { AppDisabledReason } from "./AppDisabledReason"; export type { AppInfo } from "./AppInfo"; export type { AppListUpdatedNotification } from "./AppListUpdatedNotification"; +export type { AppMetadata } from "./AppMetadata"; +export type { AppReview } from "./AppReview"; +export type { AppScreenshot } from "./AppScreenshot"; export type { AppsConfig } from "./AppsConfig"; export type { AppsListParams } from "./AppsListParams"; export type { AppsListResponse } from "./AppsListResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4d4b15127..72732af85 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1287,6 +1287,55 @@ pub struct AppsListParams { pub force_refetch: bool, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +/// EXPERIMENTAL - app metadata returned by app-list APIs. +pub struct AppBranding { + pub category: Option, + pub developer: Option, + pub website: Option, + pub privacy_policy: Option, + pub terms_of_service: Option, + pub is_discoverable_app: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppReview { + pub status: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppScreenshot { + pub url: Option, + #[serde(alias = "file_id")] + pub file_id: Option, + #[serde(alias = "user_prompt")] + pub user_prompt: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct AppMetadata { + pub review: Option, + pub categories: Option>, + pub sub_categories: Option>, + pub seo_description: Option, + pub screenshots: Option>, + pub developer: Option, + pub version: Option, + pub version_id: Option, + pub version_notes: Option, + pub first_party_type: Option, + pub first_party_requires_install: Option, + pub show_in_composer_when_unlinked: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -1298,6 +1347,9 @@ pub struct AppInfo { pub logo_url: Option, pub logo_url_dark: Option, pub distribution_channel: Option, + pub branding: Option, + pub app_metadata: Option, + pub labels: Option>, pub install_url: Option, #[serde(default)] pub is_accessible: bool, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index c65793d24..823e69387 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -772,7 +772,7 @@ To enable or disable a skill by path: ## Apps -Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, whether it is currently accessible, and whether it is enabled in config. +Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config. ```json { "method": "app/list", "id": 50, "params": { @@ -790,6 +790,9 @@ Use `app/list` to fetch available apps (connectors). Each entry includes metadat "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, "distributionChannel": null, + "branding": null, + "appMetadata": null, + "labels": null, "installUrl": "https://chatgpt.com/apps/demo-app/demo-app", "isAccessible": true, "isEnabled": true @@ -817,6 +820,9 @@ The server also emits `app/list/updated` notifications whenever either source (a "logoUrl": "https://example.com/demo-app.png", "logoUrlDark": null, "distributionChannel": null, + "branding": null, + "appMetadata": null, + "labels": null, "installUrl": "https://chatgpt.com/apps/demo-app/demo-app", "isAccessible": true, "isEnabled": true diff --git a/codex-rs/app-server/tests/suite/v2/app_list.rs b/codex-rs/app-server/tests/suite/v2/app_list.rs index 079eba479..1fd3f5a44 100644 --- a/codex-rs/app-server/tests/suite/v2/app_list.rs +++ b/codex-rs/app-server/tests/suite/v2/app_list.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::collections::HashMap; use std::sync::Arc; use std::sync::Mutex as StdMutex; use std::time::Duration; @@ -16,8 +17,12 @@ use axum::http::HeaderMap; use axum::http::StatusCode; use axum::http::header::AUTHORIZATION; use axum::routing::get; +use codex_app_server_protocol::AppBranding; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppListUpdatedNotification; +use codex_app_server_protocol::AppMetadata; +use codex_app_server_protocol::AppReview; +use codex_app_server_protocol::AppScreenshot; use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListResponse; use codex_app_server_protocol::JSONRPCError; @@ -85,6 +90,9 @@ async fn list_apps_uses_thread_feature_flag_when_thread_id_is_provided() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -184,6 +192,9 @@ async fn list_apps_reports_is_enabled_from_config() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -249,6 +260,39 @@ enabled = false #[tokio::test] async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<()> { + let alpha_branding = Some(AppBranding { + category: Some("PRODUCTIVITY".to_string()), + developer: Some("Acme".to_string()), + website: Some("https://acme.example".to_string()), + privacy_policy: Some("https://acme.example/privacy".to_string()), + terms_of_service: Some("https://acme.example/terms".to_string()), + is_discoverable_app: true, + }); + let alpha_app_metadata = Some(AppMetadata { + review: Some(AppReview { + status: "APPROVED".to_string(), + }), + categories: Some(vec!["PRODUCTIVITY".to_string()]), + sub_categories: Some(vec!["WRITING".to_string()]), + seo_description: Some("Alpha connector".to_string()), + screenshots: Some(vec![AppScreenshot { + url: Some("https://example.com/alpha-screenshot.png".to_string()), + file_id: Some("file_123".to_string()), + user_prompt: "Summarize this draft".to_string(), + }]), + developer: Some("Acme".to_string()), + version: Some("1.2.3".to_string()), + version_id: Some("version_123".to_string()), + version_notes: Some("Fixes and improvements".to_string()), + first_party_type: Some("internal".to_string()), + first_party_requires_install: Some(true), + show_in_composer_when_unlinked: Some(true), + }); + let alpha_labels = Some(HashMap::from([ + ("feature".to_string(), "beta".to_string()), + ("source".to_string(), "directory".to_string()), + ])); + let connectors = vec![ AppInfo { id: "alpha".to_string(), @@ -257,6 +301,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: alpha_branding.clone(), + app_metadata: alpha_app_metadata.clone(), + labels: alpha_labels.clone(), install_url: None, is_accessible: false, is_enabled: true, @@ -268,6 +315,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -313,6 +363,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -329,6 +382,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -340,6 +396,9 @@ async fn list_apps_emits_updates_and_returns_after_both_lists_load() -> Result<( logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: alpha_branding, + app_metadata: alpha_app_metadata, + labels: alpha_labels, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -377,6 +436,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -388,6 +450,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -437,6 +502,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -448,6 +516,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: false, is_enabled: true, @@ -463,6 +534,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -474,6 +548,9 @@ async fn list_apps_returns_connectors_with_accessible_flags() -> Result<()> { logo_url: Some("https://example.com/alpha.png".to_string()), logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -506,6 +583,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -517,6 +597,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -571,6 +654,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta/beta".to_string()), is_accessible: true, is_enabled: true, @@ -611,6 +697,9 @@ async fn list_apps_paginates_results() -> Result<()> { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -632,6 +721,9 @@ async fn list_apps_force_refetch_preserves_previous_cache_on_failure() -> Result logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -733,6 +825,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -744,6 +839,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -790,6 +888,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -807,6 +908,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: true, is_enabled: true, @@ -818,6 +922,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -844,6 +951,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -870,6 +980,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, @@ -881,6 +994,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/beta-app/beta".to_string()), is_accessible: false, is_enabled: true, @@ -895,6 +1011,9 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://chatgpt.com/apps/alpha/alpha".to_string()), is_accessible: false, is_enabled: true, diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index bafb0273a..8955fbf84 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -14,7 +14,9 @@ use crate::chatgpt_client::chatgpt_get_request_with_timeout; use crate::chatgpt_token::get_chatgpt_token_data; use crate::chatgpt_token::init_chatgpt_token_from_auth; +use codex_core::connectors::AppBranding; pub use codex_core::connectors::AppInfo; +use codex_core::connectors::AppMetadata; use codex_core::connectors::CONNECTORS_CACHE_TTL; pub use codex_core::connectors::connector_display_label; use codex_core::connectors::connector_install_url; @@ -36,6 +38,10 @@ struct DirectoryApp { id: String, name: String, description: Option, + #[serde(alias = "appMetadata")] + app_metadata: Option, + branding: Option, + labels: Option>, #[serde(alias = "logoUrl")] logo_url: Option, #[serde(alias = "logoUrlDark")] @@ -269,6 +275,9 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { id: _, name, description, + app_metadata, + branding, + labels, logo_url, logo_url_dark, distribution_channel, @@ -302,6 +311,108 @@ fn merge_directory_app(existing: &mut DirectoryApp, incoming: DirectoryApp) { if existing.distribution_channel.is_none() && distribution_channel.is_some() { existing.distribution_channel = distribution_channel; } + + if let Some(incoming_branding) = branding { + if let Some(existing_branding) = existing.branding.as_mut() { + if existing_branding.category.is_none() && incoming_branding.category.is_some() { + existing_branding.category = incoming_branding.category; + } + if existing_branding.developer.is_none() && incoming_branding.developer.is_some() { + existing_branding.developer = incoming_branding.developer; + } + if existing_branding.website.is_none() && incoming_branding.website.is_some() { + existing_branding.website = incoming_branding.website; + } + if existing_branding.privacy_policy.is_none() + && incoming_branding.privacy_policy.is_some() + { + existing_branding.privacy_policy = incoming_branding.privacy_policy; + } + if existing_branding.terms_of_service.is_none() + && incoming_branding.terms_of_service.is_some() + { + existing_branding.terms_of_service = incoming_branding.terms_of_service; + } + if !existing_branding.is_discoverable_app && incoming_branding.is_discoverable_app { + existing_branding.is_discoverable_app = true; + } + } else { + existing.branding = Some(incoming_branding); + } + } + + if let Some(incoming_app_metadata) = app_metadata { + if let Some(existing_app_metadata) = existing.app_metadata.as_mut() { + if existing_app_metadata.review.is_none() && incoming_app_metadata.review.is_some() { + existing_app_metadata.review = incoming_app_metadata.review; + } + if existing_app_metadata.categories.is_none() + && incoming_app_metadata.categories.is_some() + { + existing_app_metadata.categories = incoming_app_metadata.categories; + } + if existing_app_metadata.sub_categories.is_none() + && incoming_app_metadata.sub_categories.is_some() + { + existing_app_metadata.sub_categories = incoming_app_metadata.sub_categories; + } + if existing_app_metadata.seo_description.is_none() + && incoming_app_metadata.seo_description.is_some() + { + existing_app_metadata.seo_description = incoming_app_metadata.seo_description; + } + if existing_app_metadata.screenshots.is_none() + && incoming_app_metadata.screenshots.is_some() + { + existing_app_metadata.screenshots = incoming_app_metadata.screenshots; + } + if existing_app_metadata.developer.is_none() + && incoming_app_metadata.developer.is_some() + { + existing_app_metadata.developer = incoming_app_metadata.developer; + } + if existing_app_metadata.version.is_none() && incoming_app_metadata.version.is_some() { + existing_app_metadata.version = incoming_app_metadata.version; + } + if existing_app_metadata.version_id.is_none() + && incoming_app_metadata.version_id.is_some() + { + existing_app_metadata.version_id = incoming_app_metadata.version_id; + } + if existing_app_metadata.version_notes.is_none() + && incoming_app_metadata.version_notes.is_some() + { + existing_app_metadata.version_notes = incoming_app_metadata.version_notes; + } + if existing_app_metadata.first_party_type.is_none() + && incoming_app_metadata.first_party_type.is_some() + { + existing_app_metadata.first_party_type = incoming_app_metadata.first_party_type; + } + if existing_app_metadata.first_party_requires_install.is_none() + && incoming_app_metadata.first_party_requires_install.is_some() + { + existing_app_metadata.first_party_requires_install = + incoming_app_metadata.first_party_requires_install; + } + if existing_app_metadata + .show_in_composer_when_unlinked + .is_none() + && incoming_app_metadata + .show_in_composer_when_unlinked + .is_some() + { + existing_app_metadata.show_in_composer_when_unlinked = + incoming_app_metadata.show_in_composer_when_unlinked; + } + } else { + existing.app_metadata = Some(incoming_app_metadata); + } + } + + if existing.labels.is_none() && labels.is_some() { + existing.labels = labels; + } } fn is_hidden_directory_app(app: &DirectoryApp) -> bool { @@ -316,6 +427,9 @@ fn directory_app_to_app_info(app: DirectoryApp) -> AppInfo { logo_url: app.logo_url, logo_url_dark: app.logo_url_dark, distribution_channel: app.distribution_channel, + branding: app.branding, + app_metadata: app.app_metadata, + labels: app.labels, install_url: None, is_accessible: false, is_enabled: true, @@ -342,6 +456,8 @@ const DISALLOWED_CONNECTOR_IDS: &[&str] = &[ "asdk_app_6938a94a61d881918ef32cb999ff937c", "connector_2b0a9009c9c64bf9933a3dae3f2b1254", "connector_68de829bf7648191acd70a907364c67c", + "connector_68e004f14af881919eb50893d3d9f523", + "connector_69272cb413a081919685ec3c88d1744e", ]; const DISALLOWED_CONNECTOR_PREFIX: &str = "connector_openai_"; @@ -375,6 +491,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: false, is_enabled: true, @@ -429,6 +548,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some(connector_install_url(id, id)), is_accessible, is_enabled: true, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cf362d4ba..c0eb25975 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -5823,6 +5823,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: true, is_enabled: true, diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 35c2dc5ce..5a8930231 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -7,7 +7,9 @@ use std::time::Duration; use std::time::Instant; use async_channel::unbounded; +pub use codex_app_server_protocol::AppBranding; pub use codex_app_server_protocol::AppInfo; +pub use codex_app_server_protocol::AppMetadata; use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; use tokio_util::sync::CancellationToken; @@ -320,6 +322,9 @@ where logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some(connector_install_url(&connector_name, &connector_id)), is_accessible: true, is_enabled: true, diff --git a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs index e535abac9..c5e267cb4 100644 --- a/codex-rs/core/src/tools/handlers/search_tool_bm25.rs +++ b/codex-rs/core/src/tools/handlers/search_tool_bm25.rs @@ -261,6 +261,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: None, is_accessible: true, is_enabled: enabled, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 36142cbae..91dfa00a5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4596,6 +4596,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4633,6 +4636,9 @@ mod tests { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: false, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f49d31827..bfae9581b 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -4424,6 +4424,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4457,6 +4460,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4468,6 +4474,9 @@ async fn apps_popup_refreshes_when_connectors_snapshot_updates() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: true, is_enabled: true, @@ -4504,6 +4513,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4515,6 +4527,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true, @@ -4536,6 +4551,9 @@ async fn apps_refresh_failure_keeps_existing_full_snapshot() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4572,6 +4590,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4583,6 +4604,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true, @@ -4606,6 +4630,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4617,6 +4644,9 @@ async fn apps_partial_refresh_uses_same_filtering_as_full_refresh() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/hidden-openai".to_string()), is_accessible: true, is_enabled: true, @@ -4657,6 +4687,9 @@ async fn apps_popup_shows_disabled_status_for_installed_but_disabled_apps() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: false, @@ -4704,6 +4737,9 @@ async fn apps_initial_load_applies_enabled_state_from_config() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4738,6 +4774,9 @@ async fn apps_refresh_preserves_toggled_enabled_state() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4756,6 +4795,9 @@ async fn apps_refresh_preserves_toggled_enabled_state() { logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/notion".to_string()), is_accessible: true, is_enabled: true, @@ -4797,6 +4839,9 @@ async fn apps_popup_for_not_installed_app_uses_install_only_selected_description logo_url: None, logo_url_dark: None, distribution_channel: None, + branding: None, + app_metadata: None, + labels: None, install_url: Some("https://example.test/linear".to_string()), is_accessible: false, is_enabled: true,