Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 36 additions & 6 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,10 +1535,22 @@ pub struct TurnInterruptResponse {}
#[ts(tag = "type")]
#[ts(export_to = "v2/")]
pub enum UserInput {
Text { text: String },
Image { url: String },
LocalImage { path: PathBuf },
Skill { name: String, path: PathBuf },
Text {
text: String,
},
Image {
url: String,
},
LocalImage {
path: PathBuf,
},
Skill {
name: String,
path: PathBuf,
#[serde(default)]
// Experimental; subject to change.
validate_dependencies: bool,
},
}

impl UserInput {
Expand All @@ -1547,7 +1559,15 @@ impl UserInput {
UserInput::Text { text } => CoreUserInput::Text { text },
UserInput::Image { url } => CoreUserInput::Image { image_url: url },
UserInput::LocalImage { path } => CoreUserInput::LocalImage { path },
UserInput::Skill { name, path } => CoreUserInput::Skill { name, path },
UserInput::Skill {
name,
path,
validate_dependencies,
} => CoreUserInput::Skill {
name,
path,
validate_dependencies,
},
}
}
}
Expand All @@ -1558,7 +1578,15 @@ impl From<CoreUserInput> for UserInput {
CoreUserInput::Text { text } => UserInput::Text { text },
CoreUserInput::Image { image_url } => UserInput::Image { url: image_url },
CoreUserInput::LocalImage { path } => UserInput::LocalImage { path },
CoreUserInput::Skill { name, path } => UserInput::Skill { name, path },
CoreUserInput::Skill {
name,
path,
validate_dependencies,
} => UserInput::Skill {
name,
path,
validate_dependencies,
},
_ => unreachable!("unsupported user input variant"),
}
}
Expand Down Expand Up @@ -2147,6 +2175,7 @@ mod tests {
CoreUserInput::Skill {
name: "skill-creator".to_string(),
path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"),
validate_dependencies: false,
},
],
});
Expand All @@ -2168,6 +2197,7 @@ mod tests {
UserInput::Skill {
name: "skill-creator".to_string(),
path: PathBuf::from("/repo/.codex/skills/skill-creator/SKILL.md"),
validate_dependencies: false,
},
],
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ pub(crate) async fn apply_bespoke_event_handling(
});
}
},
EventMsg::SkillDependencyRequest(_ev) => {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward or resolve skill dependency requests

This match arm drops SkillDependencyRequest events entirely. When a client sets validate_dependencies: true (now part of the v2 UserInput::Skill payload), core awaits a response via request_skill_dependencies; if the app-server never forwards or auto-resolves the request, the turn will hang indefinitely waiting on the oneshot. Please mirror the exec/patch approval handling (e.g., send a request to the client or auto-cancel with ResolveSkillDependencies) so sessions don’t stall when dependencies are validated.

Useful? React with 👍 / 👎.

EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent {
call_id,
turn_id,
Expand Down
82 changes: 73 additions & 9 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,13 @@ use crate::rollout::RolloutRecorderParams;
use crate::rollout::map_session_init_error;
use crate::shell;
use crate::shell_snapshot::ShellSnapshot;
use crate::skills::SkillDependencyResponse;
use crate::skills::SkillError;
use crate::skills::SkillInjections;
use crate::skills::SkillMetadata;
use crate::skills::SkillTurnPrep;
use crate::skills::SkillsManager;
use crate::skills::build_skill_injections;
use crate::skills::build_skill_turn_prep;
use crate::skills::resolve_skill_dependencies_for_turn;
use crate::state::ActiveTurn;
use crate::state::SessionServices;
use crate::state::SessionState;
Expand Down Expand Up @@ -681,7 +683,7 @@ impl Session {
.await
.map(Arc::new);
}
let state = SessionState::new(session_configuration.clone());
let state = SessionState::new(session_configuration.clone(), config.codex_home.clone());

let services = SessionServices {
mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::default())),
Expand Down Expand Up @@ -1197,6 +1199,50 @@ impl Session {
rx_approve
}

pub async fn dependency_env(&self) -> HashMap<String, String> {
let state = self.state.lock().await;
state.dependency_env()
}

pub async fn set_dependency_env(&self, values: HashMap<String, String>) {
let mut state = self.state.lock().await;
state.set_dependency_env(values);
}

pub async fn codex_home(&self) -> PathBuf {
let state = self.state.lock().await;
state.codex_home()
}

pub async fn insert_pending_skill_dependencies(
&self,
request_id: String,
tx_response: oneshot::Sender<SkillDependencyResponse>,
) -> Option<oneshot::Sender<SkillDependencyResponse>> {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.insert_pending_skill_dependencies(request_id, tx_response)
}
None => None,
}
}

pub async fn remove_pending_skill_dependencies(
&self,
request_id: &str,
) -> Option<oneshot::Sender<SkillDependencyResponse>> {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
ts.remove_pending_skill_dependencies(request_id)
}
None => None,
}
}

