This commit is contained in:
Ahmed Ibrahim
2026-03-25 01:08:48 -07:00
parent e4b154f9ff
commit d46d7161b3
6 changed files with 20 additions and 223 deletions
@@ -1,70 +1,13 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::sync::Arc;
use codex_protocol::request_user_input::RequestUserInputArgs;
use codex_protocol::request_user_input::RequestUserInputQuestion;
use codex_protocol::request_user_input::RequestUserInputResponse;
use tracing::warn;
use crate::codex::Session;
use crate::skills::SkillMetadata;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SkillDependencyInfo {
pub(crate) skill_name: String,
pub(crate) name: String,
pub(crate) description: Option<String>,
pub struct SkillDependencyInfo {
pub skill_name: String,
pub name: String,
pub description: Option<String>,
}
/// Resolve required dependency values (session cache, then env vars),
/// and prompt the UI for any missing ones.
pub(crate) async fn resolve_skill_dependencies_for_turn(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
dependencies: &[SkillDependencyInfo],
) {
if dependencies.is_empty() {
return;
}
let existing_env = sess.dependency_env().await;
let mut loaded_values = HashMap::new();
let mut missing = Vec::new();
let mut seen_names = HashSet::new();
for dependency in dependencies {
let name = dependency.name.clone();
if !seen_names.insert(name.clone()) {
continue;
}
if existing_env.contains_key(&name) {
continue;
}
match env::var(&name) {
Ok(value) => {
loaded_values.insert(name.clone(), value);
continue;
}
Err(env::VarError::NotPresent) => {}
Err(err) => {
warn!("failed to read env var {name}: {err}");
}
}
missing.push(dependency.clone());
}
if !loaded_values.is_empty() {
sess.set_dependency_env(loaded_values).await;
}
if !missing.is_empty() {
request_skill_dependencies(sess, turn_context, &missing).await;
}
}
pub(crate) fn collect_env_var_dependencies(
pub fn collect_env_var_dependencies(
mentioned_skills: &[SkillMetadata],
) -> Vec<SkillDependencyInfo> {
let mut dependencies = Vec::new();
@@ -73,10 +16,7 @@ pub(crate) fn collect_env_var_dependencies(
continue;
};
for tool in &skill_dependencies.tools {
if tool.r#type != "env_var" {
continue;
}
if tool.value.is_empty() {
if tool.r#type != "env_var" || tool.value.is_empty() {
continue;
}
dependencies.push(SkillDependencyInfo {
@@ -88,74 +28,3 @@ pub(crate) fn collect_env_var_dependencies(
}
dependencies
}
/// Prompt via request_user_input to gather missing env vars.
pub(crate) async fn request_skill_dependencies(
sess: &Arc<Session>,
turn_context: &Arc<TurnContext>,
dependencies: &[SkillDependencyInfo],
) {
let questions = dependencies
.iter()
.map(|dep| {
let requirement = dep.description.as_ref().map_or_else(
|| format!("The skill \"{}\" requires \"{}\" to be set.", dep.skill_name, dep.name),
|description| {
format!(
"The skill \"{}\" requires \"{}\" to be set ({}).",
dep.skill_name, dep.name, description
)
},
);
let question = format!(
"{requirement} This is an experimental internal feature. The value is stored in memory for this session only.",
);
RequestUserInputQuestion {
id: dep.name.clone(),
header: "Skill requires environment variable".to_string(),
question,
is_other: false,
is_secret: true,
options: None,
}
})
.collect::<Vec<_>>();
if questions.is_empty() {
return;
}
let args = RequestUserInputArgs { questions };
let call_id = format!("skill-deps-{}", turn_context.sub_id);
let response = sess
.request_user_input(turn_context, call_id, args)
.await
.unwrap_or_else(|| RequestUserInputResponse {
answers: HashMap::new(),
});
if response.answers.is_empty() {
return;
}
let mut values = HashMap::new();
for (name, answer) in response.answers {
let mut user_note = None;
for entry in &answer.answers {
if let Some(note) = entry.strip_prefix("user_note: ")
&& !note.trim().is_empty()
{
user_note = Some(note.trim().to_string());
}
}
if let Some(value) = user_note {
values.insert(name, value);
}
}
if values.is_empty() {
return;
}
sess.set_dependency_env(values).await;
}
@@ -1,15 +1,9 @@
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use crate::codex::Session;
use crate::skills::SkillLoadOutcome;
use crate::skills::SkillMetadata;
use codex_analytics::InvocationType;
use codex_analytics::SkillInvocation;
use codex_analytics::build_track_events_context;
use tokio::sync::Mutex;
pub(crate) fn build_implicit_skill_path_indexes(
skills: Vec<SkillMetadata>,
@@ -32,10 +26,10 @@ pub(crate) fn build_implicit_skill_path_indexes(
(by_scripts_dir, by_skill_doc_path)
}
fn detect_implicit_skill_invocation_for_command(
pub fn detect_implicit_skill_invocation_for_command(
outcome: &SkillLoadOutcome,
command: &str,
workdir: &Path
workdir: &Path,
) -> Option<SkillMetadata> {
let workdir = normalize_path(workdir);
let tokens = tokenize_command(command);
@@ -45,78 +39,12 @@ fn detect_implicit_skill_invocation_for_command(
return Some(candidate);
}
if let Some(candidate) = detect_skill_doc_read(outcome, tokens.as_slice(), workdir.as_path()) {
return Some(candidate);
}
None
}
pub(crate) async fn maybe_emit_implicit_skill_invocation(
sess: &Session,
outcome: &SkillLoadOutcome,
command: &str,
seen_skills: &Mutex<HashSet<String>>,
workdir: &Path,
) {
let Some(candidate) = detect_implicit_skill_invocation_for_command(
&outcome,
command,
&workdir,
) else {
return;
};
let invocation = SkillInvocation {
skill_name: candidate.name,
skill_scope: candidate.scope,
skill_path: candidate.path_to_skills_md,
invocation_type: InvocationType::Implicit,
};
let skill_scope = match invocation.skill_scope {
codex_protocol::protocol::SkillScope::User => "user",
codex_protocol::protocol::SkillScope::Repo => "repo",
codex_protocol::protocol::SkillScope::System => "system",
codex_protocol::protocol::SkillScope::Admin => "admin",
};
let skill_path = invocation.skill_path.to_string_lossy();
let skill_name = invocation.skill_name.clone();
let seen_key = format!("{skill_scope}:{skill_path}:{skill_name}");
let inserted = {
let mut seen_skills = seen_skills.lock().await;
seen_skills.insert(seen_key)
};
if !inserted {
return;
}
turn_context.session_telemetry.counter(
"codex.skill.injected",
/*inc*/ 1,
&[
("status", "ok"),
("skill", skill_name.as_str()),
("invoke_type", "implicit"),
],
);
sess.services
.analytics_events_client
.track_skill_invocations(
build_track_events_context(
turn_context.model_info.slug.clone(),
sess.conversation_id.to_string(),
turn_context.sub_id.clone(),
),
vec![invocation],
);
detect_skill_doc_read(outcome, tokens.as_slice(), workdir.as_path())
}
fn tokenize_command(command: &str) -> Vec<String> {
shlex::split(command).unwrap_or_else(|| {
command
.split_whitespace()
.map(std::string::ToString::to_string)
.collect()
})
shlex::split(command)
.unwrap_or_else(|| command.split_whitespace().map(str::to_string).collect())
}
fn script_run_token(tokens: &[String]) -> Option<&str> {
@@ -132,12 +60,9 @@ fn script_run_token(tokens: &[String]) -> Option<&str> {
return None;
}
let mut script_token: Option<&str> = None;
let mut script_token = None;
for token in tokens.iter().skip(1) {
if token == "--" {
continue;
}
if token.starts_with('-') {
if token == "--" || token.starts_with('-') {
continue;
}
script_token = Some(token.as_str());
+4 -1
View File
@@ -1,4 +1,4 @@
pub(crate) mod config_rules;
pub mod config_rules;
mod env_var_dependencies;
pub mod injection;
pub(crate) mod invocation_utils;
@@ -10,7 +10,10 @@ pub mod remote;
pub mod render;
pub mod system;
pub use env_var_dependencies::SkillDependencyInfo;
pub use env_var_dependencies::collect_env_var_dependencies;
pub(crate) use invocation_utils::build_implicit_skill_path_indexes;
pub use invocation_utils::detect_implicit_skill_invocation_for_command;
pub use manager::SkillsManager;
pub use mention_counts::build_skill_name_counts;
pub use model::SkillError;
+1 -1
View File
@@ -80,6 +80,7 @@ pub mod sandboxing;
mod session_prefix;
mod session_startup_prewarm;
mod shell_detect;
pub mod skills;
mod skills_watcher;
mod stream_events_utils;
pub mod test_support;
@@ -133,7 +134,6 @@ pub mod seatbelt;
mod session_rollout_init_error;
pub mod shell;
pub mod shell_snapshot;
pub use codex_core_skills::skills;
pub mod spawn;
pub mod state_db_bridge;
pub use codex_rollout::state_db;
+1 -1
View File
@@ -291,7 +291,7 @@ impl ToolHandler for ShellCommandHandler {
let workdir = turn.resolve_path(params.workdir.clone());
maybe_emit_implicit_skill_invocation(
session.as_ref(),
&turn_skills.outcome,
turn.as_ref(),
&params.command,
&workdir,
)
@@ -148,7 +148,7 @@ impl ToolHandler for UnifiedExecHandler {
let workdir = context.turn.resolve_path(args.workdir.clone());
maybe_emit_implicit_skill_invocation(
session.as_ref(),
&context.turn_skills.outcome,
context.turn.as_ref(),
&args.cmd,
&workdir,
)