skills: render catalog locators by authority (#27591)

## Why

Hosted skills introduced by #27388 use opaque `skill://` resource
identifiers, but the skills catalog rendered every locator as a `file`
and told the model that every skill body lived on disk. That can send
the model toward filesystem tools for a resource that must instead be
read through its owning authority.

The catalog should describe how each source is accessed without changing
the underlying discovery or invocation behavior.

## What changed

- Render host skills as `file`, executor-owned skills as `environment
resource`, orchestrator-owned skills as `orchestrator resource`, and
custom-provider skills as `custom resource`.
- Update the shared no-alias guidance to describe source locators rather
than assuming every skill is stored on the host filesystem.
- Direct orchestrator resources through `skills.list` and `skills.read`,
and explicitly tell the model not to treat `skill://` identifiers as
filesystem paths.
- Preserve the existing filesystem and alias behavior for local skills.

## Scope

This PR changes only model-visible catalog rendering and guidance. It
does not change skill discovery, selection, prompt injection, provider
routing, catalog caching or refresh behavior, resource validation, or
the `skills.*` tool contract.

## Verification

- Extended skills-extension coverage for host-file and executor-resource
labels.
- Extended the no-executor app-server flow to assert
orchestrator-resource wording and non-filesystem guidance.
This commit is contained in:
jif
2026-06-11 12:51:04 +01:00
committed by GitHub
Unverified
parent dd46ef1aac
commit 4322e01d2b
4 changed files with 40 additions and 13 deletions
@@ -217,7 +217,9 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -
assert!(first_request.tool_by_name("skills", "search").is_none());
let developer_messages = first_request.message_input_texts("developer");
let catalog_line = format!("- {SKILL_NAME}: {SKILL_DESCRIPTION} (file: {SKILL_RESOURCE_URI})");
let catalog_line = format!(
"- {SKILL_NAME}: {SKILL_DESCRIPTION} (orchestrator resource: {SKILL_RESOURCE_URI})"
);
assert_eq!(
1,
developer_messages
@@ -230,6 +232,11 @@ async fn orchestrator_skill_can_read_referenced_resource_without_an_executor() -
.iter()
.all(|text| !text.contains("ignored-plugin:ignored"))
);
assert!(
developer_messages
.iter()
.any(|text| text.contains("do not treat `skill://` identifiers as filesystem paths"))
);
let skill_fragments = first_request
.message_input_texts("user")
.into_iter()
+8 -8
View File
@@ -22,17 +22,17 @@ pub const SKILL_DESCRIPTION_TRUNCATED_WARNING: &str = "Skill descriptions were s
pub const SKILL_DESCRIPTION_TRUNCATED_WARNING_WITH_PERCENT: &str = "Skill descriptions were shortened to fit the 2% skills context budget. Codex can still see every skill, but some descriptions are shorter. Disable unused skills or plugins to leave more room for the rest.";
pub const SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX: &str =
"Exceeded skills context budget. All skill descriptions were removed and";
pub const SKILLS_INTRO_WITH_ABSOLUTE_PATHS: &str = "A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.";
pub const SKILLS_INTRO_WITH_ABSOLUTE_PATHS: &str = "A skill is a set of instructions provided through a `SKILL.md` source. Below is the list of skills that can be used. Each entry includes a name, description, and source locator. `file` locators are on the host filesystem, `environment resource` locators are owned by an execution environment, `orchestrator resource` locators are opaque non-filesystem resources, and `custom resource` locators use their provider's access mechanism.";
pub const SKILLS_INTRO_WITH_ALIASES: &str = "A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and a short path that can be expanded into an absolute path using the skill roots table.";
pub const SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS: &str = r###"- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
pub const SKILLS_HOW_TO_USE_WITH_ABSOLUTE_PATHS: &str = r###"- Discovery: The list above is the skills available in this session (name + description + source locator). `file` entries live on the host filesystem, `environment resource` entries are owned by their execution environment, `orchestrator resource` entries must be accessed through `skills.list` and `skills.read`, and `custom resource` entries use their provider's access mechanism.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- Missing/blocked: If a named skill isn't in the list or its source can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
1) After deciding to use a skill, the main agent must open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
1) After deciding to use a skill, the main agent must read its `SKILL.md` completely before taking task actions. For a `file` entry, open the listed path. For an `environment resource`, use the filesystem of the owning environment. For an `orchestrator resource`, call `skills.list` with `{"authority":{"kind":"orchestrator"}}`, select the matching package, and pass its `main_resource` to `skills.read`. If a read is truncated or paginated, continue until EOF.
2) When `SKILL.md` references another resource, use the same access mechanism. Resolve relative paths against a filesystem-backed skill directory. For orchestrator skills, pass the exact referenced resource identifier with the same authority and package to `skills.read`; do not treat `skill://` identifiers as filesystem paths.
3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the resources required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
4) For filesystem-backed skills, prefer running or patching provided scripts instead of retyping large code blocks. For orchestrator skills, use `skills.read` and the available tools; do not invent a local path.
5) Reuse provided assets or templates through the same source access mechanism instead of recreating them.
- Coordination and sequencing:
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
+14 -4
View File
@@ -2,6 +2,8 @@ use codex_core::context::AvailableSkillsInstructions;
use codex_utils_string::take_bytes_at_char_boundary;
use crate::catalog::SkillCatalog;
use crate::catalog::SkillCatalogEntry;
use crate::catalog::SkillSourceKind;
const MAX_AVAILABLE_SKILLS_BYTES: usize = 8_000;
const MAX_MAIN_PROMPT_BYTES: usize = 8_000;
@@ -24,7 +26,7 @@ pub(crate) fn available_skills_fragment(
.short_description
.as_deref()
.unwrap_or(entry.description.as_str());
let line = render_skill_line(entry.name.as_str(), description, entry.rendered_path());
let line = render_skill_line(entry, description);
let next_bytes = total_bytes.saturating_add(line.len());
if next_bytes > MAX_AVAILABLE_SKILLS_BYTES {
omitted = omitted.saturating_add(1);
@@ -47,11 +49,19 @@ pub(crate) fn available_skills_fragment(
Some(AvailableSkillsInstructions::from_skill_lines(skill_lines))
}
fn render_skill_line(name: &str, description: &str, path: &str) -> String {
fn render_skill_line(entry: &SkillCatalogEntry, description: &str) -> String {
let locator_kind = match &entry.authority.kind {
SkillSourceKind::Host => "file",
SkillSourceKind::Executor => "environment resource",
SkillSourceKind::Orchestrator => "orchestrator resource",
SkillSourceKind::Custom(_) => "custom resource",
};
let name = entry.name.as_str();
let path = entry.rendered_path();
if description.is_empty() {
format!("- {name}: (file: {path})")
format!("- {name}: ({locator_kind}: {path})")
} else {
format!("- {name}: {description} (file: {path})")
format!("- {name}: {description} ({locator_kind}: {path})")
}
}
@@ -120,6 +120,11 @@ async fn installed_extension_loads_host_skills_from_legacy_roots() -> TestResult
assert_eq!("developer", fragments[0].role());
assert!(fragments[0].render().contains("demo"));
assert!(fragments[0].render().contains(&skill_prompt_path));
assert!(
fragments[0]
.render()
.contains(&format!("(file: {skill_prompt_path})"))
);
assert_eq!("user", fragments[1].role());
assert!(fragments[1].render().contains("<name>demo</name>"));
assert!(fragments[1].render().contains("# Demo"));
@@ -187,6 +192,11 @@ async fn selected_executor_catalog_is_context_and_selected_entrypoint_is_turn_in
.starts_with(SKILLS_INSTRUCTIONS_OPEN_TAG)
);
assert!(prompt_fragments[0].text().contains("lint-fix"));
assert!(
prompt_fragments[0]
.text()
.contains("(environment resource: skill://executor/lint-fix/SKILL.md)")
);
let turn_store = ExtensionData::new("turn-1");
let fragments = registry.turn_input_contributors()[0]