pub async fn notify_approval(&self, sub_id: &str, decision: ReviewDecision) {
let entry = {
let mut active = self.active_turn.lock().await;
Expand Down Expand Up @@ -1725,6 +1771,9 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
} => {
handlers::resolve_elicitation(&sess, server_name, request_id, decision).await;
}
Op::ResolveSkillDependencies { id, values } => {
handlers::resolve_skill_dependencies(&sess, id, values).await;
}
Op::Shutdown => {
if handlers::shutdown(&sess, sub.id.clone()).await {
break;
Expand All @@ -1751,6 +1800,8 @@ mod handlers {
use crate::mcp::auth::compute_auth_statuses;
use crate::mcp::collect_mcp_snapshot_from_manager;
use crate::review_prompts::resolve_review_request;
use crate::skills::SkillDependencyResponse;
use crate::skills::handle_skill_dependency_response;
use crate::tasks::CompactTask;
use crate::tasks::RegularTask;
use crate::tasks::UndoTask;
Expand All @@ -1775,6 +1826,7 @@ mod handlers {
use codex_rmcp_client::ElicitationAction;
use codex_rmcp_client::ElicitationResponse;
use mcp_types::RequestId;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
Expand Down Expand Up @@ -1908,6 +1960,15 @@ mod handlers {
}
}

pub async fn resolve_skill_dependencies(
sess: &Arc<Session>,
request_id: String,
values: HashMap<String, String>,
) {
handle_skill_dependency_response(sess, &request_id, SkillDependencyResponse { values })
.await;
}

/// Propagate a user's exec approval decision to the session.
/// Also optionally applies an execpolicy amendment.
pub async fn exec_approval(sess: &Arc<Session>, id: String, decision: ReviewDecision) {
Expand Down Expand Up @@ -2354,10 +2415,11 @@ pub(crate) async fn run_task(
.await,
);

let SkillInjections {
let SkillTurnPrep {
items: skill_items,
dependencies: skill_dependencies,
warnings: skill_warnings,
} = build_skill_injections(&input, skills_outcome.as_ref()).await;
} = build_skill_turn_prep(&input, skills_outcome.as_ref()).await;

for message in skill_warnings {
sess.send_event(&turn_context, EventMsg::Warning(WarningEvent { message }))
Expand All @@ -2369,6 +2431,8 @@ pub(crate) async fn run_task(
sess.record_response_item_and_emit_turn_item(turn_context.as_ref(), response_item)
.await;

resolve_skill_dependencies_for_turn(&sess, &turn_context, &skill_dependencies).await;

if !skill_items.is_empty() {
sess.record_conversation_items(&turn_context, &skill_items)
.await;
Expand Down Expand Up @@ -3178,7 +3242,7 @@ mod tests {
session_source: SessionSource::Exec,
};

let mut state = SessionState::new(session_configuration);
let mut state = SessionState::new(session_configuration, config.codex_home.clone());
let initial = RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 10.0,
Expand Down Expand Up @@ -3244,7 +3308,7 @@ mod tests {
session_source: SessionSource::Exec,
};

let mut state = SessionState::new(session_configuration);
let mut state = SessionState::new(session_configuration, config.codex_home.clone());
let initial = RateLimitSnapshot {
primary: Some(RateLimitWindow {
used_percent: 15.0,
Expand Down Expand Up @@ -3505,7 +3569,7 @@ mod tests {
session_configuration.session_source.clone(),
);

let state = SessionState::new(session_configuration.clone());
let state = SessionState::new(session_configuration.clone(), config.codex_home.clone());
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone()));

let services = SessionServices {
Expand Down Expand Up @@ -3599,7 +3663,7 @@ mod tests {
session_configuration.session_source.clone(),
);

let state = SessionState::new(session_configuration.clone());
let state = SessionState::new(session_configuration.clone(), config.codex_home.clone());
let skills_manager = Arc::new(SkillsManager::new(config.codex_home.clone()));

let services = SessionServices {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/rollout/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
| EventMsg::ExecApprovalRequest(_)
| EventMsg::ElicitationRequest(_)
| EventMsg::ApplyPatchApprovalRequest(_)
| EventMsg::SkillDependencyRequest(_)
| EventMsg::BackgroundEvent(_)
| EventMsg::StreamError(_)
| EventMsg::PatchApplyBegin(_)
Expand Down
89 changes: 89 additions & 0 deletions codex-rs/core/src/skills/dependencies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use serde::Deserialize;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SkillDependency {
pub(crate) name: String,
pub(crate) description: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SkillDependencyInfo {
pub(crate) skill_name: String,
pub(crate) dependency: SkillDependency,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SkillDependencyResponse {
pub(crate) values: std::collections::HashMap<String, String>,
}

#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
#[serde(default)]
dependencies: Vec<SkillDependencyEntry>,
}

#[derive(Debug, Deserialize)]
struct SkillDependencyEntry {
#[serde(rename = "type")]
dep_type: String,
name: Option<String>,
description: Option<String>,
}

pub(crate) fn parse_env_var_dependencies(contents: &str) -> Vec<SkillDependency> {
let Some(frontmatter) = extract_frontmatter(contents) else {
return Vec::new();
};

let parsed: SkillFrontmatter = match serde_yaml::from_str(&frontmatter) {
Ok(parsed) => parsed,
Err(_) => return Vec::new(),
};

parsed
.dependencies
.into_iter()
.filter_map(|entry| {
if entry.dep_type != "env_var" {
return None;
}
let name = entry.name.map(|value| sanitize_single_line(&value))?;
if name.is_empty() {
return None;
}
let description = entry
.description
.map(|value| sanitize_single_line(&value))
.filter(|value| !value.is_empty());
Some(SkillDependency { name, description })
})
.collect()
}

fn extract_frontmatter(contents: &str) -> Option<String> {
let mut lines = contents.lines();
if !matches!(lines.next(), Some(line) if line.trim() == "---") {
return None;
}

let mut frontmatter_lines: Vec<&str> = Vec::new();
let mut found_closing = false;
for line in lines.by_ref() {
if line.trim() == "---" {
found_closing = true;
break;
}
frontmatter_lines.push(line);
}

if frontmatter_lines.is_empty() || !found_closing {
return None;
}

Some(frontmatter_lines.join("\n"))
}

fn sanitize_single_line(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
Loading
Loading