Add runtime extra skill roots API (#24977)

## Summary
- Add v2 `skills/extraRoots/set` to replace app-server process-local
standalone skill roots. The setting is not persisted, accepts missing
roots, and `extraRoots: []` clears the runtime set.
- Wire runtime roots into core skill discovery for `skills/list` and
turn loads, clear skill caches on set, and register the roots with the
skills watcher so later filesystem changes emit `skills/changed`.
- Update app-server docs, generated JSON/TypeScript schemas, and
coverage for serialization, missing roots, empty clears, and restart
behavior.

## Testing
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-core-skills`
- `cargo test -p codex-app-server
skills_extra_roots_set_updates_process_runtime_roots`
- `just fix -p codex-app-server-protocol`
- `just fix -p codex-core-skills`
- `just fix -p codex-app-server`
This commit is contained in:
xl-openai
2026-05-28 21:14:34 -07:00
committed by GitHub
parent 42c80385cd
commit f0a839ea0c
23 changed files with 632 additions and 4 deletions
@@ -610,6 +610,11 @@ client_request_definitions! {
serialization: global_shared_read("config"),
response: v2::SkillsListResponse,
},
SkillsExtraRootsSet => "skills/extraRoots/set" {
params: v2::SkillsExtraRootsSetParams,
serialization: global("config"),
response: v2::SkillsExtraRootsSetResponse,
},
HooksList => "hooks/list" {
params: v2::HooksListParams,
serialization: global("config"),
@@ -1721,6 +1726,17 @@ mod tests {
Some(ClientRequestSerializationScope::GlobalSharedRead("config"))
);
let skills_extra_roots_set = ClientRequest::SkillsExtraRootsSet {
request_id: request_id(),
params: v2::SkillsExtraRootsSetParams {
extra_roots: vec![absolute_path("/tmp/skills")],
},
};
assert_eq!(
skills_extra_roots_set.serialization_scope(),
Some(ClientRequestSerializationScope::Global("config"))
);
let plugin_list = ClientRequest::PluginList {
request_id: request_id(),
params: v2::PluginListParams {
@@ -35,6 +35,18 @@ pub struct SkillsListResponse {
pub data: Vec<SkillsListEntry>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsExtraRootsSetParams {
pub extra_roots: Vec<AbsolutePathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsExtraRootsSetResponse {}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -2626,6 +2626,27 @@ fn skills_list_params_serialization_uses_force_reload() {
);
}
#[test]
fn skills_extra_roots_set_params_serialization_uses_extra_roots() {
assert_eq!(
serde_json::to_value(SkillsExtraRootsSetParams {
extra_roots: vec![absolute_path("tmp/skills")],
})
.unwrap(),
json!({
"extraRoots": [absolute_path_string("tmp/skills")],
}),
);
}
#[test]
fn skills_extra_roots_set_params_rejects_relative_roots() {
let result = serde_json::from_value::<SkillsExtraRootsSetParams>(json!({
"extraRoots": ["relative/path"],
}));
assert!(result.is_err());
}
#[test]
fn plugin_source_serializes_local_git_and_remote_variants() {
let local_path = if cfg!(windows) {