[codex] Expose unavailable app templates in plugin detail (#26317)

## Summary
- Adds `unavailable_app_templates` to the app-server protocol and
generated schemas/types.
- Parses plugin-service `release.unavailable_app_templates` in the
remote plugin client.
- Maps remote unavailable templates into app-server `PluginDetail`.
- Defaults local plugins to an empty unavailable app template list.

## Validation
- `just write-app-server-schema`
- `cargo +1.95.0 fmt --manifest-path codex-rs/Cargo.toml --all --check`
- `cargo +1.95.0 test --manifest-path codex-rs/Cargo.toml -p
codex-app-server-protocol schema_fixtures`
- `cargo +1.95.0 check --manifest-path codex-rs/Cargo.toml -p
codex-app-server-protocol -p codex-core-plugins -p codex-app-server`
- `git diff --check`

Note: default `cargo check` uses rustc 1.89 locally and failed because
dependencies require newer Rust, so validation was rerun with installed
Rust 1.95.
This commit is contained in:
charlesgong-openai
2026-06-04 16:42:27 -07:00
committed by GitHub
Unverified
parent e695ec8ec6
commit b9ff450902
13 changed files with 383 additions and 4 deletions
@@ -3786,8 +3786,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -6278,6 +6278,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/v2/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"AppToolApproval": {
"enum": [
"auto",
@@ -12168,6 +12231,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/v2/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/v2/AppSummary"
@@ -12216,6 +12285,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -19232,4 +19302,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -643,6 +643,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"AppToolApproval": {
"enum": [
"auto",
@@ -8690,6 +8753,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/AppSummary"
@@ -8738,6 +8807,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -37,6 +37,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"HookEventName": {
"enum": [
"preToolUse",
@@ -78,6 +141,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/AppSummary"
@@ -126,6 +195,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -0,0 +1,6 @@
// 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 { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason";
export type AppTemplateSummary = { templateId: string, name: string, description: string | null, canonicalConnectorId: string | null, logoUrl: string | null, logoUrlDark: string | null, materializedAppIds: Array<string>, reason: AppTemplateUnavailableReason | null, };
@@ -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 AppTemplateUnavailableReason = "NOT_CONFIGURED_FOR_WORKSPACE" | "NO_ACTIVE_WORKSPACE";
@@ -3,8 +3,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { AppSummary } from "./AppSummary";
import type { AppTemplateSummary } from "./AppTemplateSummary";
import type { PluginHookSummary } from "./PluginHookSummary";
import type { PluginSummary } from "./PluginSummary";
import type { SkillSummary } from "./SkillSummary";
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, mcpServers: Array<string>, };
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, };
@@ -21,6 +21,8 @@ export type { AppMetadata } from "./AppMetadata";
export type { AppReview } from "./AppReview";
export type { AppScreenshot } from "./AppScreenshot";
export type { AppSummary } from "./AppSummary";
export type { AppTemplateSummary } from "./AppTemplateSummary";
export type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason";
export type { AppToolApproval } from "./AppToolApproval";
export type { AppToolsConfig } from "./AppToolsConfig";
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
@@ -642,9 +642,32 @@ pub struct PluginDetail {
pub skills: Vec<SkillSummary>,
pub hooks: Vec<PluginHookSummary>,
pub apps: Vec<AppSummary>,
pub app_templates: Vec<AppTemplateSummary>,
pub mcp_servers: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[ts(export_to = "v2/")]
pub enum AppTemplateUnavailableReason {
NotConfiguredForWorkspace,
NoActiveWorkspace,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AppTemplateSummary {
pub template_id: String,
pub name: String,
pub description: Option<String>,
pub canonical_connector_id: Option<String>,
pub logo_url: Option<String>,
pub logo_url_dark: Option<String>,
pub materialized_app_ids: Vec<String>,
pub reason: Option<AppTemplateUnavailableReason>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -30,6 +30,8 @@ use codex_app_server_protocol::AdditionalContextKind;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppListUpdatedNotification;
use codex_app_server_protocol::AppSummary;
use codex_app_server_protocol::AppTemplateSummary;
use codex_app_server_protocol::AppTemplateUnavailableReason;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::AskForApproval;
@@ -8,6 +8,7 @@ use codex_app_server_protocol::PluginShareTargetRole;
use codex_config::types::McpServerConfig;
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use codex_core_plugins::remote::RemoteAppTemplateUnavailableReason;
use codex_core_plugins::remote::RemotePluginScope;
use codex_core_plugins::remote::is_valid_remote_plugin_id;
use codex_core_plugins::remote::validate_remote_plugin_id;
@@ -1062,6 +1063,7 @@ impl PluginRequestProcessor {
})
.collect(),
apps: app_summaries,
app_templates: Vec::new(),
mcp_servers: outcome.plugin.mcp_server_names,
}
}
@@ -2007,6 +2009,28 @@ fn remote_plugin_detail_to_info(
detail: RemoteCatalogPluginDetail,
apps: Vec<AppSummary>,
) -> PluginDetail {
let app_templates = detail
.app_templates
.into_iter()
.map(|template| AppTemplateSummary {
template_id: template.template_id,
name: template.name,
description: template.description,
canonical_connector_id: template.canonical_connector_id,
logo_url: template.logo_url,
logo_url_dark: template.logo_url_dark,
materialized_app_ids: template.materialized_app_ids,
reason: template.reason.map(|reason| match reason {
RemoteAppTemplateUnavailableReason::NotConfiguredForWorkspace => {
AppTemplateUnavailableReason::NotConfiguredForWorkspace
}
RemoteAppTemplateUnavailableReason::NoActiveWorkspace => {
AppTemplateUnavailableReason::NoActiveWorkspace
}
}),
})
.collect();
PluginDetail {
marketplace_name: detail.marketplace_name,
marketplace_path: None,
@@ -2026,6 +2050,7 @@ fn remote_plugin_detail_to_info(
.collect(),
hooks: Vec::new(),
apps,
app_templates,
mcp_servers: detail.mcp_servers,
}
}
@@ -17,6 +17,8 @@ use axum::http::Uri;
use axum::http::header::AUTHORIZATION;
use axum::routing::get;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppTemplateSummary;
use codex_app_server_protocol::AppTemplateUnavailableReason;
use codex_app_server_protocol::HookEventName;
use codex_app_server_protocol::JSONRPCError;
use codex_app_server_protocol::JSONRPCResponse;
@@ -427,6 +429,28 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
"display_name": "Linear",
"description": "Track work in Linear",
"app_ids": [],
"app_templates": [
{
"template_id": "templated_apps_GitHubEnterprise",
"name": "GitHub Enterprise",
"description": "Connect GitHub Enterprise",
"canonical_connector_id": "github_enterprise",
"logo_url": "https://example.com/ghe-light.png",
"logo_url_dark": "https://example.com/ghe-dark.png",
"materialized_app_ids": ["asdk_app_ghe"],
"reason": null
},
{
"template_id": "templated_apps_Databricks",
"name": "Databricks",
"description": null,
"canonical_connector_id": null,
"logo_url": null,
"logo_url_dark": null,
"materialized_app_ids": [],
"reason": "NOT_CONFIGURED_FOR_WORKSPACE"
}
],
"keywords": ["issue-tracking", "project management"],
"interface": {
"short_description": "Plan and track work",
@@ -564,6 +588,31 @@ async fn plugin_read_reads_remote_plugin_details_when_remote_plugin_enabled() ->
assert_eq!(response.plugin.skills[0].path, None);
assert_eq!(response.plugin.skills[0].enabled, false);
assert_eq!(response.plugin.apps.len(), 0);
assert_eq!(
response.plugin.app_templates,
vec![
AppTemplateSummary {
template_id: "templated_apps_GitHubEnterprise".to_string(),
name: "GitHub Enterprise".to_string(),
description: Some("Connect GitHub Enterprise".to_string()),
canonical_connector_id: Some("github_enterprise".to_string()),
logo_url: Some("https://example.com/ghe-light.png".to_string()),
logo_url_dark: Some("https://example.com/ghe-dark.png".to_string()),
materialized_app_ids: vec!["asdk_app_ghe".to_string()],
reason: None,
},
AppTemplateSummary {
template_id: "templated_apps_Databricks".to_string(),
name: "Databricks".to_string(),
description: None,
canonical_connector_id: None,
logo_url: None,
logo_url_dark: None,
materialized_app_ids: Vec::new(),
reason: Some(AppTemplateUnavailableReason::NotConfiguredForWorkspace),
},
]
);
Ok(())
}
+55
View File
@@ -167,9 +167,29 @@ pub struct RemotePluginDetail {
pub app_manifest: Option<JsonValue>,
pub skills: Vec<RemotePluginSkill>,
pub app_ids: Vec<String>,
pub app_templates: Vec<RemoteAppTemplate>,
pub mcp_servers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteAppTemplate {
pub template_id: String,
pub name: String,
pub description: Option<String>,
pub canonical_connector_id: Option<String>,
pub logo_url: Option<String>,
pub logo_url_dark: Option<String>,
pub materialized_app_ids: Vec<String>,
pub reason: Option<RemoteAppTemplateUnavailableReason>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RemoteAppTemplateUnavailableReason {
NotConfiguredForWorkspace,
NoActiveWorkspace,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RemotePluginSkill {
pub name: String,
@@ -413,6 +433,8 @@ struct RemotePluginReleaseResponse {
app_ids: Vec<String>,
#[serde(default)]
app_manifest: Option<JsonValue>,
#[serde(default, alias = "unavailable_app_templates")]
app_templates: Vec<RemoteAppTemplateResponse>,
#[serde(default)]
keywords: Vec<String>,
interface: RemotePluginReleaseInterfaceResponse,
@@ -427,6 +449,24 @@ struct RemotePluginMcpServerResponse {
key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
struct RemoteAppTemplateResponse {
template_id: String,
name: String,
#[serde(default)]
description: Option<String>,
#[serde(default)]
canonical_connector_id: Option<String>,
#[serde(default)]
logo_url: Option<String>,
#[serde(default)]
logo_url_dark: Option<String>,
#[serde(default)]
materialized_app_ids: Vec<String>,
#[serde(default)]
reason: Option<RemoteAppTemplateUnavailableReason>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
struct RemotePluginDirectoryItem {
id: String,
@@ -975,6 +1015,21 @@ async fn build_remote_plugin_detail(
app_manifest: plugin.release.app_manifest,
skills,
app_ids: plugin.release.app_ids,
app_templates: plugin
.release
.app_templates
.into_iter()
.map(|template| RemoteAppTemplate {
template_id: template.template_id,
name: template.name,
description: template.description,
canonical_connector_id: template.canonical_connector_id,
logo_url: template.logo_url,
logo_url_dark: template.logo_url_dark,
materialized_app_ids: template.materialized_app_ids,
reason: template.reason,
})
.collect(),
mcp_servers,
})
}
@@ -1429,6 +1429,7 @@ pub(super) fn plugins_test_detail(
needs_auth: *needs_auth,
})
.collect(),
app_templates: Vec::new(),
mcp_servers: mcp_servers.iter().map(|name| (*name).to_string()).collect(),
}
}