[apps] Add gated instructions for Apps. (#10924)

- [x] Add gated instructions for Apps.
This commit is contained in:
Matthew Zeng
2026-02-09 14:48:09 -08:00
committed by GitHub
Unverified
parent ed977dbeda
commit d90df4761b
6 changed files with 111 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
mod render;
pub(crate) use render::render_apps_section;
+7
View File
@@ -0,0 +1,7 @@
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
pub(crate) fn render_apps_section() -> String {
format!(
"## Apps\nApps are mentioned in the prompt in the format `[$app-name](apps://{{connector_id}})`.\nAn app is equivalent to a set of MCP tools within the `{CODEX_APPS_MCP_SERVER_NAME}` MCP.\nWhen you see an app mention, the app's MCP tools are either already provided in `{CODEX_APPS_MCP_SERVER_NAME}`, or do not exist because the user did not install it.\nDo not additionally call list_mcp_resources for apps that are already mentioned."
)
}
+4
View File
@@ -15,6 +15,7 @@ use crate::agent::MAX_THREAD_SPAWN_DEPTH;
use crate::agent::agent_status_from_event;
use crate::analytics_client::AnalyticsEventsClient;
use crate::analytics_client::build_track_events_context;
use crate::apps::render_apps_section;
use crate::compact;
use crate::compact::run_inline_auto_compact_task;
use crate::compact::should_use_remote_compact_task;
@@ -2280,6 +2281,9 @@ impl Session {
);
}
}
if turn_context.features.enabled(Feature::Apps) {
items.push(DeveloperInstructions::new(render_apps_section()).into());
}
if let Some(user_instructions) = turn_context.user_instructions.as_deref() {
items.push(
UserInstructions {
+1
View File
@@ -8,6 +8,7 @@
mod analytics_client;
pub mod api_bridge;
mod apply_patch;
mod apps;
pub mod auth;
pub mod bash;
mod client;
+24
View File
@@ -547,6 +547,30 @@ mod tests {
assert_eq!(res, expected);
}
#[tokio::test]
async fn apps_feature_does_not_emit_user_instructions_by_itself() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut cfg = make_config(&tmp, 4096, None).await;
cfg.features.enable(Feature::Apps);
let res = get_user_instructions(&cfg, None).await;
assert_eq!(res, None);
}
#[tokio::test]
async fn apps_feature_does_not_append_to_project_doc_user_instructions() {
let tmp = tempfile::tempdir().expect("tempdir");
fs::write(tmp.path().join("AGENTS.md"), "base doc").unwrap();
let mut cfg = make_config(&tmp, 4096, None).await;
cfg.features.enable(Feature::Apps);
let res = get_user_instructions(&cfg, None)
.await
.expect("instructions expected");
assert_eq!(res, "base doc");
}
fn create_skill(codex_home: PathBuf, name: &str, description: &str) {
let skill_dir = codex_home.join(format!("skills/{name}"));
fs::create_dir_all(&skill_dir).unwrap();
+72
View File
@@ -16,6 +16,7 @@ use codex_core::auth::AuthCredentialsStoreMode;
use codex_core::built_in_model_providers;
use codex_core::default_client::originator;
use codex_core::error::CodexErr;
use codex_core::features::Feature;
use codex_core::models_manager::manager::ModelsManager;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
@@ -666,6 +667,77 @@ async fn includes_user_instructions_message_in_request() {
assert_message_ends_with(&request_body["input"][2], "</environment_context>");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_apps_guidance_as_developer_message_when_enabled() {
skip_if_no_network!();
let server = MockServer::start().await;
let resp_mock = mount_sse_once(
&server,
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
)
.await;
let mut builder = test_codex()
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(|config| {
config.features.enable(Feature::Apps);
});
let codex = builder
.build(&server)
.await
.expect("create new conversation")
.codex;
codex
.submit(Op::UserInput {
items: vec![UserInput::Text {
text: "hello".into(),
text_elements: Vec::new(),
}],
final_output_json_schema: None,
})
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let request = resp_mock.single_request();
let request_body = request.body_json();
let input = request_body["input"].as_array().expect("input array");
let apps_snippet = "Apps are mentioned in the prompt in the format";
let has_developer_apps_guidance = input.iter().any(|item| {
item.get("role").and_then(|value| value.as_str()) == Some("developer")
&& item
.get("content")
.and_then(|value| value.as_array())
.and_then(|value| value.first())
.and_then(|value| value.get("text"))
.and_then(|value| value.as_str())
.is_some_and(|text| text.contains(apps_snippet))
});
assert!(
has_developer_apps_guidance,
"expected apps guidance in a developer message, got {input:#?}"
);
let has_user_apps_guidance = input.iter().any(|item| {
item.get("role").and_then(|value| value.as_str()) == Some("user")
&& item
.get("content")
.and_then(|value| value.as_array())
.and_then(|value| value.first())
.and_then(|value| value.get("text"))
.and_then(|value| value.as_str())
.is_some_and(|text| text.contains(apps_snippet))
});
assert!(
!has_user_apps_guidance,
"did not expect apps guidance in user messages, got {input:#?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn skills_append_to_instructions() {
skip_if_no_network!();