mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
## Why Repository skill discovery must reflect current filesystem access, including newly approved grants and narrower permissions. Previously listed skills can remain in conversation history after a turn-scoped grant expires, so catalog updates need to tell the model when availability changes. ## What changed - Discover repository skills through the selected executor's permission-aware filesystem accessor and refresh snapshots when the working directory or access changes. Use the current step's snapshot for skill selection and invocation. - Preserve repository boundaries even when project markers are hidden by read restrictions, and report unsupported sandbox backends as skill-loading errors. - Track grant-dependent catalog entries and emit availability notices when skills disappear or return, even when the metadata budget is exhausted. - Resolve implicit skill paths through their owning filesystem and exclude remote repository skills from local implicit command attribution. ## Testing Add coverage for turn and session grants, grant expiry and restoration, hidden and newly created repository markers, cached read denials on Windows, unsupported remote sandboxes, and executor-backed implicit skill paths. GitOrigin-RevId: ac1af483e73e3130524aeeabe218630435054028
117 lines
4.2 KiB
Rust
117 lines
4.2 KiB
Rust
use codex_analytics::InvocationType;
|
|
use codex_analytics::SkillInvocation;
|
|
use codex_analytics::SkillInvocationLocation;
|
|
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
|
use codex_extension_api::ExtensionData;
|
|
use codex_skills::ImplicitSkillAccess;
|
|
use codex_skills::detect_implicit_skill_invocation_for_command;
|
|
use codex_skills::implicit_skill_accesses_for_command;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_path_uri::PathUri;
|
|
|
|
use crate::HostSkillsSnapshot;
|
|
use crate::catalog::SkillAuthority;
|
|
use crate::catalog::SkillCatalogEntry;
|
|
use crate::catalog::SkillResourceId;
|
|
use crate::catalog::SkillSourceKind;
|
|
use crate::catalog::SkillSourceLocation;
|
|
|
|
impl SkillCatalogEntry {
|
|
/// Uses merged identity and scope, with plugin attribution from the route actually used.
|
|
pub(crate) fn invocation(
|
|
&self,
|
|
source: &SkillSourceLocation,
|
|
invocation_type: InvocationType,
|
|
) -> SkillInvocation {
|
|
SkillInvocation {
|
|
skill_name: self.name.clone(),
|
|
location: SkillInvocationLocation::Resource {
|
|
id: self.main_prompt.as_str().to_owned(),
|
|
skill_id: self.canonical_skill_id.clone(),
|
|
scope: self.analytics_scope,
|
|
},
|
|
plugin_id: source.plugin_id.clone(),
|
|
remote_plugin_id: self.remote_plugin_id.clone(),
|
|
invocation_type,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Identifies the executor-owned or host-owned skill referenced by a command.
|
|
pub fn detect_implicit_skill_invocation(
|
|
turn_store: &ExtensionData,
|
|
step_store: &ExtensionData,
|
|
environment_id: &str,
|
|
command: &str,
|
|
workdir: &PathUri,
|
|
native_workdir: Option<&AbsolutePathBuf>,
|
|
) -> Option<SkillInvocation> {
|
|
if environment_id == LOCAL_ENVIRONMENT_ID
|
|
&& let Some(native_workdir) = native_workdir
|
|
&& let Some(host_snapshot) = HostSkillsSnapshot::for_step(step_store, turn_store)
|
|
&& let Some(skill) = detect_implicit_skill_invocation_for_command(
|
|
host_snapshot.outcome(),
|
|
command,
|
|
native_workdir,
|
|
)
|
|
{
|
|
return Some(SkillInvocation {
|
|
skill_name: skill.name,
|
|
location: SkillInvocationLocation::Host {
|
|
path: skill.path_to_skills_md,
|
|
scope: skill.scope,
|
|
},
|
|
plugin_id: skill.plugin_id,
|
|
remote_plugin_id: skill.remote_plugin_id,
|
|
invocation_type: InvocationType::Implicit,
|
|
});
|
|
}
|
|
|
|
let accesses = implicit_skill_accesses_for_command(command, workdir);
|
|
if accesses.is_empty() {
|
|
return None;
|
|
}
|
|
let step_state = step_store.get::<crate::step_state::SkillsStepState>()?;
|
|
let step_catalog = &step_state.catalog;
|
|
for access in accesses {
|
|
let matches_route = |authority: &SkillAuthority, main_prompt: &SkillResourceId| {
|
|
if authority.kind != SkillSourceKind::Executor
|
|
|| main_prompt.environment_id() != Some(environment_id)
|
|
{
|
|
return false;
|
|
}
|
|
let Some((_, skill_path)) = main_prompt.environment_path() else {
|
|
return false;
|
|
};
|
|
match &access {
|
|
ImplicitSkillAccess::Document(path) => path == skill_path,
|
|
ImplicitSkillAccess::Script(path) => skill_path
|
|
.parent()
|
|
.and_then(|skill_dir| skill_dir.join("scripts").ok())
|
|
.is_some_and(|scripts_dir| path.starts_with(&scripts_dir)),
|
|
}
|
|
};
|
|
for entry in step_catalog.merged.entries.iter().filter(|entry| {
|
|
matches!(
|
|
entry.authority.kind,
|
|
SkillSourceKind::Cloud | SkillSourceKind::Executor
|
|
)
|
|
}) {
|
|
if entry.enabled && matches_route(&entry.authority, &entry.main_prompt) {
|
|
return Some(entry.invocation(&entry.primary_source(), InvocationType::Implicit));
|
|
}
|
|
if let Some(source) = entry.sources.iter().find(|source| {
|
|
source.enabled && matches_route(&source.authority, &source.main_prompt)
|
|
}) {
|
|
return Some(entry.invocation(source, InvocationType::Implicit));
|
|
}
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "invocation_tests.rs"]
|
|
mod tests;
|