From 67b2c3d17660f4d028dc88ef3910c20a6b2b4bc2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 14 May 2026 02:28:00 +0100 Subject: [PATCH] Delete Codex core skills crate --- notes/fresh-workspace-handoff.md | 10 +- state/ledgers.msgpack | Bin 108532 -> 108851 bytes state/map.yaml | 8 +- vendor/codex/codex-rs/Cargo.lock | 49 - vendor/codex/codex-rs/Cargo.toml | 2 - vendor/codex/codex-rs/core-skills/BUILD.bazel | 15 - vendor/codex/codex-rs/core-skills/Cargo.toml | 39 - .../codex-rs/core-skills/src/config_rules.rs | 128 -- .../core-skills/src/env_var_dependencies.rs | 30 - .../codex-rs/core-skills/src/injection.rs | 455 ----- .../core-skills/src/injection_tests.rs | 293 --- .../core-skills/src/invocation_utils.rs | 145 -- .../core-skills/src/invocation_utils_tests.rs | 122 -- vendor/codex/codex-rs/core-skills/src/lib.rs | 29 - .../codex/codex-rs/core-skills/src/loader.rs | 946 --------- .../codex-rs/core-skills/src/loader_tests.rs | 1724 ----------------- .../codex/codex-rs/core-skills/src/manager.rs | 310 --- .../codex-rs/core-skills/src/manager_tests.rs | 750 ------- .../core-skills/src/mention_counts.rs | 24 - .../codex/codex-rs/core-skills/src/model.rs | 196 -- .../codex/codex-rs/core-skills/src/remote.rs | 270 --- .../codex/codex-rs/core-skills/src/render.rs | 755 -------- .../codex/codex-rs/core-skills/src/system.rs | 15 - vendor/codex/codex-rs/core/Cargo.toml | 1 - .../codex/codex-rs/core/src/codex_delegate.rs | 2 - vendor/codex/codex-rs/core/src/config/mod.rs | 4 - .../core/src/context/skill_instructions.rs | 12 - vendor/codex/codex-rs/core/src/lib.rs | 5 - vendor/codex/codex-rs/core/src/session/mod.rs | 10 - .../codex/codex-rs/core/src/session/review.rs | 1 - .../codex-rs/core/src/session/session.rs | 4 - .../codex/codex-rs/core/src/session/tests.rs | 410 +--- .../core/src/session/tests/guardian_tests.rs | 7 - .../codex-rs/core/src/session/turn_context.rs | 17 - vendor/codex/codex-rs/core/src/skills.rs | 25 - .../codex/codex-rs/core/src/skills_watcher.rs | 9 - .../codex/codex-rs/core/src/state/service.rs | 4 - .../codex/codex-rs/core/src/thread_manager.rs | 38 - .../codex/codex-rs/tui/src/bottom_pane/mod.rs | 1 - 39 files changed, 15 insertions(+), 6850 deletions(-) delete mode 100644 vendor/codex/codex-rs/core-skills/BUILD.bazel delete mode 100644 vendor/codex/codex-rs/core-skills/Cargo.toml delete mode 100644 vendor/codex/codex-rs/core-skills/src/config_rules.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/env_var_dependencies.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/injection.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/injection_tests.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/invocation_utils.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/invocation_utils_tests.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/lib.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/loader.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/loader_tests.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/manager.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/manager_tests.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/mention_counts.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/model.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/remote.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/render.rs delete mode 100644 vendor/codex/codex-rs/core-skills/src/system.rs delete mode 100644 vendor/codex/codex-rs/core/src/skills.rs delete mode 100644 vendor/codex/codex-rs/core/src/skills_watcher.rs diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index bcfa63c5..e9d2e268 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -902,9 +902,13 @@ than repaired into new compatibility lies. The live skill mention channel is also gone. `UserInput::Skill`, the app-server schema branch, TUI skill popup, `skills_all` cache, composer skill mention API, skill insertion path, `skill://` / `SKILL.md` history decoding, and stale skill -popup snapshot were deleted. Live structured user-input mentions are MCP-shaped -only; MCP remains JSON-RPC at its protocol edge, not a reason to keep Codex -skills alive inside the organ. +popup snapshot were deleted. The deeper `codex-core-skills` crate is now deleted +from the workspace too, along with the core `skills` re-export shim, no-op +`skills_watcher`, session `SkillsManager` service, and per-turn +`TurnSkillsContext`; core keeps only a local legacy `` fragment recognizer +for old history classification. Live structured user-input mentions are +MCP-shaped only; MCP remains JSON-RPC at its protocol edge, not a reason to keep +Codex skills alive inside the organ. That husk has now been cut from `codex-core`. The root core crate no longer exports `plugins`, no longer depends on `codex-core-plugins` or `codex-plugin`, diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index e3181125a14840cf205e9e08da9926a9c9e7faa7..1c06da5e2ae38ae077eefbcd9cf9a481fda3af28 100644 GIT binary patch delta 306 zcmXX?u}T9$5M?bQHkP&yie1jT92bjNDp*+=5YnxCbC>1zc9z|}OHwJ=SX!As5Zj<& zlYbEG{0|$!!sTN3-n@D99`pUtd;RP^9&Ww-td5=-+rMA4_cdb&&&!7kw!58;lffv> zmS)2Gw_m^KsL*2$E$W2>ou`yNI{rDb9bTZ{~h%ybP*bq!5I3=FJHjjfDL Fq5#vO5B&fD diff --git a/state/map.yaml b/state/map.yaml index 6be8c404..a1c2a7e2 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -280,7 +280,7 @@ rejected_paths: reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet. current_status: - summary: The Codex starvation pass has five typing receipts, forty-eight processor/foundation cuts, a real physical extraction in `epiphany-codex-bridge`, a root `epiphany-state-model` crate for durable Epiphany state, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, native state-update contract/validation/application in `epiphany-core`, native exact retrieval path matching in `epiphany-core`, and the route-facing mutation/view/coordinator projection services in `epiphany-codex-bridge`. `codex-protocol` no longer defines the durable `Epiphany*` state structs; it re-exports the native model for legacy rollout/API compatibility while `epiphany-core` imports the native model directly. `epiphany-core` now has no direct vendored Codex dependency: pressure derivation accepts native `EpiphanyTokenUsageSnapshot`, Codex rollout replay moved to `codex-core::epiphany_rollout`, and exact retrieval search uses Epiphany-owned workspace walking/path scoring instead of `codex-file-search`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, watcher invalidation adaptation, full `thread/epiphany/view` response construction, freshness/context/graph-query response construction, role/reorient result response construction, distill/propose patch-response construction, coordinator status/view construction, and acceptance/evidence signal helpers. Vendored app-server no longer retains root Epiphany modules; `codex_message_processor.rs` still contains route dispatch, JSON-RPC response shaping, watcher registration/snapshotting calls, and notification emission, but its root no longer imports the evacuated Epiphany policy/protocol registry or owns the watcher invalidation manager. Codex product surfaces for apps, skills, plugins, marketplace, and external agent config import are now inert in the app-server compatibility path: list routes return empty lists, mutation/detail/import routes report disabled, `externalAgentConfig/detect` returns no import candidates, helper/import modules and stale implementation-shape tests were deleted, runtime feature enablement no longer supports `apps` or `plugins`, config writes no longer emit plugin-toggle analytics, plugin/skill cache invalidation on config writes is gone, plugin startup warmups and app-list refreshes were removed, and app-server no longer directly depends on `codex-plugin`, `codex-chatgpt`, or `codex-core-plugins`. `Config::to_mcp_config` now keeps user-declared MCP servers but no longer accepts `PluginsManager`, folds plugin MCP servers into the runtime, enables Codex apps, enables skill MCP dependency install, or advertises plugin capability summaries; `TurnContext::apps_enabled()` now returns false so model turns cannot awaken Codex Apps through ChatGPT auth or feature flags; session startup and per-turn context no longer load skills, turns receive an empty `SkillLoadOutcome`, internal `Op::ListSkills` is gone from `codex-protocol` / `codex-core` / TUI command routing, the Codex skill watcher has been collapsed to a no-op marker with no file watcher, skill-root registration, cache-clear task, or `SkillsUpdateAvailable` session listener, model turns no longer run skill mention collection, skill env-var prompts, skill MCP dependency installation, skill instruction injection, or implicit skill invocation analytics from shell tools, and the turn prompt/router path no longer loads plugin/app capability summaries, injects available-skill/available-plugin/plugin instruction fragments, parses plugin/app mentions, records app/plugin invocation telemetry, or filters MCP tools through Codex app connectors. Skill mutation/list/notification is gone from the app-server edge too: `skills/list`, `skills/config/write`, `skills/changed`, `Op::ListSkills`, `EventMsg::ListSkillsResponse`, `EventMsg::SkillsUpdateAvailable`, the app-server handlers, TUI slash/startup/list/manage-skill surfaces, generated schema entries, core skill-list suite, and fake live-reload/developer-message tests were deleted. Skill mention plumbing is also gone from the live TUI/protocol edge: no `UserInput::Skill` variant, app-server schema branch, TUI skill popup, `skills_all` cache, composer `set_skill_mentions`, skill mention insertion, `skill://` / `SKILL.md` history decoding, or stale skill popup snapshot remains. The TUI app connector product path is also deleted: no `/apps` slash command, app-link view, connector AppEvent variants, app connector cache/popup/refresh loop, app mention insertion, `app://` mention codec support, stale app-popup tests, or TUI browser-opening shim remains. The `app/list` route and `app/list/updated` notification are gone from app-server protocol, generated schemas, app-server dispatch, test helpers, and docs; `AppInfo`/app-list metadata schema structs are deleted; the orphaned `codex-connectors` crate and `core::connectors` shim are deleted from the workspace; Codex app analytics no longer tracks `codex_app_mentioned` / `codex_app_used` facts, events, reducer arms, client APIs, or dedupe cache state; `[apps.*]`, `Feature::Apps`, `include_apps_instructions`, app config/read schemas, app managed requirements, app prompt tests, and app file-MCP tests are gone; `Feature::Plugins`, `Feature::RemotePlugin`, `plugin://` structured mentions, plugin instruction tags, and stale plugin popup snapshots are gone too; `[plugins]` / `[marketplaces]`, `PluginConfig`, `MarketplaceConfig`, marketplace edit helpers, stale plugin/external-agent README route claims, and generated schema entries are removed from config; and the bundled `codex-skills` sample-skill crate plus embedded imagegen/openai-docs/plugin-creator/skill-creator/skill-installer payloads are deleted from the workspace. Live MCP tool approval is named `McpToolApproval`, not `AppToolApproval`, and remains under MCP server config; live structured user-input mentions are MCP-shaped only. `codex_message_processor.rs` is about 9,637 lines; root `message_processor.rs` is about 1,292 lines after the external-agent/config-product cuts; `epiphany_read_routes.rs` is about 579 lines after the full view and read-response extractions; `epiphany_mutation_routes.rs` is about 694 lines after route-load/notification helper cuts and product-surface starvation; `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader; and `codex_thread.rs` is about 1,425 lines after state-update law moved native. `epiphany-openai-adapter` defines CultCache `DatabaseEntry` request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` owns all direct Codex auth/model-transport imports for the native OpenAI path: it exposes `default_codex_home` and `auth_manager`, projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, maps deltas / completion into typed stream events and receipts, and no longer directly depends on `codex-app-server-protocol` just to name auth mode. `epiphany-openai-runtime` no longer depends directly on `codex-login`; it writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, can emit the assistant text artifact for legacy result consumers, is now the default executor for startup-only repo birth specialists, and now has `run-worker` to load a durable `EpiphanyRuntimeWorkerLaunchRequest`, call the typed OpenAI runtime through the spine, persist typed `EpiphanyRuntimeRoleWorkerResult` / `EpiphanyRuntimeReorientWorkerResult` documents, and complete the original heartbeat/specialist runtime job without Codex worker execution; `codex exec` survives only as explicit fallback in repo-birth startup. `epiphany-mvp-coordinator` now invokes `epiphany-openai-runtime run-worker` for launched runtime job ids and no longer creates a second coordinator-owned shadow runtime job/result as a compensator. `epiphany-core::plan_runtime_spine_heartbeat_launch` validates launch contracts/conflicts and projects durable job bindings/runtime links; `epiphany-core::open_runtime_spine_heartbeat_job` owns the initialize-runtime/ensure-session/create-job sequence and now persists `EpiphanyRuntimeWorkerLaunchRequest` CultCache records keyed by runtime job id, with indexed kind/contract fields plus MessagePack-encoded typed launch documents instead of discarding work intent after queue receipt creation; `roleResult` and `reorientResult` now prefer typed worker result documents and fall back only to generic job summaries for older receipts; `epiphany-core::EpiphanyStateUpdate`, `epiphany_state_update_validation_errors`, and `apply_epiphany_state_update` own state-update document shape and mutation law while `codex-core` calls them for compatibility. Vendored `CodexThread::epiphany_launch_job` is now mostly revision check, persistence validation, launch request handoff, and rollout/session writeback for the compatibility route. Vendored `codex-core` no longer has `epiphany_distillation`, `epiphany_promotion`, `epiphany_proposal`, or `epiphany_retrieval` husk modules; it re-exports native surfaces from `epiphany-core` directly, and now owns only the real Codex rollout adapter because it is a host seam. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the native caller now has a real storage route instead of only JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after richer live callers move. + summary: The Codex starvation pass has five typing receipts, forty-nine processor/foundation cuts, a real physical extraction in `epiphany-codex-bridge`, a root `epiphany-state-model` crate for durable Epiphany state, a source-grounded Codex auth spine inventory, the native `epiphany-openai-adapter` document boundary, a buildable Codex auth/model wrapper in `epiphany-openai-codex-spine`, the first native runtime caller in `epiphany-openai-runtime`, native heartbeat launch planning/job opening in `epiphany-core`, native state-update contract/validation/application in `epiphany-core`, native exact retrieval path matching in `epiphany-core`, and the route-facing mutation/view/coordinator projection services in `epiphany-codex-bridge`. `codex-protocol` no longer defines the durable `Epiphany*` state structs; it re-exports the native model for legacy rollout/API compatibility while `epiphany-core` imports the native model directly. `epiphany-core` now has no direct vendored Codex dependency: pressure derivation accepts native `EpiphanyTokenUsageSnapshot`, Codex rollout replay moved to `codex-core::epiphany_rollout`, and exact retrieval search uses Epiphany-owned workspace walking/path scoring instead of `codex-file-search`. The bridge owns Codex-facing Epiphany adapters for scene, jobs, context/planning/graph query, retrieval/indexing projection, result projection, runtime-spine result adaptation, launch/prompt doctrine, mutation document mechanics, mutation service update/promote/role-accept/reorient-accept/role-launch/reorient-launch/job-launch/job-interrupt application, live-state projection, pressure/pre-compaction rendering, freshness/reorientation projection, CRRC/coordinator/role-board projection, watcher invalidation adaptation, full `thread/epiphany/view` response construction, freshness/context/graph-query response construction, role/reorient result response construction, distill/propose patch-response construction, coordinator status/view construction, and acceptance/evidence signal helpers. Vendored app-server no longer retains root Epiphany modules; `codex_message_processor.rs` still contains route dispatch, JSON-RPC response shaping, watcher registration/snapshotting calls, and notification emission, but its root no longer imports the evacuated Epiphany policy/protocol registry or owns the watcher invalidation manager. Codex product surfaces for apps, skills, plugins, marketplace, and external agent config import are now inert in the app-server compatibility path: list routes return empty lists, mutation/detail/import routes report disabled, `externalAgentConfig/detect` returns no import candidates, helper/import modules and stale implementation-shape tests were deleted, runtime feature enablement no longer supports `apps` or `plugins`, config writes no longer emit plugin-toggle analytics, plugin/skill cache invalidation on config writes is gone, plugin startup warmups and app-list refreshes were removed, and app-server no longer directly depends on `codex-plugin`, `codex-chatgpt`, or `codex-core-plugins`. `Config::to_mcp_config` now keeps user-declared MCP servers but no longer accepts `PluginsManager`, folds plugin MCP servers into the runtime, enables Codex apps, enables skill MCP dependency install, or advertises plugin capability summaries; `TurnContext::apps_enabled()` now returns false so model turns cannot awaken Codex Apps through ChatGPT auth or feature flags; session startup and per-turn context no longer load skills, turns receive an empty `SkillLoadOutcome`, internal `Op::ListSkills` is gone from `codex-protocol` / `codex-core` / TUI command routing, the Codex skill watcher has been collapsed to a no-op marker with no file watcher, skill-root registration, cache-clear task, or `SkillsUpdateAvailable` session listener, model turns no longer run skill mention collection, skill env-var prompts, skill MCP dependency installation, skill instruction injection, or implicit skill invocation analytics from shell tools, and the turn prompt/router path no longer loads plugin/app capability summaries, injects available-skill/available-plugin/plugin instruction fragments, parses plugin/app mentions, records app/plugin invocation telemetry, or filters MCP tools through Codex app connectors. Skill mutation/list/notification is gone from the app-server edge too: `skills/list`, `skills/config/write`, `skills/changed`, `Op::ListSkills`, `EventMsg::ListSkillsResponse`, `EventMsg::SkillsUpdateAvailable`, the app-server handlers, TUI slash/startup/list/manage-skill surfaces, generated schema entries, core skill-list suite, and fake live-reload/developer-message tests were deleted. Skill mention plumbing is also gone from the live TUI/protocol edge: no `UserInput::Skill` variant, app-server schema branch, TUI skill popup, `skills_all` cache, composer `set_skill_mentions`, skill mention insertion, `skill://` / `SKILL.md` history decoding, or stale skill popup snapshot remains. The `codex-core-skills` crate, core `skills` re-export shim, no-op `skills_watcher`, session `SkillsManager` service, and per-turn `TurnSkillsContext` are deleted; core now keeps only a local legacy `` fragment recognizer for old history classification. The TUI app connector product path is also deleted: no `/apps` slash command, app-link view, connector AppEvent variants, app connector cache/popup/refresh loop, app mention insertion, `app://` mention codec support, stale app-popup tests, or TUI browser-opening shim remains. The `app/list` route and `app/list/updated` notification are gone from app-server protocol, generated schemas, app-server dispatch, test helpers, and docs; `AppInfo`/app-list metadata schema structs are deleted; the orphaned `codex-connectors` crate and `core::connectors` shim are deleted from the workspace; Codex app analytics no longer tracks `codex_app_mentioned` / `codex_app_used` facts, events, reducer arms, client APIs, or dedupe cache state; `[apps.*]`, `Feature::Apps`, `include_apps_instructions`, app config/read schemas, app managed requirements, app prompt tests, and app file-MCP tests are gone; `Feature::Plugins`, `Feature::RemotePlugin`, `plugin://` structured mentions, plugin instruction tags, and stale plugin popup snapshots are gone too; `[plugins]` / `[marketplaces]`, `PluginConfig`, `MarketplaceConfig`, marketplace edit helpers, stale plugin/external-agent README route claims, and generated schema entries are removed from config; and the bundled `codex-skills` sample-skill crate plus embedded imagegen/openai-docs/plugin-creator/skill-creator/skill-installer payloads are deleted from the workspace. Live MCP tool approval is named `McpToolApproval`, not `AppToolApproval`, and remains under MCP server config; live structured user-input mentions are MCP-shaped only. `codex_message_processor.rs` is about 9,637 lines; root `message_processor.rs` is about 1,292 lines after the external-agent/config-product cuts; `epiphany_read_routes.rs` is about 579 lines after the full view and read-response extractions; `epiphany_mutation_routes.rs` is about 694 lines after route-load/notification helper cuts and product-surface starvation; `epiphany_state_helpers.rs` is down to a 19-line rollout-state loader; and `codex_thread.rs` is about 1,425 lines after state-update law moved native. `epiphany-openai-adapter` defines CultCache `DatabaseEntry` request/event/receipt/auth-status documents with no Codex app-server or generic JSON dependency. `epiphany-openai-codex-spine` owns all direct Codex auth/model-transport imports for the native OpenAI path: it exposes `default_codex_home` and `auth_manager`, projects `AuthManager` / `CodexAuth` into typed `EpiphanyOpenAiAdapterStatus`, maps typed `EpiphanyOpenAiModelRequest` into Codex API `ResponsesApiRequest`, resolves auth/provider through keeper Codex crates, opens HTTP Responses streams through `codex-api`, maps deltas / completion into typed stream events and receipts, and no longer directly depends on `codex-app-server-protocol` just to name auth mode. `epiphany-openai-runtime` no longer depends directly on `codex-login`; it writes OpenAI status/request/stream-event/receipt records plus runtime sessions/jobs/results/events into `epiphany-core::runtime_spine_cache`, can emit the assistant text artifact for legacy result consumers, is now the default executor for startup-only repo birth specialists, and now has `run-worker` to load a durable `EpiphanyRuntimeWorkerLaunchRequest`, call the typed OpenAI runtime through the spine, persist typed `EpiphanyRuntimeRoleWorkerResult` / `EpiphanyRuntimeReorientWorkerResult` documents, and complete the original heartbeat/specialist runtime job without Codex worker execution; `codex exec` survives only as explicit fallback in repo-birth startup. `epiphany-mvp-coordinator` now invokes `epiphany-openai-runtime run-worker` for launched runtime job ids and no longer creates a second coordinator-owned shadow runtime job/result as a compensator. `epiphany-core::plan_runtime_spine_heartbeat_launch` validates launch contracts/conflicts and projects durable job bindings/runtime links; `epiphany-core::open_runtime_spine_heartbeat_job` owns the initialize-runtime/ensure-session/create-job sequence and now persists `EpiphanyRuntimeWorkerLaunchRequest` CultCache records keyed by runtime job id, with indexed kind/contract fields plus MessagePack-encoded typed launch documents instead of discarding work intent after queue receipt creation; `roleResult` and `reorientResult` now prefer typed worker result documents and fall back only to generic job summaries for older receipts; `epiphany-core::EpiphanyStateUpdate`, `epiphany_state_update_validation_errors`, and `apply_epiphany_state_update` own state-update document shape and mutation law while `codex-core` calls them for compatibility. Vendored `CodexThread::epiphany_launch_job` is now mostly revision check, persistence validation, launch request handoff, and rollout/session writeback for the compatibility route. Vendored `codex-core` no longer has `epiphany_distillation`, `epiphany_promotion`, `epiphany_proposal`, or `epiphany_retrieval` husk modules; it re-exports native surfaces from `epiphany-core` directly, and now owns only the real Codex rollout adapter because it is a host seam. `epiphany-runtime-spine` advertises OpenAI adapter status/model-request/stream-event/receipt CultNet schemas and mutation contracts, so the native caller now has a real storage route instead of only JSON-RPC. MCP itself is correctly JSON at its protocol edge; the target is typed Epiphany intent/result/receipt wrappers around MCP, not replacing MCP JSON. The current OpenAI spine still pulls `codex-api` and a large transport dependency stack, so its next cut is dependency starvation after richer live callers move. latest_cut: > Codex Apps no longer have privileged MCP/runtime treatment in the keeper path. MCP exposure is now uniform: small tool sets are direct, large or @@ -384,7 +384,11 @@ current_status: The TUI/protocol skill mention organ is deleted: no `UserInput::Skill`, app-server schema branch, TUI skill popup, `skills_all` cache, composer `set_skill_mentions`, skill mention insertion, `skill://` / - `SKILL.md` history decoding, or stale skill popup snapshot remains. Live + `SKILL.md` history decoding, or stale skill popup snapshot remains. The + `codex-core-skills` crate, core `skills` shim, no-op `skills_watcher`, + session `SkillsManager` service, and per-turn `TurnSkillsContext` are + deleted; core keeps only a local legacy `` fragment recognizer for + old history classification. Live structured user-input mentions are MCP-shaped only. MCP remains JSON-RPC at the edge; user-declared MCP servers are the only runtime MCP authority. next_action: Continue starving coordinator/runtime execution and operator surfaces off app-server JSON-RPC and out of `codex_message_processor.rs`; keep the vestigial Codex organ narrowed to subscription auth/model transport. diff --git a/vendor/codex/codex-rs/Cargo.lock b/vendor/codex/codex-rs/Cargo.lock index 61f768b9..21fe3c33 100644 --- a/vendor/codex/codex-rs/Cargo.lock +++ b/vendor/codex/codex-rs/Cargo.lock @@ -2345,7 +2345,6 @@ dependencies = [ "codex-async-utils", "codex-code-mode", "codex-config", - "codex-core-skills", "codex-exec-server", "codex-execpolicy", "codex-features", @@ -2446,34 +2445,6 @@ dependencies = [ "zstd 0.13.3", ] -[[package]] -name = "codex-core-skills" -version = "0.0.0" -dependencies = [ - "anyhow", - "codex-analytics", - "codex-app-server-protocol", - "codex-config", - "codex-exec-server", - "codex-login", - "codex-otel", - "codex-protocol", - "codex-utils-absolute-path", - "codex-utils-output-truncation", - "dirs", - "dunce", - "pretty_assertions", - "serde", - "serde_json", - "serde_yaml", - "shlex", - "tempfile", - "tokio", - "toml 0.9.11+spec-1.1.0", - "tracing", - "zip 2.4.2", -] - [[package]] name = "codex-debug-client" version = "0.0.0" @@ -3367,7 +3338,6 @@ dependencies = [ "codex-cli", "codex-cloud-requirements", "codex-config", - "codex-core-skills", "codex-exec-server", "codex-features", "codex-feedback", @@ -11680,19 +11650,6 @@ dependencies = [ "syn 2.0.114", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.13.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serial2" version = "0.2.33" @@ -13562,12 +13519,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.7.1" diff --git a/vendor/codex/codex-rs/Cargo.toml b/vendor/codex/codex-rs/Cargo.toml index 70d91d76..68930bc5 100644 --- a/vendor/codex/codex-rs/Cargo.toml +++ b/vendor/codex/codex-rs/Cargo.toml @@ -29,7 +29,6 @@ members = [ "shell-command", "shell-escalation", "core", - "core-skills", "hooks", "secrets", "exec", @@ -129,7 +128,6 @@ codex-cloud-tasks-mock-client = { path = "cloud-tasks-mock-client" } codex-code-mode = { path = "code-mode" } codex-config = { path = "config" } codex-core = { path = "core" } -codex-core-skills = { path = "core-skills" } codex-device-key = { path = "device-key" } codex-exec = { path = "exec" } codex-exec-server = { path = "exec-server" } diff --git a/vendor/codex/codex-rs/core-skills/BUILD.bazel b/vendor/codex/codex-rs/core-skills/BUILD.bazel deleted file mode 100644 index e80412a5..00000000 --- a/vendor/codex/codex-rs/core-skills/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "core-skills", - crate_name = "codex_core_skills", - compile_data = glob( - include = ["**"], - exclude = [ - "**/* *", - "BUILD.bazel", - "Cargo.toml", - ], - allow_empty = True, - ), -) diff --git a/vendor/codex/codex-rs/core-skills/Cargo.toml b/vendor/codex/codex-rs/core-skills/Cargo.toml deleted file mode 100644 index c89a9b49..00000000 --- a/vendor/codex/codex-rs/core-skills/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -edition.workspace = true -license.workspace = true -name = "codex-core-skills" -version.workspace = true - -[lib] -doctest = false -name = "codex_core_skills" -path = "src/lib.rs" - -[lints] -workspace = true - -[dependencies] -anyhow = { workspace = true } -codex-analytics = { workspace = true } -codex-app-server-protocol = { workspace = true } -codex-config = { workspace = true } -codex-exec-server = { workspace = true } -codex-login = { workspace = true } -codex-otel = { workspace = true } -codex-protocol = { workspace = true } -codex-utils-absolute-path = { workspace = true } -codex-utils-output-truncation = { workspace = true } -dirs = { workspace = true } -dunce = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -serde_yaml = { workspace = true } -shlex = { workspace = true } -tokio = { workspace = true, features = ["fs", "macros", "rt"] } -toml = { workspace = true } -tracing = { workspace = true } -zip = { workspace = true } - -[dev-dependencies] -pretty_assertions = { workspace = true } -tempfile = { workspace = true } diff --git a/vendor/codex/codex-rs/core-skills/src/config_rules.rs b/vendor/codex/codex-rs/core-skills/src/config_rules.rs deleted file mode 100644 index 92ad2ab1..00000000 --- a/vendor/codex/codex-rs/core-skills/src/config_rules.rs +++ /dev/null @@ -1,128 +0,0 @@ -use std::collections::HashSet; - -use codex_app_server_protocol::ConfigLayerSource; -use codex_config::ConfigLayerStack; -use codex_config::ConfigLayerStackOrdering; -use codex_config::SkillConfig; -use codex_config::SkillsConfig; -use codex_utils_absolute_path::AbsolutePathBuf; -use tracing::warn; - -use crate::SkillMetadata; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum SkillConfigRuleSelector { - Name(String), - Path(AbsolutePathBuf), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SkillConfigRule { - pub selector: SkillConfigRuleSelector, - pub enabled: bool, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] -pub struct SkillConfigRules { - pub entries: Vec, -} - -pub fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> SkillConfigRules { - let mut entries = Vec::new(); - for layer in config_layer_stack.get_layers( - ConfigLayerStackOrdering::LowestPrecedenceFirst, - /*include_disabled*/ true, - ) { - if !matches!( - layer.name, - ConfigLayerSource::User { .. } | ConfigLayerSource::SessionFlags - ) { - continue; - } - - let Some(skills_value) = layer.config.get("skills") else { - continue; - }; - let skills: SkillsConfig = match skills_value.clone().try_into() { - Ok(skills) => skills, - Err(err) => { - warn!("invalid skills config: {err}"); - continue; - } - }; - - for entry in skills.config { - let Some(selector) = skill_config_rule_selector(&entry) else { - continue; - }; - // Preserve layer order so a later name selector can override an earlier path selector - // for the same loaded skill. - entries.retain(|entry: &SkillConfigRule| entry.selector != selector); - entries.push(SkillConfigRule { - selector, - enabled: entry.enabled, - }); - } - } - - SkillConfigRules { entries } -} - -pub fn resolve_disabled_skill_paths( - skills: &[SkillMetadata], - rules: &SkillConfigRules, -) -> HashSet { - let mut disabled_paths = HashSet::new(); - - for entry in &rules.entries { - match &entry.selector { - SkillConfigRuleSelector::Path(path) => { - if entry.enabled { - disabled_paths.remove(path); - } else { - disabled_paths.insert(path.clone()); - } - } - SkillConfigRuleSelector::Name(name) => { - for path in skills - .iter() - .filter(|skill| skill.name == *name) - .map(|skill| skill.path_to_skills_md.clone()) - { - if entry.enabled { - disabled_paths.remove(&path); - } else { - disabled_paths.insert(path); - } - } - } - } - } - - disabled_paths -} - -fn skill_config_rule_selector(entry: &SkillConfig) -> Option { - match (entry.path.as_ref(), entry.name.as_deref()) { - (Some(path), None) => Some(SkillConfigRuleSelector::Path( - path.canonicalize().unwrap_or_else(|_| path.clone()), - )), - (None, Some(name)) => { - let name = name.trim(); - if name.is_empty() { - warn!("ignoring empty skills.config name override"); - None - } else { - Some(SkillConfigRuleSelector::Name(name.to_string())) - } - } - (Some(_), Some(_)) => { - warn!("ignoring skills.config entry with both path and name selectors"); - None - } - (None, None) => { - warn!("ignoring skills.config entry without a path or name selector"); - None - } - } -} diff --git a/vendor/codex/codex-rs/core-skills/src/env_var_dependencies.rs b/vendor/codex/codex-rs/core-skills/src/env_var_dependencies.rs deleted file mode 100644 index 4d8e5716..00000000 --- a/vendor/codex/codex-rs/core-skills/src/env_var_dependencies.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::SkillMetadata; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillDependencyInfo { - pub skill_name: String, - pub name: String, - pub description: Option, -} - -pub fn collect_env_var_dependencies( - mentioned_skills: &[SkillMetadata], -) -> Vec { - let mut dependencies = Vec::new(); - for skill in mentioned_skills { - let Some(skill_dependencies) = &skill.dependencies else { - continue; - }; - for tool in &skill_dependencies.tools { - if tool.r#type != "env_var" || tool.value.is_empty() { - continue; - } - dependencies.push(SkillDependencyInfo { - skill_name: skill.name.clone(), - name: tool.value.clone(), - description: tool.description.clone(), - }); - } - } - dependencies -} diff --git a/vendor/codex/codex-rs/core-skills/src/injection.rs b/vendor/codex/codex-rs/core-skills/src/injection.rs deleted file mode 100644 index 26bd6b07..00000000 --- a/vendor/codex/codex-rs/core-skills/src/injection.rs +++ /dev/null @@ -1,455 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::sync::Arc; - -use crate::SkillLoadOutcome; -use crate::SkillMetadata; -use crate::build_skill_name_counts; -use codex_analytics::AnalyticsEventsClient; -use codex_analytics::InvocationType; -use codex_analytics::SkillInvocation; -use codex_analytics::TrackEventsContext; -use codex_exec_server::LOCAL_FS; -use codex_otel::SessionTelemetry; -use codex_utils_absolute_path::AbsolutePathBuf; - -const TOOL_MENTION_SIGIL: char = '$'; - -#[derive(Debug, Default)] -pub struct SkillInjections { - pub items: Vec, - pub warnings: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillInjection { - pub name: String, - pub path: String, - pub contents: String, -} - -pub async fn build_skill_injections( - mentioned_skills: &[SkillMetadata], - loaded_skills: Option<&SkillLoadOutcome>, - otel: Option<&SessionTelemetry>, - analytics_client: &AnalyticsEventsClient, - tracking: TrackEventsContext, -) -> SkillInjections { - if mentioned_skills.is_empty() { - return SkillInjections::default(); - } - - let mut result = SkillInjections { - items: Vec::with_capacity(mentioned_skills.len()), - warnings: Vec::new(), - }; - let mut invocations = Vec::new(); - - for skill in mentioned_skills { - let fs = loaded_skills - .and_then(|outcome| outcome.file_system_for_skill(skill)) - .unwrap_or_else(|| Arc::clone(&LOCAL_FS)); - match fs - .read_file_text(&skill.path_to_skills_md, /*sandbox*/ None) - .await - { - Ok(contents) => { - emit_skill_injected_metric(otel, skill, "ok"); - invocations.push(SkillInvocation { - skill_name: skill.name.clone(), - skill_scope: skill.scope, - skill_path: skill.path_to_skills_md.to_path_buf(), - invocation_type: InvocationType::Explicit, - }); - result.items.push(SkillInjection { - name: skill.name.clone(), - path: skill.path_to_skills_md.to_string_lossy().into_owned(), - contents, - }); - } - Err(err) => { - emit_skill_injected_metric(otel, skill, "error"); - let message = format!( - "Failed to load skill {name} at {path}: {err:#}", - name = skill.name, - path = skill.path_to_skills_md.display() - ); - result.warnings.push(message); - } - } - } - - analytics_client.track_skill_invocations(tracking, invocations); - - result -} - -fn emit_skill_injected_metric( - otel: Option<&SessionTelemetry>, - skill: &SkillMetadata, - status: &str, -) { - let Some(otel) = otel else { - return; - }; - - otel.counter( - "codex.skill.injected", - /*inc*/ 1, - &[("status", status), ("skill", skill.name.as_str())], - ); -} - -/// Collect explicitly mentioned skills from text mentions. -/// -/// Text inputs are scanned to extract `$skill-name` tokens, and we iterate `skills` in -/// their existing order to preserve prior ordering semantics. -/// Explicit links are resolved by path and plain names are only used when the match -/// is unambiguous. -/// -/// Complexity: `O(T + N * S)` time, `O(S + M)` space, where: -/// `S` = number of skills, `T` = total text length, `N` = number of text inputs, -/// and `M` = max mentions parsed from a single text input. -pub fn collect_explicit_skill_mentions( - inputs: &[codex_protocol::user_input::UserInput], - skills: &[SkillMetadata], - disabled_paths: &HashSet, - connector_slug_counts: &HashMap, -) -> Vec { - let skill_name_counts = build_skill_name_counts(skills, disabled_paths).0; - - let selection_context = SkillSelectionContext { - skills, - disabled_paths, - skill_name_counts: &skill_name_counts, - connector_slug_counts, - }; - let mut selected: Vec = Vec::new(); - let mut seen_names: HashSet = HashSet::new(); - let mut seen_paths: HashSet = HashSet::new(); - - for input in inputs { - if let codex_protocol::user_input::UserInput::Text { text, .. } = input { - let mentioned_names = extract_tool_mentions(text); - select_skills_from_mentions( - &selection_context, - &mentioned_names, - &mut seen_names, - &mut seen_paths, - &mut selected, - ); - } - } - - selected -} - -struct SkillSelectionContext<'a> { - skills: &'a [SkillMetadata], - disabled_paths: &'a HashSet, - skill_name_counts: &'a HashMap, - connector_slug_counts: &'a HashMap, -} - -pub struct ToolMentions<'a> { - names: HashSet<&'a str>, - paths: HashSet<&'a str>, - plain_names: HashSet<&'a str>, -} - -impl<'a> ToolMentions<'a> { - fn is_empty(&self) -> bool { - self.names.is_empty() && self.paths.is_empty() - } - - pub fn plain_names(&self) -> impl Iterator + '_ { - self.plain_names.iter().copied() - } - - pub fn paths(&self) -> impl Iterator + '_ { - self.paths.iter().copied() - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ToolMentionKind { - Mcp, - Skill, - Other, -} - -const MCP_PATH_PREFIX: &str = "mcp://"; -const SKILL_PATH_PREFIX: &str = "skill://"; -const SKILL_FILENAME: &str = "SKILL.md"; - -pub fn tool_kind_for_path(path: &str) -> ToolMentionKind { - if path.starts_with(MCP_PATH_PREFIX) { - ToolMentionKind::Mcp - } else if path.starts_with(SKILL_PATH_PREFIX) || is_skill_filename(path) { - ToolMentionKind::Skill - } else { - ToolMentionKind::Other - } -} - -fn is_skill_filename(path: &str) -> bool { - let file_name = path.rsplit(['/', '\\']).next().unwrap_or(path); - file_name.eq_ignore_ascii_case(SKILL_FILENAME) -} - -pub(crate) fn normalize_skill_path(path: &str) -> &str { - path.strip_prefix(SKILL_PATH_PREFIX).unwrap_or(path) -} - -/// Extract `$tool-name` mentions from a single text input. -/// -/// Supports explicit resource links in the form `[$tool-name](resource path)`. When a -/// resource path is present, it is captured for exact path matching while also tracking -/// the name for fallback matching. -pub fn extract_tool_mentions(text: &str) -> ToolMentions<'_> { - extract_tool_mentions_with_sigil(text, TOOL_MENTION_SIGIL) -} - -pub fn extract_tool_mentions_with_sigil(text: &str, sigil: char) -> ToolMentions<'_> { - let text_bytes = text.as_bytes(); - let mut mentioned_names: HashSet<&str> = HashSet::new(); - let mut mentioned_paths: HashSet<&str> = HashSet::new(); - let mut plain_names: HashSet<&str> = HashSet::new(); - - let mut index = 0; - while index < text_bytes.len() { - let byte = text_bytes[index]; - if byte == b'[' - && let Some((name, path, end_index)) = - parse_linked_tool_mention(text, text_bytes, index, sigil) - { - if !is_common_env_var(name) { - if !matches!(tool_kind_for_path(path), ToolMentionKind::Mcp) { - mentioned_names.insert(name); - } - mentioned_paths.insert(path); - } - index = end_index; - continue; - } - - if byte != sigil as u8 { - index += 1; - continue; - } - - let name_start = index + 1; - let Some(first_name_byte) = text_bytes.get(name_start) else { - index += 1; - continue; - }; - if !is_mention_name_char(*first_name_byte) { - index += 1; - continue; - } - - let mut name_end = name_start + 1; - while let Some(next_byte) = text_bytes.get(name_end) - && is_mention_name_char(*next_byte) - { - name_end += 1; - } - - let name = &text[name_start..name_end]; - if !is_common_env_var(name) { - mentioned_names.insert(name); - plain_names.insert(name); - } - index = name_end; - } - - ToolMentions { - names: mentioned_names, - paths: mentioned_paths, - plain_names, - } -} - -/// Select mentioned skills while preserving the order of `skills`. -fn select_skills_from_mentions( - selection_context: &SkillSelectionContext<'_>, - mentions: &ToolMentions<'_>, - seen_names: &mut HashSet, - seen_paths: &mut HashSet, - selected: &mut Vec, -) { - if mentions.is_empty() { - return; - } - - let mention_skill_paths: HashSet<&str> = mentions - .paths() - .filter(|path| !matches!(tool_kind_for_path(path), ToolMentionKind::Mcp)) - .map(normalize_skill_path) - .collect(); - - for skill in selection_context.skills { - if selection_context - .disabled_paths - .contains(&skill.path_to_skills_md) - || seen_paths.contains(&skill.path_to_skills_md) - { - continue; - } - - let path_str = skill.path_to_skills_md.to_string_lossy(); - if mention_skill_paths.contains(path_str.as_ref()) { - seen_paths.insert(skill.path_to_skills_md.clone()); - seen_names.insert(skill.name.clone()); - selected.push(skill.clone()); - } - } - - for skill in selection_context.skills { - if selection_context - .disabled_paths - .contains(&skill.path_to_skills_md) - || seen_paths.contains(&skill.path_to_skills_md) - { - continue; - } - - if !mentions.plain_names.contains(skill.name.as_str()) { - continue; - } - - let skill_count = selection_context - .skill_name_counts - .get(skill.name.as_str()) - .copied() - .unwrap_or(0); - let connector_count = selection_context - .connector_slug_counts - .get(&skill.name.to_ascii_lowercase()) - .copied() - .unwrap_or(0); - if skill_count != 1 || connector_count != 0 { - continue; - } - - if seen_names.insert(skill.name.clone()) { - seen_paths.insert(skill.path_to_skills_md.clone()); - selected.push(skill.clone()); - } - } -} - -fn parse_linked_tool_mention<'a>( - text: &'a str, - text_bytes: &[u8], - start: usize, - sigil: char, -) -> Option<(&'a str, &'a str, usize)> { - let sigil_index = start + 1; - if text_bytes.get(sigil_index) != Some(&(sigil as u8)) { - return None; - } - - let name_start = sigil_index + 1; - let first_name_byte = text_bytes.get(name_start)?; - if !is_mention_name_char(*first_name_byte) { - return None; - } - - let mut name_end = name_start + 1; - while let Some(next_byte) = text_bytes.get(name_end) - && is_mention_name_char(*next_byte) - { - name_end += 1; - } - - if text_bytes.get(name_end) != Some(&b']') { - return None; - } - - let mut path_start = name_end + 1; - while let Some(next_byte) = text_bytes.get(path_start) - && next_byte.is_ascii_whitespace() - { - path_start += 1; - } - if text_bytes.get(path_start) != Some(&b'(') { - return None; - } - - let mut path_end = path_start + 1; - while let Some(next_byte) = text_bytes.get(path_end) - && *next_byte != b')' - { - path_end += 1; - } - if text_bytes.get(path_end) != Some(&b')') { - return None; - } - - let path = text[path_start + 1..path_end].trim(); - if path.is_empty() { - return None; - } - - let name = &text[name_start..name_end]; - Some((name, path, path_end + 1)) -} - -fn is_common_env_var(name: &str) -> bool { - let upper = name.to_ascii_uppercase(); - matches!( - upper.as_str(), - "PATH" - | "HOME" - | "USER" - | "SHELL" - | "PWD" - | "TMPDIR" - | "TEMP" - | "TMP" - | "LANG" - | "TERM" - | "XDG_CONFIG_HOME" - ) -} - -#[cfg(test)] -fn text_mentions_skill(text: &str, skill_name: &str) -> bool { - if skill_name.is_empty() { - return false; - } - - let text_bytes = text.as_bytes(); - let skill_bytes = skill_name.as_bytes(); - - for (index, byte) in text_bytes.iter().copied().enumerate() { - if byte != b'$' { - continue; - } - - let name_start = index + 1; - let Some(rest) = text_bytes.get(name_start..) else { - continue; - }; - if !rest.starts_with(skill_bytes) { - continue; - } - - let after_index = name_start + skill_bytes.len(); - let after = text_bytes.get(after_index).copied(); - if after.is_none_or(|b| !is_mention_name_char(b)) { - return true; - } - } - - false -} - -fn is_mention_name_char(byte: u8) -> bool { - matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b':') -} - -#[cfg(test)] -#[path = "injection_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-skills/src/injection_tests.rs b/vendor/codex/codex-rs/core-skills/src/injection_tests.rs deleted file mode 100644 index e9cd1472..00000000 --- a/vendor/codex/codex-rs/core-skills/src/injection_tests.rs +++ /dev/null @@ -1,293 +0,0 @@ -use super::*; -use codex_protocol::user_input::UserInput; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_absolute_path::test_support::PathBufExt; -use codex_utils_absolute_path::test_support::test_path_buf; -use pretty_assertions::assert_eq; -use std::collections::HashMap; -use std::collections::HashSet; - -fn make_skill(name: &str, path: &str) -> SkillMetadata { - SkillMetadata { - name: name.to_string(), - description: format!("{name} skill"), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf(path).abs(), - scope: codex_protocol::protocol::SkillScope::User, - } -} - -fn set<'a>(items: &'a [&'a str]) -> HashSet<&'a str> { - items.iter().copied().collect() -} - -fn assert_mentions(text: &str, expected_names: &[&str], expected_paths: &[&str]) { - let mentions = extract_tool_mentions(text); - assert_eq!(mentions.names, set(expected_names)); - assert_eq!(mentions.paths, set(expected_paths)); -} - -fn linked_skill_mention(name: &str, unix_path: &str) -> String { - format!("[${name}]({})", test_path_buf(unix_path).display()) -} - -fn collect_mentions( - inputs: &[UserInput], - skills: &[SkillMetadata], - disabled_paths: &HashSet, - connector_slug_counts: &HashMap, -) -> Vec { - collect_explicit_skill_mentions(inputs, skills, disabled_paths, connector_slug_counts) -} - -#[test] -fn text_mentions_skill_requires_exact_boundary() { - assert_eq!( - true, - text_mentions_skill("use $notion-research-doc please", "notion-research-doc") - ); - assert_eq!( - true, - text_mentions_skill("($notion-research-doc)", "notion-research-doc") - ); - assert_eq!( - true, - text_mentions_skill("$notion-research-doc.", "notion-research-doc") - ); - assert_eq!( - false, - text_mentions_skill("$notion-research-docs", "notion-research-doc") - ); - assert_eq!( - false, - text_mentions_skill("$notion-research-doc_extra", "notion-research-doc") - ); -} - -#[test] -fn text_mentions_skill_handles_end_boundary_and_near_misses() { - assert_eq!(true, text_mentions_skill("$alpha-skill", "alpha-skill")); - assert_eq!(false, text_mentions_skill("$alpha-skillx", "alpha-skill")); - assert_eq!( - true, - text_mentions_skill("$alpha-skillx and later $alpha-skill ", "alpha-skill") - ); -} - -#[test] -fn text_mentions_skill_handles_many_dollars_without_looping() { - let prefix = "$".repeat(256); - let text = format!("{prefix} not-a-mention"); - assert_eq!(false, text_mentions_skill(&text, "alpha-skill")); -} - -#[test] -fn extract_tool_mentions_handles_plain_and_linked_mentions() { - assert_mentions( - "use $alpha and [$beta](/tmp/beta)", - &["alpha", "beta"], - &["/tmp/beta"], - ); -} - -#[test] -fn extract_tool_mentions_skips_common_env_vars() { - assert_mentions("use $PATH and $alpha", &["alpha"], &[]); - assert_mentions("use [$HOME](/tmp/skill)", &[], &[]); - assert_mentions("use $XDG_CONFIG_HOME and $beta", &["beta"], &[]); -} - -#[test] -fn extract_tool_mentions_requires_link_syntax() { - assert_mentions("[beta](/tmp/beta)", &[], &[]); - assert_mentions("[$beta] /tmp/beta", &["beta"], &[]); - assert_mentions("[$beta]()", &["beta"], &[]); -} - -#[test] -fn extract_tool_mentions_trims_linked_paths_and_allows_spacing() { - assert_mentions("use [$beta] ( /tmp/beta )", &["beta"], &["/tmp/beta"]); -} - -#[test] -fn extract_tool_mentions_stops_at_non_name_chars() { - assert_mentions( - "use $alpha.skill and $beta_extra", - &["alpha", "beta_extra"], - &[], - ); -} - -#[test] -fn extract_tool_mentions_keeps_tool_style_namespaces() { - assert_mentions( - "use $slack:search and $alpha", - &["alpha", "slack:search"], - &[], - ); -} - -#[test] -fn collect_explicit_skill_mentions_text_respects_skill_order() { - let alpha = make_skill("alpha-skill", "/tmp/alpha"); - let beta = make_skill("beta-skill", "/tmp/beta"); - let skills = vec![beta.clone(), alpha.clone()]; - let inputs = vec![UserInput::Text { - text: "first $alpha-skill then $beta-skill".to_string(), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - // Text scanning should not change the previous selection ordering semantics. - assert_eq!(selected, vec![beta, alpha]); -} - -#[test] -fn collect_explicit_skill_mentions_dedupes_by_path() { - let alpha = make_skill("alpha-skill", "/tmp/alpha"); - let skills = vec![alpha.clone()]; - let mention = linked_skill_mention("alpha-skill", "/tmp/alpha"); - let inputs = vec![UserInput::Text { - text: format!("use {mention} and {mention}"), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, vec![alpha]); -} - -#[test] -fn collect_explicit_skill_mentions_skips_ambiguous_name() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let beta = make_skill("demo-skill", "/tmp/beta"); - let skills = vec![alpha, beta]; - let inputs = vec![UserInput::Text { - text: "use $demo-skill and again $demo-skill".to_string(), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, Vec::new()); -} - -#[test] -fn collect_explicit_skill_mentions_prefers_linked_path_over_name() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let beta = make_skill("demo-skill", "/tmp/beta"); - let skills = vec![alpha, beta.clone()]; - let inputs = vec![UserInput::Text { - text: format!( - "use $demo-skill and {}", - linked_skill_mention("demo-skill", "/tmp/beta") - ), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, vec![beta]); -} - -#[test] -fn collect_explicit_skill_mentions_skips_plain_name_when_connector_matches() { - let alpha = make_skill("alpha-skill", "/tmp/alpha"); - let skills = vec![alpha]; - let inputs = vec![UserInput::Text { - text: "use $alpha-skill".to_string(), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::from([("alpha-skill".to_string(), 1)]); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, Vec::new()); -} - -#[test] -fn collect_explicit_skill_mentions_allows_explicit_path_with_connector_conflict() { - let alpha = make_skill("alpha-skill", "/tmp/alpha"); - let skills = vec![alpha.clone()]; - let inputs = vec![UserInput::Text { - text: format!("use {}", linked_skill_mention("alpha-skill", "/tmp/alpha")), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::from([("alpha-skill".to_string(), 1)]); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, vec![alpha]); -} - -#[test] -fn collect_explicit_skill_mentions_skips_when_linked_path_disabled() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let beta = make_skill("demo-skill", "/tmp/beta"); - let skills = vec![alpha, beta]; - let inputs = vec![UserInput::Text { - text: format!("use {}", linked_skill_mention("demo-skill", "/tmp/alpha")), - text_elements: Vec::new(), - }]; - let disabled = HashSet::from([test_path_buf("/tmp/alpha").abs()]); - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &disabled, &connector_counts); - - assert_eq!(selected, Vec::new()); -} - -#[test] -fn collect_explicit_skill_mentions_prefers_resource_path() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let beta = make_skill("demo-skill", "/tmp/beta"); - let skills = vec![alpha, beta.clone()]; - let inputs = vec![UserInput::Text { - text: format!("use {}", linked_skill_mention("demo-skill", "/tmp/beta")), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, vec![beta]); -} - -#[test] -fn collect_explicit_skill_mentions_skips_missing_path_with_no_fallback() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let beta = make_skill("demo-skill", "/tmp/beta"); - let skills = vec![alpha, beta]; - let inputs = vec![UserInput::Text { - text: format!("use {}", linked_skill_mention("demo-skill", "/tmp/missing")), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, Vec::new()); -} - -#[test] -fn collect_explicit_skill_mentions_skips_missing_path_without_fallback() { - let alpha = make_skill("demo-skill", "/tmp/alpha"); - let skills = vec![alpha]; - let inputs = vec![UserInput::Text { - text: format!("use {}", linked_skill_mention("demo-skill", "/tmp/missing")), - text_elements: Vec::new(), - }]; - let connector_counts = HashMap::new(); - - let selected = collect_mentions(&inputs, &skills, &HashSet::new(), &connector_counts); - - assert_eq!(selected, Vec::new()); -} diff --git a/vendor/codex/codex-rs/core-skills/src/invocation_utils.rs b/vendor/codex/codex-rs/core-skills/src/invocation_utils.rs deleted file mode 100644 index 4c9d0a41..00000000 --- a/vendor/codex/codex-rs/core-skills/src/invocation_utils.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::collections::HashMap; -use std::path::Path; - -use crate::SkillLoadOutcome; -use crate::SkillMetadata; -use codex_utils_absolute_path::AbsolutePathBuf; - -pub(crate) fn build_implicit_skill_path_indexes( - skills: Vec, -) -> ( - HashMap, - HashMap, -) { - let mut by_scripts_dir = HashMap::new(); - let mut by_skill_doc_path = HashMap::new(); - for skill in skills { - let skill_doc_path = canonicalize_if_exists(&skill.path_to_skills_md); - by_skill_doc_path.insert(skill_doc_path, skill.clone()); - - if let Some(skill_dir) = skill.path_to_skills_md.parent() { - let scripts_dir = canonicalize_if_exists(&skill_dir.join("scripts")); - by_scripts_dir.insert(scripts_dir, skill); - } - } - - (by_scripts_dir, by_skill_doc_path) -} - -pub fn detect_implicit_skill_invocation_for_command( - outcome: &SkillLoadOutcome, - command: &str, - workdir: &AbsolutePathBuf, -) -> Option { - let workdir = canonicalize_if_exists(workdir); - let tokens = tokenize_command(command); - - if let Some(candidate) = detect_skill_script_run(outcome, tokens.as_slice(), &workdir) { - return Some(candidate); - } - - detect_skill_doc_read(outcome, tokens.as_slice(), &workdir) -} - -fn tokenize_command(command: &str) -> Vec { - shlex::split(command) - .unwrap_or_else(|| command.split_whitespace().map(str::to_string).collect()) -} - -fn script_run_token(tokens: &[String]) -> Option<&str> { - const RUNNERS: [&str; 10] = [ - "python", "python3", "bash", "zsh", "sh", "node", "deno", "ruby", "perl", "pwsh", - ]; - const SCRIPT_EXTENSIONS: [&str; 7] = [".py", ".sh", ".js", ".ts", ".rb", ".pl", ".ps1"]; - - let runner_token = tokens.first()?; - let runner = command_basename(runner_token).to_ascii_lowercase(); - let runner = runner.strip_suffix(".exe").unwrap_or(&runner); - if !RUNNERS.contains(&runner) { - return None; - } - - let mut script_token = None; - for token in tokens.iter().skip(1) { - if token == "--" || token.starts_with('-') { - continue; - } - script_token = Some(token.as_str()); - break; - } - let script_token = script_token?; - if SCRIPT_EXTENSIONS - .iter() - .any(|extension| script_token.to_ascii_lowercase().ends_with(extension)) - { - return Some(script_token); - } - - None -} - -fn detect_skill_script_run( - outcome: &SkillLoadOutcome, - tokens: &[String], - workdir: &AbsolutePathBuf, -) -> Option { - let script_token = script_run_token(tokens)?; - let script_path = Path::new(script_token); - let script_path = canonicalize_if_exists(&workdir.join(script_path)); - - for path in script_path.ancestors() { - if let Some(candidate) = outcome.implicit_skills_by_scripts_dir.get(&path) { - return Some(candidate.clone()); - } - } - - None -} - -fn detect_skill_doc_read( - outcome: &SkillLoadOutcome, - tokens: &[String], - workdir: &AbsolutePathBuf, -) -> Option { - if !command_reads_file(tokens) { - return None; - } - - for token in tokens.iter().skip(1) { - if token.starts_with('-') { - continue; - } - let path = Path::new(token); - let candidate_path = canonicalize_if_exists(&workdir.join(path)); - if let Some(candidate) = outcome.implicit_skills_by_doc_path.get(&candidate_path) { - return Some(candidate.clone()); - } - } - - None -} - -fn command_reads_file(tokens: &[String]) -> bool { - const READERS: [&str; 8] = ["cat", "sed", "head", "tail", "less", "more", "bat", "awk"]; - let Some(program) = tokens.first() else { - return false; - }; - let program = command_basename(program).to_ascii_lowercase(); - READERS.contains(&program.as_str()) -} - -fn command_basename(command: &str) -> String { - Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(command) - .to_string() -} - -fn canonicalize_if_exists(path: &AbsolutePathBuf) -> AbsolutePathBuf { - path.canonicalize().unwrap_or_else(|_| path.clone()) -} - -#[cfg(test)] -#[path = "invocation_utils_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-skills/src/invocation_utils_tests.rs b/vendor/codex/codex-rs/core-skills/src/invocation_utils_tests.rs deleted file mode 100644 index ab3a3e8d..00000000 --- a/vendor/codex/codex-rs/core-skills/src/invocation_utils_tests.rs +++ /dev/null @@ -1,122 +0,0 @@ -use super::SkillLoadOutcome; -use super::SkillMetadata; -use super::canonicalize_if_exists; -use super::detect_skill_doc_read; -use super::detect_skill_script_run; -use super::script_run_token; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_absolute_path::test_support::PathBufExt; -use codex_utils_absolute_path::test_support::test_path_buf; -use pretty_assertions::assert_eq; -use std::collections::HashMap; -use std::sync::Arc; - -fn test_skill_metadata(skill_doc_path: AbsolutePathBuf) -> SkillMetadata { - SkillMetadata { - name: "test-skill".to_string(), - description: "test".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: skill_doc_path, - scope: codex_protocol::protocol::SkillScope::User, - } -} - -fn test_path_display(unix_path: &str) -> String { - test_path_buf(unix_path).display().to_string() -} - -#[test] -fn script_run_detection_matches_runner_plus_extension() { - let tokens = vec![ - "python3".to_string(), - "-u".to_string(), - "scripts/fetch_comments.py".to_string(), - ]; - - assert_eq!(script_run_token(&tokens).is_some(), true); -} - -#[test] -fn script_run_detection_excludes_python_c() { - let tokens = vec![ - "python3".to_string(), - "-c".to_string(), - "print(1)".to_string(), - ]; - - assert_eq!(script_run_token(&tokens).is_some(), false); -} - -#[test] -fn skill_doc_read_detection_matches_absolute_path() { - let skill_doc_path = test_path_buf("/tmp/skill-test/SKILL.md").abs(); - let normalized_skill_doc_path = canonicalize_if_exists(&skill_doc_path); - let skill = test_skill_metadata(skill_doc_path); - let outcome = SkillLoadOutcome { - implicit_skills_by_scripts_dir: Arc::new(HashMap::new()), - implicit_skills_by_doc_path: Arc::new(HashMap::from([(normalized_skill_doc_path, skill)])), - ..Default::default() - }; - - let tokens = vec![ - "cat".to_string(), - test_path_display("/tmp/skill-test/SKILL.md"), - "|".to_string(), - "head".to_string(), - ]; - let found = detect_skill_doc_read(&outcome, &tokens, &test_path_buf("/tmp").abs()); - - assert_eq!( - found.map(|value| value.name), - Some("test-skill".to_string()) - ); -} - -#[test] -fn skill_script_run_detection_matches_relative_path_from_skill_root() { - let skill_doc_path = test_path_buf("/tmp/skill-test/SKILL.md").abs(); - let scripts_dir = canonicalize_if_exists(&test_path_buf("/tmp/skill-test/scripts").abs()); - let skill = test_skill_metadata(skill_doc_path); - let outcome = SkillLoadOutcome { - implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(scripts_dir, skill)])), - implicit_skills_by_doc_path: Arc::new(HashMap::new()), - ..Default::default() - }; - let tokens = vec![ - "python3".to_string(), - "scripts/fetch_comments.py".to_string(), - ]; - - let found = detect_skill_script_run(&outcome, &tokens, &test_path_buf("/tmp/skill-test").abs()); - - assert_eq!( - found.map(|value| value.name), - Some("test-skill".to_string()) - ); -} - -#[test] -fn skill_script_run_detection_matches_absolute_path_from_any_workdir() { - let skill_doc_path = test_path_buf("/tmp/skill-test/SKILL.md").abs(); - let scripts_dir = canonicalize_if_exists(&test_path_buf("/tmp/skill-test/scripts").abs()); - let skill = test_skill_metadata(skill_doc_path); - let outcome = SkillLoadOutcome { - implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(scripts_dir, skill)])), - implicit_skills_by_doc_path: Arc::new(HashMap::new()), - ..Default::default() - }; - let tokens = vec![ - "python3".to_string(), - test_path_display("/tmp/skill-test/scripts/fetch_comments.py"), - ]; - - let found = detect_skill_script_run(&outcome, &tokens, &test_path_buf("/tmp/other").abs()); - - assert_eq!( - found.map(|value| value.name), - Some("test-skill".to_string()) - ); -} diff --git a/vendor/codex/codex-rs/core-skills/src/lib.rs b/vendor/codex/codex-rs/core-skills/src/lib.rs deleted file mode 100644 index 06ced0d5..00000000 --- a/vendor/codex/codex-rs/core-skills/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -pub mod config_rules; -mod env_var_dependencies; -pub mod injection; -pub(crate) mod invocation_utils; -pub mod loader; -pub mod manager; -mod mention_counts; -pub mod model; -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::SkillsLoadInput; -pub use manager::SkillsManager; -pub use mention_counts::build_skill_name_counts; -pub use model::SkillError; -pub use model::SkillLoadOutcome; -pub use model::SkillMetadata; -pub use model::SkillPolicy; -pub use model::filter_skill_load_outcome_for_product; -pub use render::AvailableSkills; -pub use render::SkillMetadataBudget; -pub use render::SkillRenderReport; -pub use render::build_available_skills; -pub use render::default_skill_metadata_budget; diff --git a/vendor/codex/codex-rs/core-skills/src/loader.rs b/vendor/codex/codex-rs/core-skills/src/loader.rs deleted file mode 100644 index 8679ea8a..00000000 --- a/vendor/codex/codex-rs/core-skills/src/loader.rs +++ /dev/null @@ -1,946 +0,0 @@ -use crate::model::SkillDependencies; -use crate::model::SkillError; -use crate::model::SkillFileSystemsByPath; -use crate::model::SkillInterface; -use crate::model::SkillLoadOutcome; -use crate::model::SkillMetadata; -use crate::model::SkillPolicy; -use crate::model::SkillToolDependency; -use crate::system::system_cache_root_dir; -use codex_app_server_protocol::ConfigLayerSource; -use codex_config::ConfigLayerStack; -use codex_config::ConfigLayerStackOrdering; -use codex_config::default_project_root_markers; -use codex_config::merge_toml_values; -use codex_config::project_root_markers_from_config; -use codex_exec_server::ExecutorFileSystem; -use codex_exec_server::LOCAL_FS; -use codex_protocol::protocol::Product; -use codex_protocol::protocol::SkillScope; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_absolute_path::AbsolutePathBufGuard; -use dirs::home_dir; -use serde::Deserialize; -use std::collections::HashMap; -use std::collections::HashSet; -use std::collections::VecDeque; -use std::error::Error; -use std::fmt; -use std::io; -use std::path::Component; -use std::path::PathBuf; -use std::sync::Arc; -use toml::Value as TomlValue; -use tracing::error; - -#[derive(Debug, Deserialize)] -struct SkillFrontmatter { - #[serde(default)] - name: Option, - #[serde(default)] - description: Option, - #[serde(default)] - metadata: SkillFrontmatterMetadata, -} - -#[derive(Debug, Default, Deserialize)] -struct SkillFrontmatterMetadata { - #[serde(default, rename = "short-description")] - short_description: Option, -} - -#[derive(Debug, Default, Deserialize)] -struct SkillMetadataFile { - #[serde(default)] - interface: Option, - #[serde(default)] - dependencies: Option, - #[serde(default)] - policy: Option, -} - -#[derive(Default)] -struct LoadedSkillMetadata { - interface: Option, - dependencies: Option, - policy: Option, -} - -#[derive(Debug, Default, Deserialize)] -struct Interface { - display_name: Option, - short_description: Option, - icon_small: Option, - icon_large: Option, - brand_color: Option, - default_prompt: Option, -} - -#[derive(Debug, Default, Deserialize)] -struct Dependencies { - #[serde(default)] - tools: Vec, -} - -#[derive(Debug, Deserialize)] -struct Policy { - #[serde(default)] - allow_implicit_invocation: Option, - #[serde(default)] - products: Vec, -} - -#[derive(Debug, Default, Deserialize)] -struct DependencyTool { - #[serde(rename = "type")] - kind: Option, - value: Option, - description: Option, - transport: Option, - command: Option, - url: Option, -} - -const SKILLS_FILENAME: &str = "SKILL.md"; -const AGENTS_DIR_NAME: &str = ".agents"; -const SKILLS_METADATA_DIR: &str = "agents"; -const SKILLS_METADATA_FILENAME: &str = "openai.yaml"; -const SKILLS_DIR_NAME: &str = "skills"; -const MAX_NAME_LEN: usize = 64; -const MAX_DESCRIPTION_LEN: usize = 1024; -const MAX_SHORT_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN; -const MAX_DEFAULT_PROMPT_LEN: usize = MAX_DESCRIPTION_LEN; -const MAX_DEPENDENCY_TYPE_LEN: usize = MAX_NAME_LEN; -const MAX_DEPENDENCY_TRANSPORT_LEN: usize = MAX_NAME_LEN; -const MAX_DEPENDENCY_VALUE_LEN: usize = MAX_DESCRIPTION_LEN; -const MAX_DEPENDENCY_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN; -const MAX_DEPENDENCY_COMMAND_LEN: usize = MAX_DESCRIPTION_LEN; -const MAX_DEPENDENCY_URL_LEN: usize = MAX_DESCRIPTION_LEN; -// Traversal depth from the skills root. -const MAX_SCAN_DEPTH: usize = 6; -const MAX_SKILLS_DIRS_PER_ROOT: usize = 2000; - -#[derive(Debug)] -enum SkillParseError { - Read(std::io::Error), - MissingFrontmatter, - InvalidYaml(serde_yaml::Error), - MissingField(&'static str), - InvalidField { field: &'static str, reason: String }, -} - -impl fmt::Display for SkillParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SkillParseError::Read(e) => write!(f, "failed to read file: {e}"), - SkillParseError::MissingFrontmatter => { - write!(f, "missing YAML frontmatter delimited by ---") - } - SkillParseError::InvalidYaml(e) => write!(f, "invalid YAML: {e}"), - SkillParseError::MissingField(field) => write!(f, "missing field `{field}`"), - SkillParseError::InvalidField { field, reason } => { - write!(f, "invalid {field}: {reason}") - } - } - } -} - -impl Error for SkillParseError {} - -pub struct SkillRoot { - pub path: AbsolutePathBuf, - pub scope: SkillScope, - pub file_system: Arc, -} - -pub async fn load_skills_from_roots(roots: I) -> SkillLoadOutcome -where - I: IntoIterator, -{ - let mut outcome = SkillLoadOutcome::default(); - let mut file_systems_by_skill_path: HashMap> = - HashMap::new(); - for root in roots { - let fs = root.file_system; - let skills_before_root = outcome.skills.len(); - discover_skills_under_root(fs.as_ref(), &root.path, root.scope, &mut outcome).await; - for skill in &outcome.skills[skills_before_root..] { - file_systems_by_skill_path - .entry(skill.path_to_skills_md.clone()) - .or_insert_with(|| Arc::clone(&fs)); - } - } - - let mut seen: HashSet = HashSet::new(); - outcome - .skills - .retain(|skill| seen.insert(skill.path_to_skills_md.clone())); - let retained_skill_paths: HashSet = outcome - .skills - .iter() - .map(|skill| skill.path_to_skills_md.clone()) - .collect(); - file_systems_by_skill_path.retain(|path, _| retained_skill_paths.contains(path)); - outcome.file_systems_by_skill_path = SkillFileSystemsByPath::new(file_systems_by_skill_path); - - fn scope_rank(scope: SkillScope) -> u8 { - // Higher-priority scopes first (matches root scan order for dedupe). - match scope { - SkillScope::Repo => 0, - SkillScope::User => 1, - SkillScope::System => 2, - SkillScope::Admin => 3, - } - } - - outcome.skills.sort_by(|a, b| { - scope_rank(a.scope) - .cmp(&scope_rank(b.scope)) - .then_with(|| a.name.cmp(&b.name)) - .then_with(|| a.path_to_skills_md.cmp(&b.path_to_skills_md)) - }); - - outcome -} - -pub(crate) async fn skill_roots( - fs: Option>, - config_layer_stack: &ConfigLayerStack, - cwd: &AbsolutePathBuf, - plugin_skill_roots: Vec, -) -> Vec { - let home_dir = - home_dir().and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok()); - skill_roots_with_home_dir( - fs, - config_layer_stack, - cwd, - home_dir.as_ref(), - plugin_skill_roots, - ) - .await -} - -async fn skill_roots_with_home_dir( - fs: Option>, - config_layer_stack: &ConfigLayerStack, - cwd: &AbsolutePathBuf, - home_dir: Option<&AbsolutePathBuf>, - plugin_skill_roots: Vec, -) -> Vec { - let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir, fs.clone()); - roots.extend(plugin_skill_roots.into_iter().map(|path| SkillRoot { - path, - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - })); - roots.extend(repo_agents_skill_roots(fs, config_layer_stack, cwd).await); - dedupe_skill_roots_by_path(&mut roots); - roots -} - -fn skill_roots_from_layer_stack_inner( - config_layer_stack: &ConfigLayerStack, - home_dir: Option<&AbsolutePathBuf>, - repo_fs: Option>, -) -> Vec { - let mut roots = Vec::new(); - - for layer in config_layer_stack.get_layers( - ConfigLayerStackOrdering::HighestPrecedenceFirst, - /*include_disabled*/ true, - ) { - let Some(config_folder) = layer.config_folder() else { - continue; - }; - - match &layer.name { - ConfigLayerSource::Project { .. } => { - if let Some(repo_fs) = &repo_fs { - roots.push(SkillRoot { - path: config_folder.join(SKILLS_DIR_NAME), - scope: SkillScope::Repo, - file_system: Arc::clone(repo_fs), - }); - } - } - ConfigLayerSource::User { .. } => { - // Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward - // compatibility. - roots.push(SkillRoot { - path: config_folder.join(SKILLS_DIR_NAME), - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - }); - - // `$HOME/.agents/skills` (user-installed skills). - if let Some(home_dir) = home_dir { - roots.push(SkillRoot { - path: home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - }); - } - - // Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a - // special case (not a config layer). - roots.push(SkillRoot { - path: system_cache_root_dir(&config_folder), - scope: SkillScope::System, - file_system: Arc::clone(&LOCAL_FS), - }); - } - ConfigLayerSource::System { .. } => { - // The system config layer lives under `/etc/codex/` on Unix, so treat - // `/etc/codex/skills` as admin-scoped skills. - roots.push(SkillRoot { - path: config_folder.join(SKILLS_DIR_NAME), - scope: SkillScope::Admin, - file_system: Arc::clone(&LOCAL_FS), - }); - } - ConfigLayerSource::Mdm { .. } - | ConfigLayerSource::SessionFlags - | ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. } - | ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {} - } - } - - roots -} - -async fn repo_agents_skill_roots( - fs: Option>, - config_layer_stack: &ConfigLayerStack, - cwd: &AbsolutePathBuf, -) -> Vec { - let Some(fs) = fs else { - return Vec::new(); - }; - let project_root_markers = project_root_markers_from_stack(config_layer_stack); - let project_root = find_project_root(fs.as_ref(), cwd, &project_root_markers).await; - let dirs = dirs_between_project_root_and_cwd(cwd, &project_root); - let mut roots = Vec::new(); - for dir in dirs { - let agents_skills = dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME); - match fs.get_metadata(&agents_skills, /*sandbox*/ None).await { - Ok(metadata) if metadata.is_directory => roots.push(SkillRoot { - path: agents_skills, - scope: SkillScope::Repo, - file_system: Arc::clone(&fs), - }), - Ok(_) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => { - tracing::warn!( - "failed to stat repo skills root {}: {err:#}", - agents_skills.display() - ); - } - } - } - roots -} - -fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec { - let mut merged = TomlValue::Table(toml::map::Map::new()); - for layer in config_layer_stack.get_layers( - ConfigLayerStackOrdering::LowestPrecedenceFirst, - /*include_disabled*/ false, - ) { - if matches!(layer.name, ConfigLayerSource::Project { .. }) { - continue; - } - merge_toml_values(&mut merged, &layer.config); - } - - match project_root_markers_from_config(&merged) { - Ok(Some(markers)) => markers, - Ok(None) => default_project_root_markers(), - Err(err) => { - tracing::warn!("invalid project_root_markers: {err}"); - default_project_root_markers() - } - } -} - -async fn find_project_root( - fs: &dyn ExecutorFileSystem, - cwd: &AbsolutePathBuf, - project_root_markers: &[String], -) -> AbsolutePathBuf { - if project_root_markers.is_empty() { - return cwd.clone(); - } - - for ancestor in cwd.ancestors() { - for marker in project_root_markers { - let marker_path = ancestor.join(marker); - match fs.get_metadata(&marker_path, /*sandbox*/ None).await { - Ok(_) => return ancestor, - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => { - tracing::warn!( - "failed to stat project root marker {}: {err:#}", - marker_path.display() - ); - } - } - } - } - - cwd.clone() -} - -fn dirs_between_project_root_and_cwd( - cwd: &AbsolutePathBuf, - project_root: &AbsolutePathBuf, -) -> Vec { - let mut dirs = cwd - .ancestors() - .scan(false, |done, dir| { - if *done { - None - } else { - if &dir == project_root { - *done = true; - } - Some(dir) - } - }) - .collect::>(); - dirs.reverse(); - dirs -} - -fn dedupe_skill_roots_by_path(roots: &mut Vec) { - let mut seen: HashSet = HashSet::new(); - roots.retain(|root| seen.insert(root.path.clone())); -} - -fn canonicalize_for_skill_identity(path: &AbsolutePathBuf) -> AbsolutePathBuf { - path.canonicalize().unwrap_or_else(|_| path.clone()) -} - -async fn discover_skills_under_root( - fs: &dyn ExecutorFileSystem, - root: &AbsolutePathBuf, - scope: SkillScope, - outcome: &mut SkillLoadOutcome, -) { - let root = canonicalize_for_skill_identity(root); - - match fs.get_metadata(&root, /*sandbox*/ None).await { - Ok(metadata) if metadata.is_directory => {} - Ok(_) => return, - Err(err) if err.kind() == io::ErrorKind::NotFound => return, - Err(err) => { - error!("failed to stat skills root {}: {err:#}", root.display()); - return; - } - } - - fn enqueue_dir( - queue: &mut VecDeque<(AbsolutePathBuf, usize)>, - visited_dirs: &mut HashSet, - truncated_by_dir_limit: &mut bool, - path: AbsolutePathBuf, - depth: usize, - ) { - if depth > MAX_SCAN_DEPTH { - return; - } - if visited_dirs.len() >= MAX_SKILLS_DIRS_PER_ROOT { - *truncated_by_dir_limit = true; - return; - } - if visited_dirs.insert(path.clone()) { - queue.push_back((path, depth)); - } - } - - // Follow symlinked directories for user, admin, and repo skills. System skills are written by Codex itself. - let follow_symlinks = matches!( - scope, - SkillScope::Repo | SkillScope::User | SkillScope::Admin - ); - - let mut visited_dirs: HashSet = HashSet::new(); - visited_dirs.insert(root.clone()); - - let mut queue: VecDeque<(AbsolutePathBuf, usize)> = VecDeque::from([(root.clone(), 0)]); - let mut truncated_by_dir_limit = false; - - while let Some((dir, depth)) = queue.pop_front() { - let entries = match fs.read_directory(&dir, /*sandbox*/ None).await { - Ok(entries) => entries, - Err(e) => { - error!("failed to read skills dir {}: {e:#}", dir.display()); - continue; - } - }; - - for entry in entries { - let file_name = entry.file_name; - if file_name.starts_with('.') { - continue; - } - - let path = dir.join(&file_name); - let metadata = match fs.get_metadata(&path, /*sandbox*/ None).await { - Ok(metadata) => metadata, - Err(e) => { - error!("failed to stat skills path {}: {e:#}", path.display()); - continue; - } - }; - - if metadata.is_symlink { - if !follow_symlinks { - continue; - } - match fs.read_directory(&path, /*sandbox*/ None).await { - Ok(_) => { - let resolved_dir = canonicalize_for_skill_identity(&path); - enqueue_dir( - &mut queue, - &mut visited_dirs, - &mut truncated_by_dir_limit, - resolved_dir, - depth + 1, - ); - } - Err(err) - if matches!( - err.kind(), - io::ErrorKind::NotADirectory | io::ErrorKind::NotFound - ) => {} - Err(err) => { - error!( - "failed to read skills symlink dir {}: {err:#}", - path.display() - ); - } - } - continue; - } - - if metadata.is_directory { - let resolved_dir = canonicalize_for_skill_identity(&path); - enqueue_dir( - &mut queue, - &mut visited_dirs, - &mut truncated_by_dir_limit, - resolved_dir, - depth + 1, - ); - continue; - } - - if metadata.is_file && file_name == SKILLS_FILENAME { - match parse_skill_file(fs, &path, scope).await { - Ok(skill) => { - outcome.skills.push(skill); - } - Err(err) => { - if scope != SkillScope::System { - outcome.errors.push(SkillError { - path: path.clone(), - message: err.to_string(), - }); - } - } - } - } - } - } - - if truncated_by_dir_limit { - tracing::warn!( - "skills scan truncated after {} directories (root: {})", - MAX_SKILLS_DIRS_PER_ROOT, - root.display() - ); - } -} - -async fn parse_skill_file( - fs: &dyn ExecutorFileSystem, - path: &AbsolutePathBuf, - scope: SkillScope, -) -> Result { - let contents = fs - .read_file_text(path, /*sandbox*/ None) - .await - .map_err(SkillParseError::Read)?; - - let frontmatter = extract_frontmatter(&contents).ok_or(SkillParseError::MissingFrontmatter)?; - - let parsed: SkillFrontmatter = - serde_yaml::from_str(&frontmatter).map_err(SkillParseError::InvalidYaml)?; - - let base_name = parsed - .name - .as_deref() - .map(sanitize_single_line) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| default_skill_name(path)); - let name = base_name.to_string(); - let description = parsed - .description - .as_deref() - .map(sanitize_single_line) - .unwrap_or_default(); - let short_description = parsed - .metadata - .short_description - .as_deref() - .map(sanitize_single_line) - .filter(|value| !value.is_empty()); - let LoadedSkillMetadata { - interface, - dependencies, - policy, - } = load_skill_metadata(fs, path).await; - - validate_len(&name, MAX_NAME_LEN, "name")?; - validate_len(&description, MAX_DESCRIPTION_LEN, "description")?; - if let Some(short_description) = short_description.as_deref() { - validate_len( - short_description, - MAX_SHORT_DESCRIPTION_LEN, - "metadata.short-description", - )?; - } - - let resolved_path = canonicalize_for_skill_identity(path); - - Ok(SkillMetadata { - name, - description, - short_description, - interface, - dependencies, - policy, - path_to_skills_md: resolved_path, - scope, - }) -} - -fn default_skill_name(path: &AbsolutePathBuf) -> String { - path.parent() - .and_then(|parent| { - parent - .file_name() - .and_then(|name| name.to_str()) - .map(sanitize_single_line) - }) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "skill".to_string()) -} - -async fn load_skill_metadata( - fs: &dyn ExecutorFileSystem, - skill_path: &AbsolutePathBuf, -) -> LoadedSkillMetadata { - // Fail open: optional metadata should not block loading SKILL.md. - let Some(skill_dir) = skill_path.parent() else { - return LoadedSkillMetadata::default(); - }; - let metadata_path = skill_dir - .join(SKILLS_METADATA_DIR) - .join(SKILLS_METADATA_FILENAME); - match fs.get_metadata(&metadata_path, /*sandbox*/ None).await { - Ok(metadata) if metadata.is_file => {} - Ok(_) => return LoadedSkillMetadata::default(), - Err(error) if error.kind() == io::ErrorKind::NotFound => { - return LoadedSkillMetadata::default(); - } - Err(error) => { - tracing::warn!( - "ignoring {path}: failed to stat {label}: {error}", - path = metadata_path.display(), - label = SKILLS_METADATA_FILENAME - ); - return LoadedSkillMetadata::default(); - } - } - - let contents = match fs.read_file_text(&metadata_path, /*sandbox*/ None).await { - Ok(contents) => contents, - Err(error) => { - tracing::warn!( - "ignoring {path}: failed to read {label}: {error}", - path = metadata_path.display(), - label = SKILLS_METADATA_FILENAME - ); - return LoadedSkillMetadata::default(); - } - }; - - let parsed: SkillMetadataFile = { - let _guard = AbsolutePathBufGuard::new(skill_dir.as_path()); - match serde_yaml::from_str(&contents) { - Ok(parsed) => parsed, - Err(error) => { - tracing::warn!( - "ignoring {path}: invalid {label}: {error}", - path = metadata_path.display(), - label = SKILLS_METADATA_FILENAME - ); - return LoadedSkillMetadata::default(); - } - } - }; - - let SkillMetadataFile { - interface, - dependencies, - policy, - } = parsed; - LoadedSkillMetadata { - interface: resolve_interface(interface, &skill_dir), - dependencies: resolve_dependencies(dependencies), - policy: resolve_policy(policy), - } -} - -fn resolve_interface( - interface: Option, - skill_dir: &AbsolutePathBuf, -) -> Option { - let interface = interface?; - let interface = SkillInterface { - display_name: resolve_str( - interface.display_name, - MAX_NAME_LEN, - "interface.display_name", - ), - short_description: resolve_str( - interface.short_description, - MAX_SHORT_DESCRIPTION_LEN, - "interface.short_description", - ), - icon_small: resolve_asset_path(skill_dir, "interface.icon_small", interface.icon_small), - icon_large: resolve_asset_path(skill_dir, "interface.icon_large", interface.icon_large), - brand_color: resolve_color_str(interface.brand_color, "interface.brand_color"), - default_prompt: resolve_str( - interface.default_prompt, - MAX_DEFAULT_PROMPT_LEN, - "interface.default_prompt", - ), - }; - let has_fields = interface.display_name.is_some() - || interface.short_description.is_some() - || interface.icon_small.is_some() - || interface.icon_large.is_some() - || interface.brand_color.is_some() - || interface.default_prompt.is_some(); - if has_fields { Some(interface) } else { None } -} - -fn resolve_dependencies(dependencies: Option) -> Option { - let dependencies = dependencies?; - let tools: Vec = dependencies - .tools - .into_iter() - .filter_map(resolve_dependency_tool) - .collect(); - if tools.is_empty() { - None - } else { - Some(SkillDependencies { tools }) - } -} - -fn resolve_policy(policy: Option) -> Option { - policy.map(|policy| SkillPolicy { - allow_implicit_invocation: policy.allow_implicit_invocation, - products: policy.products, - }) -} - -fn resolve_dependency_tool(tool: DependencyTool) -> Option { - let r#type = resolve_required_str( - tool.kind, - MAX_DEPENDENCY_TYPE_LEN, - "dependencies.tools.type", - )?; - let value = resolve_required_str( - tool.value, - MAX_DEPENDENCY_VALUE_LEN, - "dependencies.tools.value", - )?; - let description = resolve_str( - tool.description, - MAX_DEPENDENCY_DESCRIPTION_LEN, - "dependencies.tools.description", - ); - let transport = resolve_str( - tool.transport, - MAX_DEPENDENCY_TRANSPORT_LEN, - "dependencies.tools.transport", - ); - let command = resolve_str( - tool.command, - MAX_DEPENDENCY_COMMAND_LEN, - "dependencies.tools.command", - ); - let url = resolve_str(tool.url, MAX_DEPENDENCY_URL_LEN, "dependencies.tools.url"); - - Some(SkillToolDependency { - r#type, - value, - description, - transport, - command, - url, - }) -} - -fn resolve_asset_path( - skill_dir: &AbsolutePathBuf, - field: &'static str, - path: Option, -) -> Option { - // Icons must be relative paths under the skill's assets/ directory; otherwise return None. - let path = path?; - if path.as_os_str().is_empty() { - return None; - } - - let assets_dir = skill_dir.join("assets"); - if path.is_absolute() { - tracing::warn!( - "ignoring {field}: icon must be a relative assets path (not {})", - assets_dir.display() - ); - return None; - } - - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - Component::CurDir => {} - Component::Normal(component) => normalized.push(component), - Component::ParentDir => { - tracing::warn!("ignoring {field}: icon path must not contain '..'"); - return None; - } - _ => { - tracing::warn!("ignoring {field}: icon path must be under assets/"); - return None; - } - } - } - - let mut components = normalized.components(); - match components.next() { - Some(Component::Normal(component)) if component == "assets" => {} - _ => { - tracing::warn!("ignoring {field}: icon path must be under assets/"); - return None; - } - } - - Some(skill_dir.join(normalized)) -} - -fn sanitize_single_line(raw: &str) -> String { - raw.split_whitespace().collect::>().join(" ") -} - -fn validate_len( - value: &str, - max_len: usize, - field_name: &'static str, -) -> Result<(), SkillParseError> { - if value.is_empty() { - return Err(SkillParseError::MissingField(field_name)); - } - if value.chars().count() > max_len { - return Err(SkillParseError::InvalidField { - field: field_name, - reason: format!("exceeds maximum length of {max_len} characters"), - }); - } - Ok(()) -} - -fn resolve_str(value: Option, max_len: usize, field: &'static str) -> Option { - let value = value?; - let value = sanitize_single_line(&value); - if value.is_empty() { - tracing::warn!("ignoring {field}: value is empty"); - return None; - } - if value.chars().count() > max_len { - tracing::warn!("ignoring {field}: exceeds maximum length of {max_len} characters"); - return None; - } - Some(value) -} - -fn resolve_required_str( - value: Option, - max_len: usize, - field: &'static str, -) -> Option { - let Some(value) = value else { - tracing::warn!("ignoring {field}: value is missing"); - return None; - }; - resolve_str(Some(value), max_len, field) -} - -fn resolve_color_str(value: Option, field: &'static str) -> Option { - let value = value?; - let value = value.trim(); - if value.is_empty() { - tracing::warn!("ignoring {field}: value is empty"); - return None; - } - let mut chars = value.chars(); - if value.len() == 7 && chars.next() == Some('#') && chars.all(|c| c.is_ascii_hexdigit()) { - Some(value.to_string()) - } else { - tracing::warn!("ignoring {field}: expected #RRGGBB, got {value}"); - None - } -} - -fn extract_frontmatter(contents: &str) -> Option { - 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")) -} -#[cfg(test)] -pub(crate) async fn skill_roots_from_layer_stack( - fs: Arc, - config_layer_stack: &ConfigLayerStack, - cwd: &AbsolutePathBuf, - home_dir: Option<&AbsolutePathBuf>, -) -> Vec { - skill_roots_with_home_dir(Some(fs), config_layer_stack, cwd, home_dir, Vec::new()).await -} - -#[cfg(test)] -#[path = "loader_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-skills/src/loader_tests.rs b/vendor/codex/codex-rs/core-skills/src/loader_tests.rs deleted file mode 100644 index f3f69f25..00000000 --- a/vendor/codex/codex-rs/core-skills/src/loader_tests.rs +++ /dev/null @@ -1,1724 +0,0 @@ -use super::*; -use codex_config::CONFIG_TOML_FILE; -use codex_config::ConfigLayerEntry; -use codex_config::ConfigLayerStack; -use codex_config::ConfigRequirements; -use codex_config::ConfigRequirementsToml; -use codex_exec_server::LOCAL_FS; -use codex_protocol::protocol::Product; -use codex_protocol::protocol::SkillScope; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_absolute_path::test_support::PathBufExt; -use codex_utils_absolute_path::test_support::PathExt; -use dunce::canonicalize as canonicalize_path; -use pretty_assertions::assert_eq; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use tempfile::TempDir; -use toml::Value as TomlValue; - -const REPO_ROOT_CONFIG_DIR_NAME: &str = ".codex"; - -struct TestConfig { - cwd: AbsolutePathBuf, - config_layer_stack: ConfigLayerStack, -} - -async fn make_config(codex_home: &TempDir) -> TestConfig { - make_config_for_cwd(codex_home, codex_home.path().to_path_buf()).await -} - -fn config_file(path: PathBuf) -> AbsolutePathBuf { - path.abs() -} - -fn project_layers_for_cwd(cwd: &Path) -> Vec { - let cwd_dir = if cwd.is_dir() { - cwd.to_path_buf() - } else { - cwd.parent() - .expect("file cwd should have a parent directory") - .to_path_buf() - }; - let project_root = cwd_dir - .ancestors() - .find(|ancestor| ancestor.join(".git").exists()) - .unwrap_or(cwd_dir.as_path()) - .to_path_buf(); - - let mut layers = cwd_dir - .ancestors() - .scan(false, |done, dir| { - if *done { - None - } else { - if dir == project_root { - *done = true; - } - Some(dir.to_path_buf()) - } - }) - .collect::>(); - layers.reverse(); - - layers - .into_iter() - .filter_map(|dir| { - let dot_codex = dir.join(REPO_ROOT_CONFIG_DIR_NAME); - dot_codex.is_dir().then(|| { - ConfigLayerEntry::new( - ConfigLayerSource::Project { - dot_codex_folder: dot_codex.abs(), - }, - TomlValue::Table(toml::map::Map::new()), - ) - }) - }) - .collect() -} - -async fn make_config_for_cwd(codex_home: &TempDir, cwd: PathBuf) -> TestConfig { - let user_config_path = codex_home.path().join(CONFIG_TOML_FILE); - let system_config_path = codex_home.path().join("etc/codex/config.toml"); - fs::create_dir_all( - system_config_path - .parent() - .expect("system config path should have a parent"), - ) - .expect("create fake system config dir"); - - let mut layers = vec![ - ConfigLayerEntry::new( - ConfigLayerSource::System { - file: config_file(system_config_path), - }, - TomlValue::Table(toml::map::Map::new()), - ), - ConfigLayerEntry::new( - ConfigLayerSource::User { - file: config_file(user_config_path), - }, - TomlValue::Table(toml::map::Map::new()), - ), - ]; - layers.extend(project_layers_for_cwd(&cwd)); - - let cwd_abs = cwd.abs(); - TestConfig { - cwd: cwd_abs, - config_layer_stack: ConfigLayerStack::new( - layers, - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"), - } -} - -async fn load_skills_for_test(config: &TestConfig) -> SkillLoadOutcome { - // Keep unit tests hermetic by never scanning the real `$HOME/.agents/skills`. - super::load_skills_from_roots( - super::skill_roots_from_layer_stack( - Arc::clone(&LOCAL_FS), - &config.config_layer_stack, - &config.cwd, - /*home_dir*/ None, - ) - .await, - ) - .await -} - -fn mark_as_git_repo(dir: &Path) { - // Config/project-root discovery only checks for the presence of `.git` (file or dir), - // so we can avoid shelling out to `git init` in tests. - fs::write(dir.join(".git"), "gitdir: fake\n").unwrap(); -} - -fn normalized(path: &Path) -> AbsolutePathBuf { - canonicalize_path(path) - .unwrap_or_else(|_| path.to_path_buf()) - .abs() -} - -#[tokio::test] -async fn skill_roots_from_layer_stack_maps_user_to_user_and_system_cache_and_system_to_admin() --> anyhow::Result<()> { - let tmp = tempfile::tempdir()?; - - let system_folder = tmp.path().join("etc/codex"); - let home_folder = tmp.path().join("home"); - let user_folder = home_folder.join("codex"); - fs::create_dir_all(&system_folder)?; - fs::create_dir_all(&user_folder)?; - - // The file path doesn't need to exist; it's only used to derive the config folder. - let system_file = system_folder.join("config.toml").abs(); - let user_file = user_folder.join("config.toml").abs(); - - let layers = vec![ - ConfigLayerEntry::new( - ConfigLayerSource::System { file: system_file }, - TomlValue::Table(toml::map::Map::new()), - ), - ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - TomlValue::Table(toml::map::Map::new()), - ), - ]; - let stack = ConfigLayerStack::new( - layers, - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - )?; - - let home_folder_abs = home_folder.abs(); - let got = skill_roots_from_layer_stack( - Arc::clone(&LOCAL_FS), - &stack, - &home_folder_abs, - Some(&home_folder_abs), - ) - .await - .into_iter() - .map(|root| (root.scope, root.path.to_path_buf())) - .collect::>(); - - assert_eq!( - got, - vec![ - (SkillScope::User, user_folder.join("skills")), - ( - SkillScope::User, - home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) - ), - ( - SkillScope::System, - user_folder.join("skills").join(".system") - ), - (SkillScope::Admin, system_folder.join("skills")), - ] - ); - - Ok(()) -} - -#[tokio::test] -async fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyhow::Result<()> { - let tmp = tempfile::tempdir()?; - - let home_folder = tmp.path().join("home"); - let user_folder = home_folder.join("codex"); - fs::create_dir_all(&user_folder)?; - - let project_root = tmp.path().join("repo"); - let dot_codex = project_root.join(".codex"); - fs::create_dir_all(&dot_codex)?; - - let user_file = user_folder.join("config.toml").abs(); - let project_dot_codex = dot_codex.abs(); - - let layers = vec![ - ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - TomlValue::Table(toml::map::Map::new()), - ), - ConfigLayerEntry::new_disabled( - ConfigLayerSource::Project { - dot_codex_folder: project_dot_codex, - }, - TomlValue::Table(toml::map::Map::new()), - "marked untrusted", - ), - ]; - let stack = ConfigLayerStack::new( - layers, - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - )?; - - let home_folder_abs = home_folder.abs(); - let project_root_abs = project_root.abs(); - let got = skill_roots_from_layer_stack( - Arc::clone(&LOCAL_FS), - &stack, - &project_root_abs, - Some(&home_folder_abs), - ) - .await - .into_iter() - .map(|root| (root.scope, root.path.to_path_buf())) - .collect::>(); - - assert_eq!( - got, - vec![ - (SkillScope::Repo, dot_codex.join("skills")), - (SkillScope::User, user_folder.join("skills")), - ( - SkillScope::User, - home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) - ), - ( - SkillScope::System, - user_folder.join("skills").join(".system") - ), - ] - ); - - Ok(()) -} - -#[tokio::test] -async fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> { - let tmp = tempfile::tempdir()?; - - let home_folder = tmp.path().join("home"); - let user_folder = home_folder.join("codex"); - fs::create_dir_all(&user_folder)?; - - let user_file = user_folder.join("config.toml").abs(); - let layers = vec![ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - TomlValue::Table(toml::map::Map::new()), - )]; - let stack = ConfigLayerStack::new( - layers, - ConfigRequirements::default(), - ConfigRequirementsToml::default(), - )?; - - let skill_path = write_skill_at( - &home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), - "agents-home", - "agents-home-skill", - "from home agents", - ); - - let home_folder_abs = home_folder.abs(); - let roots = skill_roots_from_layer_stack( - Arc::clone(&LOCAL_FS), - &stack, - &home_folder_abs, - Some(&home_folder_abs), - ) - .await; - let outcome = load_skills_from_roots(roots).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "agents-home-skill".to_string(), - description: "from home agents".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); - - Ok(()) -} - -fn write_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) -> PathBuf { - write_skill_at(&codex_home.path().join("skills"), dir, name, description) -} - -fn write_system_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) -> PathBuf { - write_skill_at( - &codex_home.path().join("skills/.system"), - dir, - name, - description, - ) -} - -fn write_skill_at(root: &Path, dir: &str, name: &str, description: &str) -> PathBuf { - let skill_dir = root.join(dir); - fs::create_dir_all(&skill_dir).unwrap(); - let indented_description = description.replace('\n', "\n "); - let content = - format!("---\nname: {name}\ndescription: |-\n {indented_description}\n---\n\n# Body\n"); - let path = skill_dir.join(SKILLS_FILENAME); - fs::write(&path, content).unwrap(); - path -} - -fn write_raw_skill_at(root: &Path, dir: &str, frontmatter: &str) -> PathBuf { - let skill_dir = root.join(dir); - fs::create_dir_all(&skill_dir).unwrap(); - let path = skill_dir.join(SKILLS_FILENAME); - let content = format!("---\n{frontmatter}\n---\n\n# Body\n"); - fs::write(&path, content).unwrap(); - path -} - -fn write_skill_metadata_at(skill_dir: &Path, contents: &str) -> PathBuf { - let path = skill_dir - .join(SKILLS_METADATA_DIR) - .join(SKILLS_METADATA_FILENAME); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).unwrap(); - } - fs::write(&path, contents).unwrap(); - path -} - -fn write_skill_interface_at(skill_dir: &Path, contents: &str) -> PathBuf { - write_skill_metadata_at(skill_dir, contents) -} - -#[tokio::test] -async fn loads_skill_dependencies_metadata_from_yaml() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "dep-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_metadata_at( - skill_dir, - r#" -{ - "dependencies": { - "tools": [ - { - "type": "env_var", - "value": "GITHUB_TOKEN", - "description": "GitHub API token with repo scopes" - }, - { - "type": "mcp", - "value": "github", - "description": "GitHub MCP server", - "transport": "streamable_http", - "url": "https://example.com/mcp" - }, - { - "type": "cli", - "value": "gh", - "description": "GitHub CLI" - }, - { - "type": "mcp", - "value": "local-gh", - "description": "Local GH MCP server", - "transport": "stdio", - "command": "gh-mcp" - } - ] - } -} -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "dep-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: None, - dependencies: Some(SkillDependencies { - tools: vec![ - SkillToolDependency { - r#type: "env_var".to_string(), - value: "GITHUB_TOKEN".to_string(), - description: Some("GitHub API token with repo scopes".to_string()), - transport: None, - command: None, - url: None, - }, - SkillToolDependency { - r#type: "mcp".to_string(), - value: "github".to_string(), - description: Some("GitHub MCP server".to_string()), - transport: Some("streamable_http".to_string()), - command: None, - url: Some("https://example.com/mcp".to_string()), - }, - SkillToolDependency { - r#type: "cli".to_string(), - value: "gh".to_string(), - description: Some("GitHub CLI".to_string()), - transport: None, - command: None, - url: None, - }, - SkillToolDependency { - r#type: "mcp".to_string(), - value: "local-gh".to_string(), - description: Some("Local GH MCP server".to_string()), - transport: Some("stdio".to_string()), - command: Some("gh-mcp".to_string()), - url: None, - }, - ], - }), - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn loads_skill_interface_metadata_from_yaml() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - let normalized_skill_dir = normalized(skill_dir); - - write_skill_interface_at( - skill_dir, - r##" -interface: - display_name: "UI Skill" - short_description: " short desc " - icon_small: "./assets/small-400px.png" - icon_large: "./assets/large-logo.svg" - brand_color: "#3B82F6" - default_prompt: " default prompt " -"##, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - let user_skills: Vec = outcome - .skills - .into_iter() - .filter(|skill| skill.scope == SkillScope::User) - .collect(); - assert_eq!( - user_skills, - vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: Some(SkillInterface { - display_name: Some("UI Skill".to_string()), - short_description: Some("short desc".to_string()), - icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")), - icon_large: Some(normalized_skill_dir.join("assets/large-logo.svg")), - brand_color: Some("#3B82F6".to_string()), - default_prompt: Some("default prompt".to_string()), - }), - dependencies: None, - policy: None, - path_to_skills_md: normalized(skill_path.as_path()), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn loads_skill_policy_from_yaml() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "policy-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_metadata_at( - skill_dir, - r#" -policy: - allow_implicit_invocation: false -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 1); - assert_eq!( - outcome.skills[0].policy, - Some(SkillPolicy { - allow_implicit_invocation: Some(false), - products: vec![], - }) - ); - assert!(outcome.allowed_skills_for_implicit_invocation().is_empty()); -} - -#[tokio::test] -async fn empty_skill_policy_defaults_to_allow_implicit_invocation() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "policy-empty", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_metadata_at( - skill_dir, - r#" -policy: {} -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 1); - assert_eq!( - outcome.skills[0].policy, - Some(SkillPolicy { - allow_implicit_invocation: None, - products: vec![], - }) - ); - assert_eq!( - outcome.allowed_skills_for_implicit_invocation(), - outcome.skills - ); -} - -#[tokio::test] -async fn loads_skill_policy_products_from_yaml() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "policy-products", "from yaml"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_metadata_at( - skill_dir, - r#" -policy: - products: - - codex - - CHATGPT - - atlas -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 1); - assert_eq!( - outcome.skills[0].policy, - Some(SkillPolicy { - allow_implicit_invocation: None, - products: vec![Product::Codex, Product::Chatgpt, Product::Atlas], - }) - ); -} - -#[tokio::test] -async fn accepts_icon_paths_under_assets_dir() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - let normalized_skill_dir = normalized(skill_dir); - - write_skill_interface_at( - skill_dir, - r#" -{ - "interface": { - "display_name": "UI Skill", - "icon_small": "assets/icon.png", - "icon_large": "./assets/logo.svg" - } -} -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: Some(SkillInterface { - display_name: Some("UI Skill".to_string()), - short_description: None, - icon_small: Some(normalized_skill_dir.join("assets/icon.png")), - icon_large: Some(normalized_skill_dir.join("assets/logo.svg")), - brand_color: None, - default_prompt: None, - }), - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn ignores_invalid_brand_color() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_interface_at( - skill_dir, - r#" -{ - "interface": { - "brand_color": "blue" - } -} -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn ignores_default_prompt_over_max_length() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - let normalized_skill_dir = normalized(skill_dir); - let too_long = "x".repeat(MAX_DEFAULT_PROMPT_LEN + 1); - - write_skill_interface_at( - skill_dir, - &format!( - r##" -{{ - "interface": {{ - "display_name": "UI Skill", - "icon_small": "./assets/small-400px.png", - "default_prompt": "{too_long}" - }} -}} -"## - ), - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: Some(SkillInterface { - display_name: Some("UI Skill".to_string()), - short_description: None, - icon_small: Some(normalized_skill_dir.join("assets/small-400px.png")), - icon_large: None, - brand_color: None, - default_prompt: None, - }), - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn drops_interface_when_icons_are_invalid() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "ui-skill", "from json"); - let skill_dir = skill_path.parent().expect("skill dir"); - - write_skill_interface_at( - skill_dir, - r#" -{ - "interface": { - "icon_small": "icon.png", - "icon_large": "./assets/../logo.svg" - } -} -"#, - ); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "ui-skill".to_string(), - description: "from json".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[cfg(unix)] -fn symlink_dir(target: &Path, link: &Path) { - std::os::unix::fs::symlink(target, link).unwrap(); -} - -#[cfg(unix)] -fn symlink_file(target: &Path, link: &Path) { - std::os::unix::fs::symlink(target, link).unwrap(); -} - -#[tokio::test] -#[cfg(unix)] -async fn loads_skills_via_symlinked_subdir_for_user_scope() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let shared = tempfile::tempdir().expect("tempdir"); - - let shared_skill_path = write_skill_at(shared.path(), "demo", "linked-skill", "from link"); - - fs::create_dir_all(codex_home.path().join("skills")).unwrap(); - symlink_dir(shared.path(), &codex_home.path().join("skills/shared")); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "linked-skill".to_string(), - description: "from link".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&shared_skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -#[cfg(unix)] -async fn ignores_symlinked_skill_file_for_user_scope() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let shared = tempfile::tempdir().expect("tempdir"); - - let shared_skill_path = write_skill_at(shared.path(), "demo", "linked-file-skill", "from link"); - - let skill_dir = codex_home.path().join("skills/demo"); - fs::create_dir_all(&skill_dir).unwrap(); - symlink_file(&shared_skill_path, &skill_dir.join(SKILLS_FILENAME)); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills, Vec::new()); -} - -#[tokio::test] -#[cfg(unix)] -async fn does_not_loop_on_symlink_cycle_for_user_scope() { - let codex_home = tempfile::tempdir().expect("tempdir"); - - // Create a cycle: - // $CODEX_HOME/skills/cycle/loop -> $CODEX_HOME/skills/cycle - let cycle_dir = codex_home.path().join("skills/cycle"); - fs::create_dir_all(&cycle_dir).unwrap(); - symlink_dir(&cycle_dir, &cycle_dir.join("loop")); - - let skill_path = write_skill_at(&cycle_dir, "demo", "cycle-skill", "still loads"); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "cycle-skill".to_string(), - description: "still loads".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -#[cfg(unix)] -async fn loads_skills_via_symlinked_subdir_for_admin_scope() { - let admin_root = tempfile::tempdir().expect("tempdir"); - let shared = tempfile::tempdir().expect("tempdir"); - - let shared_skill_path = - write_skill_at(shared.path(), "demo", "admin-linked-skill", "from link"); - fs::create_dir_all(admin_root.path()).unwrap(); - symlink_dir(shared.path(), &admin_root.path().join("shared")); - - let outcome = load_skills_from_roots([SkillRoot { - path: admin_root.path().abs(), - scope: SkillScope::Admin, - file_system: Arc::clone(&LOCAL_FS), - }]) - .await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "admin-linked-skill".to_string(), - description: "from link".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&shared_skill_path), - scope: SkillScope::Admin, - }] - ); -} - -#[tokio::test] -#[cfg(unix)] -async fn loads_skills_via_symlinked_subdir_for_repo_scope() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - let shared = tempfile::tempdir().expect("tempdir"); - - let linked_skill_path = write_skill_at(shared.path(), "demo", "repo-linked-skill", "from link"); - let repo_skills_root = repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME); - fs::create_dir_all(&repo_skills_root).unwrap(); - symlink_dir(shared.path(), &repo_skills_root.join("shared")); - - let cfg = make_config_for_cwd(&codex_home, repo_dir.path().to_path_buf()).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "repo-linked-skill".to_string(), - description: "from link".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&linked_skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -#[cfg(unix)] -async fn system_scope_ignores_symlinked_subdir() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let shared = tempfile::tempdir().expect("tempdir"); - - write_skill_at(shared.path(), "demo", "system-linked-skill", "from link"); - - let system_root = codex_home.path().join("skills/.system"); - fs::create_dir_all(&system_root).unwrap(); - symlink_dir(shared.path(), &system_root.join("shared")); - - let outcome = load_skills_from_roots([SkillRoot { - path: system_root.abs(), - scope: SkillScope::System, - file_system: Arc::clone(&LOCAL_FS), - }]) - .await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 0); -} - -#[tokio::test] -async fn respects_max_scan_depth_for_user_scope() { - let codex_home = tempfile::tempdir().expect("tempdir"); - - let within_depth_path = write_skill( - &codex_home, - "d0/d1/d2/d3/d4/d5", - "within-depth-skill", - "loads", - ); - let _too_deep_path = write_skill( - &codex_home, - "d0/d1/d2/d3/d4/d5/d6", - "too-deep-skill", - "should not load", - ); - - let skills_root = codex_home.path().join("skills"); - let outcome = load_skills_from_roots([SkillRoot { - path: skills_root.abs(), - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - }]) - .await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "within-depth-skill".to_string(), - description: "loads".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&within_depth_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn loads_valid_skill() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_skill(&codex_home, "demo", "demo-skill", "does things\ncarefully"); - let cfg = make_config(&codex_home).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "demo-skill".to_string(), - description: "does things carefully".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn falls_back_to_directory_name_when_skill_name_is_missing() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_path = write_raw_skill_at( - &codex_home.path().join("skills"), - "directory-derived", - "description: fallback name", - ); - let cfg = make_config(&codex_home).await; - - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "directory-derived".to_string(), - description: "fallback name".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn loads_short_description_from_metadata() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_dir = codex_home.path().join("skills/demo"); - fs::create_dir_all(&skill_dir).unwrap(); - let contents = "---\nname: demo-skill\ndescription: long description\nmetadata:\n short-description: short summary\n---\n\n# Body\n"; - let skill_path = skill_dir.join(SKILLS_FILENAME); - fs::write(&skill_path, contents).unwrap(); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "demo-skill".to_string(), - description: "long description".to_string(), - short_description: Some("short summary".to_string()), - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::User, - }] - ); -} - -#[tokio::test] -async fn enforces_short_description_length_limits() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let skill_dir = codex_home.path().join("skills/demo"); - fs::create_dir_all(&skill_dir).unwrap(); - let too_long = "x".repeat(MAX_SHORT_DESCRIPTION_LEN + 1); - let contents = format!( - "---\nname: demo-skill\ndescription: long description\nmetadata:\n short-description: {too_long}\n---\n\n# Body\n" - ); - fs::write(skill_dir.join(SKILLS_FILENAME), contents).unwrap(); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - assert_eq!(outcome.skills.len(), 0); - assert_eq!(outcome.errors.len(), 1); - assert!( - outcome.errors[0] - .message - .contains("invalid metadata.short-description"), - "expected length error, got: {:?}", - outcome.errors - ); -} - -#[tokio::test] -async fn skips_hidden_and_invalid() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let hidden_dir = codex_home.path().join("skills/.hidden"); - fs::create_dir_all(&hidden_dir).unwrap(); - fs::write( - hidden_dir.join(SKILLS_FILENAME), - "---\nname: hidden\ndescription: hidden\n---\n", - ) - .unwrap(); - - // Invalid because missing closing frontmatter. - let invalid_dir = codex_home.path().join("skills/invalid"); - fs::create_dir_all(&invalid_dir).unwrap(); - fs::write(invalid_dir.join(SKILLS_FILENAME), "---\nname: bad").unwrap(); - - let cfg = make_config(&codex_home).await; - let outcome = load_skills_for_test(&cfg).await; - assert_eq!(outcome.skills.len(), 0); - assert_eq!(outcome.errors.len(), 1); - assert!( - outcome.errors[0] - .message - .contains("missing YAML frontmatter"), - "expected frontmatter error" - ); -} - -#[tokio::test] -async fn enforces_length_limits() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let max_desc = "\u{1F4A1}".repeat(MAX_DESCRIPTION_LEN); - write_skill(&codex_home, "max-len", "max-len", &max_desc); - let cfg = make_config(&codex_home).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 1); - - let too_long_desc = "\u{1F4A1}".repeat(MAX_DESCRIPTION_LEN + 1); - write_skill(&codex_home, "too-long", "too-long", &too_long_desc); - let outcome = load_skills_for_test(&cfg).await; - assert_eq!(outcome.skills.len(), 1); - assert_eq!(outcome.errors.len(), 1); - assert!( - outcome.errors[0].message.contains("invalid description"), - "expected length error" - ); -} - -#[tokio::test] -async fn loads_skills_from_repo_root() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let skills_root = repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME); - let skill_path = write_skill_at(&skills_root, "repo", "repo-skill", "from repo"); - let cfg = make_config_for_cwd(&codex_home, repo_dir.path().to_path_buf()).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "repo-skill".to_string(), - description: "from repo".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -async fn loads_skills_from_agents_dir_without_codex_dir() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let skill_path = write_skill_at( - &repo_dir.path().join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), - "agents", - "agents-skill", - "from agents", - ); - let cfg = make_config_for_cwd(&codex_home, repo_dir.path().to_path_buf()).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "agents-skill".to_string(), - description: "from agents".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -async fn loads_skills_from_all_codex_dirs_under_project_root() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let nested_dir = repo_dir.path().join("nested/inner"); - fs::create_dir_all(&nested_dir).unwrap(); - - let root_skill_path = write_skill_at( - &repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "root", - "root-skill", - "from root", - ); - let nested_skill_path = write_skill_at( - &repo_dir - .path() - .join("nested") - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "nested", - "nested-skill", - "from nested", - ); - - let cfg = make_config_for_cwd(&codex_home, nested_dir).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![ - SkillMetadata { - name: "nested-skill".to_string(), - description: "from nested".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&nested_skill_path), - scope: SkillScope::Repo, - }, - SkillMetadata { - name: "root-skill".to_string(), - description: "from root".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&root_skill_path), - scope: SkillScope::Repo, - }, - ] - ); -} - -#[tokio::test] -async fn loads_skills_from_codex_dir_when_not_git_repo() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let work_dir = tempfile::tempdir().expect("tempdir"); - - let skill_path = write_skill_at( - &work_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "local", - "local-skill", - "from cwd", - ); - - let cfg = make_config_for_cwd(&codex_home, work_dir.path().to_path_buf()).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "local-skill".to_string(), - description: "from cwd".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -async fn deduplicates_by_path_preferring_first_root() { - let root = tempfile::tempdir().expect("tempdir"); - - let skill_path = write_skill_at(root.path(), "dupe", "dupe-skill", "from repo"); - - let outcome = load_skills_from_roots([ - SkillRoot { - path: root.path().abs(), - scope: SkillScope::Repo, - file_system: Arc::clone(&LOCAL_FS), - }, - SkillRoot { - path: root.path().abs(), - scope: SkillScope::User, - file_system: Arc::clone(&LOCAL_FS), - }, - ]) - .await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "dupe-skill".to_string(), - description: "from repo".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -async fn keeps_duplicate_names_from_repo_and_user() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let user_skill_path = write_skill(&codex_home, "user", "dupe-skill", "from user"); - let repo_skill_path = write_skill_at( - &repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "repo", - "dupe-skill", - "from repo", - ); - - let cfg = make_config_for_cwd(&codex_home, repo_dir.path().to_path_buf()).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![ - SkillMetadata { - name: "dupe-skill".to_string(), - description: "from repo".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&repo_skill_path), - scope: SkillScope::Repo, - }, - SkillMetadata { - name: "dupe-skill".to_string(), - description: "from user".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&user_skill_path), - scope: SkillScope::User, - }, - ] - ); -} - -#[tokio::test] -async fn keeps_duplicate_names_from_nested_codex_dirs() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let nested_dir = repo_dir.path().join("nested/inner"); - fs::create_dir_all(&nested_dir).unwrap(); - - let root_skill_path = write_skill_at( - &repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "root", - "dupe-skill", - "from root", - ); - let nested_skill_path = write_skill_at( - &repo_dir - .path() - .join("nested") - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "nested", - "dupe-skill", - "from nested", - ); - - let cfg = make_config_for_cwd(&codex_home, nested_dir).await; - let outcome = load_skills_for_test(&cfg).await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - let root_path = normalized(&root_skill_path); - let nested_path = normalized(&nested_skill_path); - let (first_path, second_path, first_description, second_description) = - if root_path <= nested_path { - (root_path, nested_path, "from root", "from nested") - } else { - (nested_path, root_path, "from nested", "from root") - }; - assert_eq!( - outcome.skills, - vec![ - SkillMetadata { - name: "dupe-skill".to_string(), - description: first_description.to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: first_path, - scope: SkillScope::Repo, - }, - SkillMetadata { - name: "dupe-skill".to_string(), - description: second_description.to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: second_path, - scope: SkillScope::Repo, - }, - ] - ); -} - -#[tokio::test] -async fn repo_skills_search_does_not_escape_repo_root() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let outer_dir = tempfile::tempdir().expect("tempdir"); - let repo_dir = outer_dir.path().join("repo"); - fs::create_dir_all(&repo_dir).unwrap(); - - let _skill_path = write_skill_at( - &outer_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "outer", - "outer-skill", - "from outer", - ); - mark_as_git_repo(&repo_dir); - - let cfg = make_config_for_cwd(&codex_home, repo_dir).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 0); -} - -#[tokio::test] -async fn loads_skills_when_cwd_is_file_in_repo() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let repo_dir = tempfile::tempdir().expect("tempdir"); - mark_as_git_repo(repo_dir.path()); - - let skill_path = write_skill_at( - &repo_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "repo", - "repo-skill", - "from repo", - ); - let file_path = repo_dir.path().join("some-file.txt"); - fs::write(&file_path, "contents").unwrap(); - - let cfg = make_config_for_cwd(&codex_home, file_path).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "repo-skill".to_string(), - description: "from repo".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::Repo, - }] - ); -} - -#[tokio::test] -async fn non_git_repo_skills_search_does_not_walk_parents() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let outer_dir = tempfile::tempdir().expect("tempdir"); - let nested_dir = outer_dir.path().join("nested/inner"); - fs::create_dir_all(&nested_dir).unwrap(); - - write_skill_at( - &outer_dir - .path() - .join(REPO_ROOT_CONFIG_DIR_NAME) - .join(SKILLS_DIR_NAME), - "outer", - "outer-skill", - "from outer", - ); - - let cfg = make_config_for_cwd(&codex_home, nested_dir).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!(outcome.skills.len(), 0); -} - -#[tokio::test] -async fn loads_skills_from_system_cache_when_present() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let work_dir = tempfile::tempdir().expect("tempdir"); - - let skill_path = write_system_skill(&codex_home, "system", "system-skill", "from system"); - - let cfg = make_config_for_cwd(&codex_home, work_dir.path().to_path_buf()).await; - - let outcome = load_skills_for_test(&cfg).await; - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - assert_eq!( - outcome.skills, - vec![SkillMetadata { - name: "system-skill".to_string(), - description: "from system".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: normalized(&skill_path), - scope: SkillScope::System, - }] - ); -} - -#[tokio::test] -async fn skill_roots_include_admin_with_lowest_priority() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cfg = make_config(&codex_home).await; - - let scopes: Vec = super::skill_roots( - Some(Arc::clone(&LOCAL_FS)), - &cfg.config_layer_stack, - &cfg.cwd, - Vec::new(), - ) - .await - .into_iter() - .map(|root| root.scope) - .collect(); - let mut expected = vec![SkillScope::User, SkillScope::System]; - if home_dir().is_some() { - expected.insert(1, SkillScope::User); - } - expected.push(SkillScope::Admin); - assert_eq!(scopes, expected); -} diff --git a/vendor/codex/codex-rs/core-skills/src/manager.rs b/vendor/codex/codex-rs/core-skills/src/manager.rs deleted file mode 100644 index 13f11fd8..00000000 --- a/vendor/codex/codex-rs/core-skills/src/manager.rs +++ /dev/null @@ -1,310 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::sync::Arc; -use std::sync::RwLock; - -use codex_config::ConfigLayerStack; -use codex_exec_server::ExecutorFileSystem; -use codex_protocol::protocol::Product; -use codex_protocol::protocol::SkillScope; -use codex_utils_absolute_path::AbsolutePathBuf; -use tracing::info; -use tracing::warn; - -use crate::SkillLoadOutcome; -use crate::build_implicit_skill_path_indexes; -use crate::config_rules::SkillConfigRules; -use crate::config_rules::resolve_disabled_skill_paths; -use crate::config_rules::skill_config_rules_from_stack; -use crate::loader::SkillRoot; -use crate::loader::load_skills_from_roots; -use crate::loader::skill_roots; -use crate::system::uninstall_system_skills; -use codex_config::SkillsConfig; - -#[derive(Debug, Clone)] -pub struct SkillsLoadInput { - pub cwd: AbsolutePathBuf, - pub effective_skill_roots: Vec, - pub config_layer_stack: ConfigLayerStack, - pub bundled_skills_enabled: bool, -} - -impl SkillsLoadInput { - pub fn new( - cwd: AbsolutePathBuf, - effective_skill_roots: Vec, - config_layer_stack: ConfigLayerStack, - bundled_skills_enabled: bool, - ) -> Self { - Self { - cwd, - effective_skill_roots, - config_layer_stack, - bundled_skills_enabled, - } - } -} - -pub struct SkillsManager { - codex_home: AbsolutePathBuf, - restriction_product: Option, - cache_by_cwd: RwLock>, - cache_by_config: RwLock>, -} - -impl SkillsManager { - pub fn new(codex_home: AbsolutePathBuf, bundled_skills_enabled: bool) -> Self { - Self::new_with_restriction_product(codex_home, bundled_skills_enabled, Some(Product::Codex)) - } - - pub fn new_with_restriction_product( - codex_home: AbsolutePathBuf, - bundled_skills_enabled: bool, - restriction_product: Option, - ) -> Self { - let manager = Self { - codex_home, - restriction_product, - cache_by_cwd: RwLock::new(HashMap::new()), - cache_by_config: RwLock::new(HashMap::new()), - }; - if !bundled_skills_enabled { - // The loader caches bundled skills under `skills/.system`. Clearing that directory is - // best-effort cleanup; root selection still enforces the config even if removal fails. - uninstall_system_skills(&manager.codex_home); - } - manager - } - - /// Load skills for an already-constructed [`Config`], avoiding any additional config-layer - /// loading. - /// - /// This path uses a cache keyed by the effective skill-relevant config state rather than just - /// cwd so role-local and session-local skill overrides cannot bleed across sessions that happen - /// to share a directory. - pub async fn skills_for_config( - &self, - input: &SkillsLoadInput, - fs: Option>, - ) -> SkillLoadOutcome { - let roots = self.skill_roots_for_config(input, fs).await; - let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack); - let cache_key = config_skills_cache_key(&roots, &skill_config_rules); - if let Some(outcome) = self.cached_outcome_for_config(&cache_key) { - return outcome; - } - - let outcome = self.build_skill_outcome(roots, &skill_config_rules).await; - let mut cache = self - .cache_by_config - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - cache.insert(cache_key, outcome.clone()); - outcome - } - - pub async fn skill_roots_for_config( - &self, - input: &SkillsLoadInput, - fs: Option>, - ) -> Vec { - let mut roots = skill_roots( - fs, - &input.config_layer_stack, - &input.cwd, - input.effective_skill_roots.clone(), - ) - .await; - if !input.bundled_skills_enabled { - roots.retain(|root| root.scope != SkillScope::System); - } - roots - } - - pub async fn skills_for_cwd( - &self, - input: &SkillsLoadInput, - force_reload: bool, - fs: Option>, - ) -> SkillLoadOutcome { - self.skills_for_cwd_with_extra_user_roots(input, force_reload, &[], fs) - .await - } - - pub async fn skills_for_cwd_with_extra_user_roots( - &self, - input: &SkillsLoadInput, - force_reload: bool, - extra_user_roots: &[AbsolutePathBuf], - fs: Option>, - ) -> SkillLoadOutcome { - let use_cwd_cache = fs.is_some(); - if use_cwd_cache - && !force_reload - && let Some(outcome) = self.cached_outcome_for_cwd(&input.cwd) - { - return outcome; - } - - let mut roots = skill_roots( - fs.clone(), - &input.config_layer_stack, - &input.cwd, - input.effective_skill_roots.clone(), - ) - .await; - if !bundled_skills_enabled_from_stack(&input.config_layer_stack) { - roots.retain(|root| root.scope != SkillScope::System); - } - if let Some(fs) = fs { - roots.extend( - normalize_extra_user_roots(extra_user_roots) - .into_iter() - .map(|path| SkillRoot { - path, - scope: SkillScope::User, - file_system: Arc::clone(&fs), - }), - ); - } - let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack); - let outcome = self.build_skill_outcome(roots, &skill_config_rules).await; - if use_cwd_cache { - let mut cache = self - .cache_by_cwd - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - cache.insert(input.cwd.clone(), outcome.clone()); - } - outcome - } - - async fn build_skill_outcome( - &self, - roots: Vec, - skill_config_rules: &SkillConfigRules, - ) -> SkillLoadOutcome { - let outcome = crate::filter_skill_load_outcome_for_product( - load_skills_from_roots(roots).await, - self.restriction_product, - ); - let disabled_paths = resolve_disabled_skill_paths(&outcome.skills, skill_config_rules); - finalize_skill_outcome(outcome, disabled_paths) - } - - pub fn clear_cache(&self) { - let cleared_cwd = { - let mut cache = self - .cache_by_cwd - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let cleared = cache.len(); - cache.clear(); - cleared - }; - let cleared_config = { - let mut cache = self - .cache_by_config - .write() - .unwrap_or_else(std::sync::PoisonError::into_inner); - let cleared = cache.len(); - cache.clear(); - cleared - }; - let cleared = cleared_cwd + cleared_config; - info!("skills cache cleared ({cleared} entries)"); - } - - fn cached_outcome_for_cwd(&self, cwd: &AbsolutePathBuf) -> Option { - match self.cache_by_cwd.read() { - Ok(cache) => cache.get(cwd).cloned(), - Err(err) => err.into_inner().get(cwd).cloned(), - } - } - - fn cached_outcome_for_config( - &self, - cache_key: &ConfigSkillsCacheKey, - ) -> Option { - match self.cache_by_config.read() { - Ok(cache) => cache.get(cache_key).cloned(), - Err(err) => err.into_inner().get(cache_key).cloned(), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -struct ConfigSkillsCacheKey { - roots: Vec<(AbsolutePathBuf, u8)>, - skill_config_rules: SkillConfigRules, -} - -pub fn bundled_skills_enabled_from_stack( - config_layer_stack: &codex_config::ConfigLayerStack, -) -> bool { - let effective_config = config_layer_stack.effective_config(); - let Some(skills_value) = effective_config - .as_table() - .and_then(|table| table.get("skills")) - else { - return true; - }; - - let skills: SkillsConfig = match skills_value.clone().try_into() { - Ok(skills) => skills, - Err(err) => { - warn!("invalid skills config: {err}"); - return true; - } - }; - - skills.bundled.unwrap_or_default().enabled -} - -fn config_skills_cache_key( - roots: &[SkillRoot], - skill_config_rules: &SkillConfigRules, -) -> ConfigSkillsCacheKey { - ConfigSkillsCacheKey { - roots: roots - .iter() - .map(|root| { - let scope_rank = match root.scope { - SkillScope::Repo => 0, - SkillScope::User => 1, - SkillScope::System => 2, - SkillScope::Admin => 3, - }; - (root.path.clone(), scope_rank) - }) - .collect(), - skill_config_rules: skill_config_rules.clone(), - } -} - -fn finalize_skill_outcome( - mut outcome: SkillLoadOutcome, - disabled_paths: HashSet, -) -> SkillLoadOutcome { - outcome.disabled_paths = disabled_paths; - let (by_scripts_dir, by_doc_path) = - build_implicit_skill_path_indexes(outcome.allowed_skills_for_implicit_invocation()); - outcome.implicit_skills_by_scripts_dir = Arc::new(by_scripts_dir); - outcome.implicit_skills_by_doc_path = Arc::new(by_doc_path); - outcome -} - -fn normalize_extra_user_roots(extra_user_roots: &[AbsolutePathBuf]) -> Vec { - let mut normalized: Vec = extra_user_roots - .iter() - .map(|root| root.canonicalize().unwrap_or_else(|_| root.clone())) - .collect(); - normalized.sort_unstable(); - normalized.dedup(); - normalized -} - -#[cfg(test)] -#[path = "manager_tests.rs"] -mod tests; diff --git a/vendor/codex/codex-rs/core-skills/src/manager_tests.rs b/vendor/codex/codex-rs/core-skills/src/manager_tests.rs deleted file mode 100644 index c95168f6..00000000 --- a/vendor/codex/codex-rs/core-skills/src/manager_tests.rs +++ /dev/null @@ -1,750 +0,0 @@ -use super::*; -use crate::SkillMetadata; -use crate::config_rules::resolve_disabled_skill_paths; -use crate::config_rules::skill_config_rules_from_stack; -use codex_app_server_protocol::ConfigLayerSource; -use codex_config::CONFIG_TOML_FILE; -use codex_config::ConfigLayerEntry; -use codex_config::ConfigLayerStack; -use codex_config::ConfigRequirementsToml; -use codex_exec_server::LOCAL_FS; -use codex_utils_absolute_path::AbsolutePathBuf; -use codex_utils_absolute_path::test_support::PathBufExt; -use codex_utils_absolute_path::test_support::PathExt; -use codex_utils_absolute_path::test_support::test_path_buf; -use pretty_assertions::assert_eq; -use std::collections::HashSet; -use std::fs; -use std::path::PathBuf; -use std::sync::Arc; -use tempfile::TempDir; - -fn write_user_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) { - let skill_dir = codex_home.path().join("skills").join(dir); - fs::create_dir_all(&skill_dir).unwrap(); - let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); - fs::write(skill_dir.join("SKILL.md"), content).unwrap(); -} - -fn write_extra_root_skill( - root: &TempDir, - dir: &str, - name: &str, - description: &str, -) -> PathBuf { - let skill_dir = root.path().join("skills").join(dir); - fs::create_dir_all(&skill_dir).unwrap(); - let content = format!("---\nname: {name}\ndescription: {description}\n---\n\n# Body\n"); - let skill_path = skill_dir.join("SKILL.md"); - fs::write(&skill_path, content).unwrap(); - skill_path -} - -fn test_skill(name: &str, path: PathBuf) -> SkillMetadata { - SkillMetadata { - name: name.to_string(), - description: "test".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: path - .abs() - .canonicalize() - .expect("skill path should canonicalize"), - scope: SkillScope::User, - } -} - -fn write_demo_skill(tempdir: &TempDir) -> PathBuf { - let skill_path = tempdir.path().join("skills").join("demo").join("SKILL.md"); - fs::create_dir_all(skill_path.parent().expect("skill path should have parent")) - .expect("create skill dir"); - fs::write( - &skill_path, - "---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n", - ) - .expect("write skill"); - skill_path -} - -fn user_config_layer(codex_home: &TempDir, config_toml: &str) -> ConfigLayerEntry { - let config_path = AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)) - .expect("user config path should be absolute"); - ConfigLayerEntry::new( - ConfigLayerSource::User { file: config_path }, - toml::from_str(config_toml).expect("user layer toml"), - ) -} - -fn config_stack(codex_home: &TempDir, user_config_toml: &str) -> ConfigLayerStack { - ConfigLayerStack::new( - vec![user_config_layer(codex_home, user_config_toml)], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack") -} - -fn config_stack_with_session_flags( - codex_home: &TempDir, - user_config_toml: &str, - session_flags_toml: &str, -) -> ConfigLayerStack { - ConfigLayerStack::new( - vec![ - user_config_layer(codex_home, user_config_toml), - ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::from_str(session_flags_toml).expect("session layer toml"), - ), - ], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack") -} - -fn path_toggle_config(path: &std::path::Path, enabled: bool) -> String { - format!( - r#"[[skills.config]] -path = "{}" -enabled = {enabled} -"#, - path.display() - ) -} - -fn name_toggle_config(name: &str, enabled: bool) -> String { - format!( - r#"[[skills.config]] -name = "{name}" -enabled = {enabled} -"# - ) -} - -async fn skills_for_config_with_stack( - skills_manager: &SkillsManager, - cwd: &TempDir, - config_layer_stack: &ConfigLayerStack, - effective_skill_roots: &[AbsolutePathBuf], -) -> SkillLoadOutcome { - let skills_input = SkillsLoadInput::new( - cwd.path().abs(), - effective_skill_roots.to_vec(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(config_layer_stack), - ); - skills_manager - .skills_for_config(&skills_input, Some(Arc::clone(&LOCAL_FS))) - .await -} - -#[test] -fn new_with_disabled_bundled_skills_removes_stale_cached_system_skills() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let stale_system_skill_dir = codex_home.path().join("skills/.system/stale-skill"); - fs::create_dir_all(&stale_system_skill_dir).expect("create stale system skill dir"); - fs::write(stale_system_skill_dir.join("SKILL.md"), "# stale\n") - .expect("write stale system skill"); - - let _skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ false, - ); - - assert!( - !codex_home.path().join("skills/.system").exists(), - "expected disabling system skills to remove stale cached bundled skills" - ); -} - -#[tokio::test] -async fn skills_for_config_reuses_cache_for_same_effective_config() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let config_layer_stack = config_stack(&codex_home, ""); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - - write_user_skill(&codex_home, "a", "skill-a", "from a"); - let outcome1 = - skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; - assert!( - outcome1.skills.iter().any(|s| s.name == "skill-a"), - "expected skill-a to be discovered" - ); - - // Write a new skill after the first call; the second call should reuse the config-aware cache - // entry because the effective skill config is unchanged. - write_user_skill(&codex_home, "b", "skill-b", "from b"); - let outcome2 = - skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; - assert_eq!(outcome2.errors, outcome1.errors); - assert_eq!(outcome2.skills, outcome1.skills); -} - -#[tokio::test] -async fn skills_for_config_disables_extra_root_skills_by_name() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let extra_root = tempfile::tempdir().expect("tempdir"); - let skill_path = write_extra_root_skill( - &extra_root, - "sample-search", - "sample-search", - "search sample data", - ); - let config_layer_stack = config_stack( - &codex_home, - &name_toggle_config("sample-search", /*enabled*/ false), - ); - let extra_skill_root = skill_path - .parent() - .and_then(std::path::Path::parent) - .expect("skill should live under a skills root") - .abs(); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - - let outcome = skills_for_config_with_stack( - &skills_manager, - &cwd, - &config_layer_stack, - &[extra_skill_root], - ) - .await; - let skill = outcome - .skills - .iter() - .find(|skill| skill.name == "sample-search") - .expect("extra root skill should load"); - let skill_path = dunce::canonicalize(skill_path) - .expect("skill path should canonicalize") - .abs(); - - assert_eq!(skill.path_to_skills_md, skill_path); - assert!(outcome.disabled_paths.contains(&skill.path_to_skills_md)); - assert!( - !outcome - .allowed_skills_for_implicit_invocation() - .iter() - .any(|allowed_skill| allowed_skill.path_to_skills_md == skill.path_to_skills_md) - ); -} - -#[tokio::test] -async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let extra_root = tempfile::tempdir().expect("tempdir"); - let config_layer_stack = config_stack(&codex_home, ""); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; - - write_user_skill(&extra_root, "x", "extra-skill", "from extra root"); - let extra_root_path = extra_root.path().abs(); - let base_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let outcome_with_extra = skills_manager - .skills_for_cwd_with_extra_user_roots( - &base_input, - /*force_reload*/ true, - std::slice::from_ref(&extra_root_path), - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - outcome_with_extra - .skills - .iter() - .any(|skill| skill.name == "extra-skill") - ); - - // The cwd-only API returns the current cached entry for this cwd, even when that entry - // was produced with extra roots. - let base_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let outcome_without_extra = skills_manager - .skills_for_cwd( - &base_input, - /*force_reload*/ false, - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert_eq!(outcome_without_extra.skills, outcome_with_extra.skills); - assert_eq!(outcome_without_extra.errors, outcome_with_extra.errors); -} - -#[tokio::test] -async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let extra_root = tempfile::tempdir().expect("tempdir"); - let repo_dot_codex = cwd.path().join(".codex"); - fs::create_dir_all(&repo_dot_codex).expect("create repo config dir"); - - write_user_skill(&codex_home, "user", "user-skill", "from local user root"); - write_user_skill(&extra_root, "extra", "extra-skill", "from extra root"); - let repo_skill_dir = repo_dot_codex.join("skills/repo"); - fs::create_dir_all(&repo_skill_dir).expect("create repo skill dir"); - fs::write( - repo_skill_dir.join("SKILL.md"), - "---\nname: repo-skill\ndescription: from repo root\n---\n\n# Body\n", - ) - .expect("write repo skill"); - - let config_layer_stack = ConfigLayerStack::new( - vec![ - user_config_layer(&codex_home, ""), - ConfigLayerEntry::new( - ConfigLayerSource::Project { - dot_codex_folder: repo_dot_codex.abs(), - }, - toml::Value::Table(toml::map::Map::new()), - ), - ], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - let skills_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - - let outcome = skills_manager - .skills_for_cwd_with_extra_user_roots( - &skills_input, - /*force_reload*/ true, - &[extra_root.path().abs()], - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - let loaded_names = outcome - .skills - .iter() - .map(|skill| skill.name.as_str()) - .collect::>(); - assert!(loaded_names.contains("user-skill")); - assert!(loaded_names.contains("repo-skill")); - assert!(loaded_names.contains("extra-skill")); -} - -#[tokio::test] -async fn skills_for_cwd_without_fs_skips_repo_and_extra_roots() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let extra_root = tempfile::tempdir().expect("tempdir"); - let repo_dot_codex = cwd.path().join(".codex"); - fs::create_dir_all(&repo_dot_codex).expect("create repo config dir"); - - write_user_skill(&codex_home, "user", "user-skill", "from local user root"); - write_user_skill(&extra_root, "extra", "extra-skill", "from extra root"); - let repo_skill_dir = repo_dot_codex.join("skills/repo"); - fs::create_dir_all(&repo_skill_dir).expect("create repo skill dir"); - fs::write( - repo_skill_dir.join("SKILL.md"), - "---\nname: repo-skill\ndescription: from repo root\n---\n\n# Body\n", - ) - .expect("write repo skill"); - - let config_layer_stack = ConfigLayerStack::new( - vec![ - user_config_layer(&codex_home, ""), - ConfigLayerEntry::new( - ConfigLayerSource::Project { - dot_codex_folder: repo_dot_codex.abs(), - }, - toml::Value::Table(toml::map::Map::new()), - ), - ], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - let skills_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - - let outcome = skills_manager - .skills_for_cwd_with_extra_user_roots( - &skills_input, - /*force_reload*/ true, - &[extra_root.path().abs()], - /*fs*/ None, - ) - .await; - - assert!( - outcome.errors.is_empty(), - "unexpected errors: {:?}", - outcome.errors - ); - let loaded_names = outcome - .skills - .iter() - .map(|skill| skill.name.as_str()) - .collect::>(); - assert!(loaded_names.contains("user-skill")); - assert!(!loaded_names.contains("repo-skill")); - assert!(!loaded_names.contains("extra-skill")); -} - -#[tokio::test] -async fn skills_for_config_excludes_bundled_skills_when_disabled_in_config() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let bundled_skill_dir = codex_home.path().join("skills/.system/bundled-skill"); - fs::create_dir_all(&bundled_skill_dir).expect("create bundled skill dir"); - fs::write( - bundled_skill_dir.join("SKILL.md"), - "---\nname: bundled-skill\ndescription: from bundled root\n---\n\n# Body\n", - ) - .expect("write bundled skill"); - let config_layer_stack = config_stack(&codex_home, "[skills.bundled]\nenabled = false\n"); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ false, - ); - - // Recreate the cached bundled skill after startup cleanup so this assertion exercises - // root selection rather than relying on directory removal succeeding. - fs::create_dir_all(&bundled_skill_dir).expect("recreate bundled skill dir"); - fs::write( - bundled_skill_dir.join("SKILL.md"), - "---\nname: bundled-skill\ndescription: from bundled root\n---\n\n# Body\n", - ) - .expect("rewrite bundled skill"); - - let outcome = - skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; - assert!( - outcome - .skills - .iter() - .all(|skill| skill.name != "bundled-skill") - ); - assert!( - outcome - .skills - .iter() - .all(|skill| skill.scope != SkillScope::System) - ); -} - -#[tokio::test] -async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let extra_root_a = tempfile::tempdir().expect("tempdir"); - let extra_root_b = tempfile::tempdir().expect("tempdir"); - let config_layer_stack = config_stack(&codex_home, ""); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; - - write_user_skill(&extra_root_a, "x", "extra-skill-a", "from extra root a"); - write_user_skill(&extra_root_b, "x", "extra-skill-b", "from extra root b"); - - let extra_root_a_path = extra_root_a.path().abs(); - let base_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - config_layer_stack.clone(), - bundled_skills_enabled_from_stack(&config_layer_stack), - ); - let outcome_a = skills_manager - .skills_for_cwd_with_extra_user_roots( - &base_input, - /*force_reload*/ true, - std::slice::from_ref(&extra_root_a_path), - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - outcome_a - .skills - .iter() - .any(|skill| skill.name == "extra-skill-a") - ); - assert!( - outcome_a - .skills - .iter() - .all(|skill| skill.name != "extra-skill-b") - ); - - let extra_root_b_path = extra_root_b.path().abs(); - let outcome_b = skills_manager - .skills_for_cwd_with_extra_user_roots( - &base_input, - /*force_reload*/ false, - std::slice::from_ref(&extra_root_b_path), - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - outcome_b - .skills - .iter() - .any(|skill| skill.name == "extra-skill-a") - ); - assert!( - outcome_b - .skills - .iter() - .all(|skill| skill.name != "extra-skill-b") - ); - - let outcome_reloaded = skills_manager - .skills_for_cwd_with_extra_user_roots( - &base_input, - /*force_reload*/ true, - std::slice::from_ref(&extra_root_b_path), - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - assert!( - outcome_reloaded - .skills - .iter() - .any(|skill| skill.name == "extra-skill-b") - ); - assert!( - outcome_reloaded - .skills - .iter() - .all(|skill| skill.name != "extra-skill-a") - ); -} - -#[test] -fn normalize_extra_user_roots_is_stable_for_equivalent_inputs() { - let a = test_path_buf("/tmp/a").abs(); - let b = test_path_buf("/tmp/b").abs(); - - let first = normalize_extra_user_roots(&[a.clone(), b.clone(), a.clone()]); - let second = normalize_extra_user_roots(&[b, a]); - - assert_eq!(first, second); -} - -#[cfg_attr(windows, ignore)] -#[test] -fn disabled_paths_for_skills_allows_session_flags_to_override_user_layer() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let skill_path = write_demo_skill(&tempdir); - let skill = test_skill("demo-skill", skill_path.clone()); - let user_file = AbsolutePathBuf::try_from(tempdir.path().join("config.toml")) - .expect("user config path should be absolute"); - let user_layer = ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false)) - .expect("user layer toml"), - ); - let session_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ true)) - .expect("session layer toml"), - ); - let stack = ConfigLayerStack::new( - vec![user_layer, session_layer], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - - let skill_config_rules = skill_config_rules_from_stack(&stack); - assert_eq!( - resolve_disabled_skill_paths(&[skill], &skill_config_rules), - HashSet::new() - ); -} - -#[cfg_attr(windows, ignore)] -#[test] -fn disabled_paths_for_skills_allows_session_flags_to_disable_user_enabled_skill() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let skill_path = write_demo_skill(&tempdir); - let skill = test_skill("demo-skill", skill_path.clone()); - let user_file = AbsolutePathBuf::try_from(tempdir.path().join("config.toml")) - .expect("user config path should be absolute"); - let user_layer = ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ true)) - .expect("user layer toml"), - ); - let session_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false)) - .expect("session layer toml"), - ); - let stack = ConfigLayerStack::new( - vec![user_layer, session_layer], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - - let skill_config_rules = skill_config_rules_from_stack(&stack); - assert_eq!( - resolve_disabled_skill_paths(&[skill], &skill_config_rules), - HashSet::from([skill_path - .abs() - .canonicalize() - .expect("skill path should canonicalize")]) - ); -} - -#[cfg_attr(windows, ignore)] -#[test] -fn disabled_paths_for_skills_disables_matching_name_selectors() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let skill_path = write_demo_skill(&tempdir); - let skill = test_skill("github:yeet", skill_path.clone()); - let user_file = AbsolutePathBuf::try_from(tempdir.path().join("config.toml")) - .expect("user config path should be absolute"); - let user_layer = ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - toml::from_str(&name_toggle_config("github:yeet", /*enabled*/ false)) - .expect("user layer toml"), - ); - let stack = ConfigLayerStack::new( - vec![user_layer], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - - let skill_config_rules = skill_config_rules_from_stack(&stack); - assert_eq!( - resolve_disabled_skill_paths(&[skill], &skill_config_rules), - HashSet::from([skill_path - .abs() - .canonicalize() - .expect("skill path should canonicalize")]) - ); -} - -#[cfg_attr(windows, ignore)] -#[test] -fn disabled_paths_for_skills_allows_name_selector_to_override_path_selector() { - let tempdir = tempfile::tempdir().expect("tempdir"); - let skill_path = write_demo_skill(&tempdir); - let skill = test_skill("github:yeet", skill_path.clone()); - let user_file = AbsolutePathBuf::try_from(tempdir.path().join("config.toml")) - .expect("user config path should be absolute"); - let user_layer = ConfigLayerEntry::new( - ConfigLayerSource::User { file: user_file }, - toml::from_str(&path_toggle_config(&skill_path, /*enabled*/ false)) - .expect("user layer toml"), - ); - let session_layer = ConfigLayerEntry::new( - ConfigLayerSource::SessionFlags, - toml::from_str(&name_toggle_config("github:yeet", /*enabled*/ true)) - .expect("session layer toml"), - ); - let stack = ConfigLayerStack::new( - vec![user_layer, session_layer], - Default::default(), - ConfigRequirementsToml::default(), - ) - .expect("valid config layer stack"); - - let skill_config_rules = skill_config_rules_from_stack(&stack); - assert_eq!( - resolve_disabled_skill_paths(&[skill], &skill_config_rules), - HashSet::new() - ); -} - -#[cfg_attr(windows, ignore)] -#[tokio::test] -async fn skills_for_config_ignores_cwd_cache_when_session_flags_reenable_skill() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let cwd = tempfile::tempdir().expect("tempdir"); - let skill_dir = codex_home.path().join("skills").join("demo"); - fs::create_dir_all(&skill_dir).expect("create skill dir"); - let skill_path = skill_dir.join("SKILL.md"); - fs::write( - &skill_path, - "---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n", - ) - .expect("write skill"); - let disabled_skill_config = path_toggle_config(&skill_path, /*enabled*/ false); - let enabled_skill_config = path_toggle_config(&skill_path, /*enabled*/ true); - let parent_stack = config_stack(&codex_home, &disabled_skill_config); - let child_stack = - config_stack_with_session_flags(&codex_home, &disabled_skill_config, &enabled_skill_config); - let skills_manager = SkillsManager::new( - codex_home.path().abs(), - /*bundled_skills_enabled*/ true, - ); - let parent_input = SkillsLoadInput::new( - cwd.path().abs(), - Vec::new(), - parent_stack.clone(), - bundled_skills_enabled_from_stack(&parent_stack), - ); - - let parent_outcome = skills_manager - .skills_for_cwd( - &parent_input, - /*force_reload*/ true, - Some(Arc::clone(&LOCAL_FS)), - ) - .await; - let parent_skill = parent_outcome - .skills - .iter() - .find(|skill| skill.name == "demo-skill") - .expect("demo skill should be discovered"); - assert_eq!(parent_outcome.is_skill_enabled(parent_skill), false); - - let child_outcome = - skills_for_config_with_stack(&skills_manager, &cwd, &child_stack, &[]).await; - let child_skill = child_outcome - .skills - .iter() - .find(|skill| skill.name == "demo-skill") - .expect("demo skill should be discovered"); - assert_eq!(child_outcome.is_skill_enabled(child_skill), true); -} diff --git a/vendor/codex/codex-rs/core-skills/src/mention_counts.rs b/vendor/codex/codex-rs/core-skills/src/mention_counts.rs deleted file mode 100644 index b7482ca3..00000000 --- a/vendor/codex/codex-rs/core-skills/src/mention_counts.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; - -use super::SkillMetadata; -use codex_utils_absolute_path::AbsolutePathBuf; - -/// Counts how often each skill name appears (exact and ASCII-lowercase), excluding disabled paths. -pub fn build_skill_name_counts( - skills: &[SkillMetadata], - disabled_paths: &HashSet, -) -> (HashMap, HashMap) { - let mut exact_counts: HashMap = HashMap::new(); - let mut lower_counts: HashMap = HashMap::new(); - for skill in skills { - if disabled_paths.contains(&skill.path_to_skills_md) { - continue; - } - *exact_counts.entry(skill.name.clone()).or_insert(0) += 1; - *lower_counts - .entry(skill.name.to_ascii_lowercase()) - .or_insert(0) += 1; - } - (exact_counts, lower_counts) -} diff --git a/vendor/codex/codex-rs/core-skills/src/model.rs b/vendor/codex/codex-rs/core-skills/src/model.rs deleted file mode 100644 index eb9a6f13..00000000 --- a/vendor/codex/codex-rs/core-skills/src/model.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::fmt; -use std::sync::Arc; - -use codex_exec_server::ExecutorFileSystem; -use codex_protocol::protocol::Product; -use codex_protocol::protocol::SkillScope; -use codex_utils_absolute_path::AbsolutePathBuf; - -#[derive(Debug, Clone, PartialEq)] -pub struct SkillMetadata { - pub name: String, - pub description: String, - pub short_description: Option, - pub interface: Option, - pub dependencies: Option, - pub policy: Option, - /// Path to the SKILLS.md file that declares this skill. - pub path_to_skills_md: AbsolutePathBuf, - pub scope: SkillScope, -} - -impl SkillMetadata { - fn allow_implicit_invocation(&self) -> bool { - self.policy - .as_ref() - .and_then(|policy| policy.allow_implicit_invocation) - .unwrap_or(true) - } - - pub fn matches_product_restriction_for_product( - &self, - restriction_product: Option, - ) -> bool { - match &self.policy { - Some(policy) => { - policy.products.is_empty() - || restriction_product.is_some_and(|product| { - product.matches_product_restriction(&policy.products) - }) - } - None => true, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SkillPolicy { - pub allow_implicit_invocation: Option, - // TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and - // storing this metadata. - pub products: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillInterface { - pub display_name: Option, - pub short_description: Option, - pub icon_small: Option, - pub icon_large: Option, - pub brand_color: Option, - pub default_prompt: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillDependencies { - pub tools: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillToolDependency { - pub r#type: String, - pub value: String, - pub description: Option, - pub transport: Option, - pub command: Option, - pub url: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillError { - pub path: AbsolutePathBuf, - pub message: String, -} - -#[derive(Debug, Clone, Default)] -pub struct SkillLoadOutcome { - pub skills: Vec, - pub errors: Vec, - pub disabled_paths: HashSet, - pub(crate) file_systems_by_skill_path: SkillFileSystemsByPath, - pub(crate) implicit_skills_by_scripts_dir: Arc>, - pub(crate) implicit_skills_by_doc_path: Arc>, -} - -impl SkillLoadOutcome { - pub fn is_skill_enabled(&self, skill: &SkillMetadata) -> bool { - !self.disabled_paths.contains(&skill.path_to_skills_md) - } - - pub fn is_skill_allowed_for_implicit_invocation(&self, skill: &SkillMetadata) -> bool { - self.is_skill_enabled(skill) && skill.allow_implicit_invocation() - } - - pub fn allowed_skills_for_implicit_invocation(&self) -> Vec { - self.skills - .iter() - .filter(|skill| self.is_skill_allowed_for_implicit_invocation(skill)) - .cloned() - .collect() - } - - pub fn skills_with_enabled(&self) -> impl Iterator { - self.skills - .iter() - .map(|skill| (skill, self.is_skill_enabled(skill))) - } - - pub(crate) fn file_system_for_skill( - &self, - skill: &SkillMetadata, - ) -> Option> { - self.file_systems_by_skill_path - .get(&skill.path_to_skills_md) - } -} - -#[derive(Clone, Default)] -pub(crate) struct SkillFileSystemsByPath { - values: Arc>>, -} - -impl SkillFileSystemsByPath { - pub(crate) fn new(values: HashMap>) -> Self { - Self { - values: Arc::new(values), - } - } - - fn get(&self, path: &AbsolutePathBuf) -> Option> { - self.values.get(path).map(Arc::clone) - } - - fn retain_paths(&mut self, paths: &HashSet) { - self.values = Arc::new( - self.values - .iter() - .filter(|(path, _)| paths.contains(*path)) - .map(|(path, fs)| (path.clone(), Arc::clone(fs))) - .collect(), - ); - } -} - -impl fmt::Debug for SkillFileSystemsByPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SkillFileSystemsByPath") - .field("len", &self.values.len()) - .finish() - } -} - -pub fn filter_skill_load_outcome_for_product( - mut outcome: SkillLoadOutcome, - restriction_product: Option, -) -> SkillLoadOutcome { - outcome - .skills - .retain(|skill| skill.matches_product_restriction_for_product(restriction_product)); - let retained_paths: HashSet = outcome - .skills - .iter() - .map(|skill| skill.path_to_skills_md.clone()) - .collect(); - outcome - .file_systems_by_skill_path - .retain_paths(&retained_paths); - outcome.implicit_skills_by_scripts_dir = Arc::new( - outcome - .implicit_skills_by_scripts_dir - .iter() - .filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product)) - .map(|(path, skill)| (path.clone(), skill.clone())) - .collect(), - ); - outcome.implicit_skills_by_doc_path = Arc::new( - outcome - .implicit_skills_by_doc_path - .iter() - .filter(|(_, skill)| skill.matches_product_restriction_for_product(restriction_product)) - .map(|(path, skill)| (path.clone(), skill.clone())) - .collect(), - ); - outcome -} diff --git a/vendor/codex/codex-rs/core-skills/src/remote.rs b/vendor/codex/codex-rs/core-skills/src/remote.rs deleted file mode 100644 index 2dc620b8..00000000 --- a/vendor/codex/codex-rs/core-skills/src/remote.rs +++ /dev/null @@ -1,270 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use serde::Deserialize; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; -use std::time::Duration; - -use codex_login::CodexAuth; -use codex_login::default_client::build_reqwest_client; - -const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30); - -// Low-level client for the remote skill API. This is intentionally kept around for -// future wiring, but it is not used yet by any active product surface. - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RemoteSkillScope { - WorkspaceShared, - AllShared, - Personal, - Example, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RemoteSkillProductSurface { - Chatgpt, - Codex, - Api, - Atlas, -} - -fn as_query_scope(scope: RemoteSkillScope) -> Option<&'static str> { - match scope { - RemoteSkillScope::WorkspaceShared => Some("workspace-shared"), - RemoteSkillScope::AllShared => Some("all-shared"), - RemoteSkillScope::Personal => Some("personal"), - RemoteSkillScope::Example => Some("example"), - } -} - -fn as_query_product_surface(product_surface: RemoteSkillProductSurface) -> &'static str { - match product_surface { - RemoteSkillProductSurface::Chatgpt => "chatgpt", - RemoteSkillProductSurface::Codex => "codex", - RemoteSkillProductSurface::Api => "api", - RemoteSkillProductSurface::Atlas => "atlas", - } -} - -fn ensure_chatgpt_auth(auth: Option<&CodexAuth>) -> Result<&CodexAuth> { - let Some(auth) = auth else { - anyhow::bail!("chatgpt authentication required for remote skill scopes"); - }; - if !auth.is_chatgpt_auth() { - anyhow::bail!( - "chatgpt authentication required for remote skill scopes; api key auth is not supported" - ); - } - Ok(auth) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillSummary { - pub id: String, - pub name: String, - pub description: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct RemoteSkillDownloadResult { - pub id: String, - pub path: PathBuf, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkillsResponse { - #[serde(rename = "hazelnuts")] - skills: Vec, -} - -#[derive(Debug, Deserialize)] -struct RemoteSkill { - id: String, - name: String, - description: String, -} - -pub async fn list_remote_skills( - chatgpt_base_url: String, - auth: Option<&CodexAuth>, - scope: RemoteSkillScope, - product_surface: RemoteSkillProductSurface, - enabled: Option, -) -> Result> { - let base_url = chatgpt_base_url.trim_end_matches('/'); - let auth = ensure_chatgpt_auth(auth)?; - - let url = format!("{base_url}/hazelnuts"); - let product_surface = as_query_product_surface(product_surface); - let mut query_params = vec![("product_surface", product_surface)]; - if let Some(scope) = as_query_scope(scope) { - query_params.push(("scope", scope)); - } - if let Some(enabled) = enabled { - let enabled = if enabled { "true" } else { "false" }; - query_params.push(("enabled", enabled)); - } - - let client = build_reqwest_client(); - let mut request = client - .get(&url) - .timeout(REMOTE_SKILLS_API_TIMEOUT) - .query(&query_params); - let token = auth - .get_token() - .context("Failed to read auth token for remote skills")?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - let response = request - .send() - .await - .with_context(|| format!("Failed to send request to {url}"))?; - - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - if !status.is_success() { - anyhow::bail!("Request failed with status {status} from {url}: {body}"); - } - - let parsed: RemoteSkillsResponse = - serde_json::from_str(&body).context("Failed to parse skills response")?; - - Ok(parsed - .skills - .into_iter() - .map(|skill| RemoteSkillSummary { - id: skill.id, - name: skill.name, - description: skill.description, - }) - .collect()) -} - -pub async fn export_remote_skill( - chatgpt_base_url: String, - codex_home: PathBuf, - auth: Option<&CodexAuth>, - skill_id: &str, -) -> Result { - let auth = ensure_chatgpt_auth(auth)?; - - let client = build_reqwest_client(); - let base_url = chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/hazelnuts/{skill_id}/export"); - let mut request = client.get(&url).timeout(REMOTE_SKILLS_API_TIMEOUT); - - let token = auth - .get_token() - .context("Failed to read auth token for remote skills")?; - request = request.bearer_auth(token); - if let Some(account_id) = auth.get_account_id() { - request = request.header("chatgpt-account-id", account_id); - } - - let response = request - .send() - .await - .with_context(|| format!("Failed to send download request to {url}"))?; - - let status = response.status(); - let body = response.bytes().await.context("Failed to read download")?; - if !status.is_success() { - let body_text = String::from_utf8_lossy(&body); - anyhow::bail!("Download failed with status {status} from {url}: {body_text}"); - } - - if !is_zip_payload(&body) { - anyhow::bail!("Downloaded remote skill payload is not a zip archive"); - } - - let output_dir = codex_home.join("skills").join(skill_id); - tokio::fs::create_dir_all(&output_dir) - .await - .context("Failed to create downloaded skills directory")?; - - let zip_bytes = body.to_vec(); - let output_dir_clone = output_dir.clone(); - let prefix_candidates = vec![skill_id.to_string()]; - tokio::task::spawn_blocking(move || { - extract_zip_to_dir(zip_bytes, &output_dir_clone, &prefix_candidates) - }) - .await - .context("Zip extraction task failed")??; - - Ok(RemoteSkillDownloadResult { - id: skill_id.to_string(), - path: output_dir, - }) -} - -fn safe_join(base: &Path, name: &str) -> Result { - let path = Path::new(name); - for component in path.components() { - match component { - Component::Normal(_) => {} - _ => { - anyhow::bail!("Invalid file path in remote skill payload: {name}"); - } - } - } - Ok(base.join(path)) -} - -fn is_zip_payload(bytes: &[u8]) -> bool { - bytes.starts_with(b"PK\x03\x04") - || bytes.starts_with(b"PK\x05\x06") - || bytes.starts_with(b"PK\x07\x08") -} - -fn extract_zip_to_dir( - bytes: Vec, - output_dir: &Path, - prefix_candidates: &[String], -) -> Result<()> { - let cursor = std::io::Cursor::new(bytes); - let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?; - for i in 0..archive.len() { - let mut file = archive.by_index(i).context("Failed to read zip entry")?; - if file.is_dir() { - continue; - } - let raw_name = file.name().to_string(); - let normalized = normalize_zip_name(&raw_name, prefix_candidates); - let Some(normalized) = normalized else { - continue; - }; - let file_path = safe_join(output_dir, &normalized)?; - if let Some(parent) = file_path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("Failed to create parent dir for {normalized}"))?; - } - let mut out = std::fs::File::create(&file_path) - .with_context(|| format!("Failed to create file {normalized}"))?; - std::io::copy(&mut file, &mut out) - .with_context(|| format!("Failed to write skill file {normalized}"))?; - } - Ok(()) -} - -fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option { - let mut trimmed = name.trim_start_matches("./"); - for prefix in prefix_candidates { - if prefix.is_empty() { - continue; - } - let prefix = format!("{prefix}/"); - if let Some(rest) = trimmed.strip_prefix(&prefix) { - trimmed = rest; - break; - } - } - if trimmed.is_empty() { - None - } else { - Some(trimmed.to_string()) - } -} diff --git a/vendor/codex/codex-rs/core-skills/src/render.rs b/vendor/codex/codex-rs/core-skills/src/render.rs deleted file mode 100644 index add2fcaf..00000000 --- a/vendor/codex/codex-rs/core-skills/src/render.rs +++ /dev/null @@ -1,755 +0,0 @@ -use crate::model::SkillMetadata; -use codex_otel::SessionTelemetry; -use codex_otel::THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC; -use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC; -use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; -use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; -use codex_protocol::protocol::SkillScope; -use codex_utils_output_truncation::approx_token_count; - -const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000; -const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2; -const SKILL_DESCRIPTION_TRUNCATION_WARNING_THRESHOLD_CHARS: usize = 10; -const APPROX_BYTES_PER_TOKEN: usize = 4; -pub const SKILL_DESCRIPTION_TRUNCATED_WARNING_PREFIX: &str = "Warning: Exceeded skills context budget. Loaded skill descriptions were truncated by an average of"; -pub const SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX: &str = - "Warning: Exceeded skills context budget. All skill descriptions were removed and"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SkillMetadataBudget { - Tokens(usize), - Characters(usize), -} - -impl SkillMetadataBudget { - fn limit(self) -> usize { - match self { - Self::Tokens(limit) | Self::Characters(limit) => limit, - } - } - - fn cost(self, text: &str) -> usize { - match self { - Self::Tokens(_) => approx_token_count(text), - Self::Characters(_) => text.chars().count(), - } - } - - fn cost_from_counts(self, chars: usize, bytes: usize) -> usize { - match self { - Self::Tokens(_) => approx_token_count_from_bytes(bytes), - Self::Characters(_) => chars, - } - } -} - -fn approx_token_count_from_bytes(bytes: usize) -> usize { - bytes.saturating_add(APPROX_BYTES_PER_TOKEN.saturating_sub(1)) / APPROX_BYTES_PER_TOKEN -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillRenderReport { - pub total_count: usize, - pub included_count: usize, - pub omitted_count: usize, - pub truncated_description_chars: usize, - pub truncated_description_count: usize, -} - -#[derive(Clone, Copy)] -pub enum SkillRenderSideEffects<'a> { - None, - ThreadStart { - session_telemetry: &'a SessionTelemetry, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AvailableSkills { - pub skill_lines: Vec, - pub report: SkillRenderReport, - pub warning_message: Option, -} - -pub fn default_skill_metadata_budget(context_window: Option) -> SkillMetadataBudget { - context_window - .and_then(|window| usize::try_from(window).ok()) - .filter(|window| *window > 0) - .map(|window| { - SkillMetadataBudget::Tokens( - window - .saturating_mul(SKILL_METADATA_CONTEXT_WINDOW_PERCENT) - .saturating_div(100) - .max(1), - ) - }) - .unwrap_or(SkillMetadataBudget::Characters( - DEFAULT_SKILL_METADATA_CHAR_BUDGET, - )) -} - -pub fn build_available_skills( - skills: &[SkillMetadata], - budget: SkillMetadataBudget, - side_effects: SkillRenderSideEffects<'_>, -) -> Option { - if skills.is_empty() { - record_skill_render_side_effects( - side_effects, - /*total_count*/ 0, - /*included_count*/ 0, - /*omitted_count*/ 0, - /*truncated_description_chars*/ 0, - ); - return None; - } - - let (skill_lines, report) = render_skill_lines(skills, budget); - let warning_message = if report.omitted_count > 0 { - let skill_word = if report.omitted_count == 1 { - "skill" - } else { - "skills" - }; - let verb = if report.omitted_count == 1 { - "was" - } else { - "were" - }; - Some(format!( - "{} {} additional {} {} not included in the model-visible skills list.", - budget_warning_prefix(budget, SKILL_DESCRIPTIONS_REMOVED_WARNING_PREFIX), - report.omitted_count, - skill_word, - verb - )) - } else if report.average_truncated_description_chars() - > SKILL_DESCRIPTION_TRUNCATION_WARNING_THRESHOLD_CHARS - { - Some(format!( - "{} {} characters per skill.", - budget_warning_prefix(budget, SKILL_DESCRIPTION_TRUNCATED_WARNING_PREFIX), - report.average_truncated_description_chars() - )) - } else { - None - }; - record_skill_render_side_effects( - side_effects, - report.total_count, - report.included_count, - report.omitted_count, - report.truncated_description_chars, - ); - if report.omitted_count > 0 || report.truncated_description_chars > 0 { - tracing::info!( - budget_limit = budget.limit(), - total_skills = report.total_count, - included_skills = report.included_count, - omitted_skills = report.omitted_count, - truncated_description_chars_per_skill = report.average_truncated_description_chars(), - truncated_skill_descriptions = report.truncated_description_count, - "truncated skill metadata to fit skills context budget" - ); - } - Some(AvailableSkills { - skill_lines, - report, - warning_message, - }) -} - -fn budget_warning_prefix(budget: SkillMetadataBudget, prefix: &str) -> String { - match budget { - SkillMetadataBudget::Tokens(_) => prefix.replacen( - "Exceeded skills context budget.", - "Exceeded skills context budget of 2%.", - 1, - ), - SkillMetadataBudget::Characters(_) => prefix.to_string(), - } -} - -fn record_skill_render_side_effects( - side_effects: SkillRenderSideEffects<'_>, - total_count: usize, - included_count: usize, - omitted_count: usize, - truncated_description_chars: usize, -) { - match side_effects { - SkillRenderSideEffects::None => {} - SkillRenderSideEffects::ThreadStart { session_telemetry } => { - session_telemetry.histogram( - THREAD_SKILLS_ENABLED_TOTAL_METRIC, - i64::try_from(total_count).unwrap_or(i64::MAX), - &[], - ); - session_telemetry.histogram( - THREAD_SKILLS_KEPT_TOTAL_METRIC, - i64::try_from(included_count).unwrap_or(i64::MAX), - &[], - ); - session_telemetry.histogram( - THREAD_SKILLS_TRUNCATED_METRIC, - if omitted_count > 0 { 1 } else { 0 }, - &[], - ); - session_telemetry.histogram( - THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC, - i64::try_from(truncated_description_chars).unwrap_or(i64::MAX), - &[], - ); - } - } -} - -fn render_skill_lines( - skills: &[SkillMetadata], - budget: SkillMetadataBudget, -) -> (Vec, SkillRenderReport) { - let ordered_skills = ordered_skills_for_budget(skills); - let skill_lines = ordered_skills - .into_iter() - .map(SkillLine::new) - .collect::>(); - - let full_cost = skill_lines.iter().fold(0usize, |used, line| { - used.saturating_add(line.full_cost(budget)) - }); - if full_cost <= budget.limit() { - let included = skill_lines - .iter() - .map(SkillLine::render_full) - .collect::>(); - - return ( - included, - skill_render_report( - /*total_count*/ skills.len(), - /*included_count*/ skill_lines.len(), - /*omitted_count*/ 0, - /*truncated_description_chars*/ 0, - /*truncated_description_count*/ 0, - ), - ); - } - - let minimum_cost = skill_lines.iter().fold(0usize, |used, line| { - used.saturating_add(line.minimum_cost(budget)) - }); - if minimum_cost <= budget.limit() { - let rendered = render_lines_with_description_budget( - budget, - &skill_lines, - budget.limit().saturating_sub(minimum_cost), - ); - let (truncated_description_chars, truncated_description_count) = - sum_description_truncation(&rendered); - let included = rendered - .into_iter() - .map(|rendered| rendered.line) - .collect::>(); - - return ( - included, - skill_render_report( - /*total_count*/ skills.len(), - /*included_count*/ skill_lines.len(), - /*omitted_count*/ 0, - truncated_description_chars, - truncated_description_count, - ), - ); - } - - render_minimum_skill_lines_until_budget(budget, skill_lines, skills.len()) -} - -fn render_minimum_skill_lines_until_budget( - budget: SkillMetadataBudget, - skill_lines: Vec>, - total_count: usize, -) -> (Vec, SkillRenderReport) { - let mut included = Vec::new(); - let mut used = 0usize; - let mut omitted_count = 0usize; - let mut truncated_description_chars = 0usize; - let mut truncated_description_count = 0usize; - for line in skill_lines { - let line_cost = line.minimum_cost(budget); - let description_char_count = line.description_char_count(); - if used.saturating_add(line_cost) <= budget.limit() { - used = used.saturating_add(line_cost); - included.push(line.render_minimum()); - } else { - omitted_count = omitted_count.saturating_add(1); - } - - truncated_description_chars = - truncated_description_chars.saturating_add(description_char_count); - if description_char_count > 0 { - truncated_description_count = truncated_description_count.saturating_add(1); - } - } - - let report = skill_render_report( - total_count, - included.len(), - omitted_count, - truncated_description_chars, - truncated_description_count, - ); - - (included, report) -} - -fn skill_render_report( - total_count: usize, - included_count: usize, - omitted_count: usize, - truncated_description_chars: usize, - truncated_description_count: usize, -) -> SkillRenderReport { - SkillRenderReport { - total_count, - included_count, - omitted_count, - truncated_description_chars, - truncated_description_count, - } -} - -impl SkillRenderReport { - fn average_truncated_description_chars(&self) -> usize { - if self.truncated_description_count == 0 { - return 0; - } - - self.truncated_description_chars - .saturating_add(self.truncated_description_count.saturating_sub(1)) - / self.truncated_description_count - } -} - -struct SkillLine<'a> { - name: &'a str, - description: &'a str, - path: String, -} - -struct RenderedSkillLine { - line: String, - truncated_chars: usize, -} - -struct DescriptionBudgetLine<'a> { - line: &'a SkillLine<'a>, - description_char_count: usize, - extra_costs: Vec, -} - -fn sum_description_truncation(rendered: &[RenderedSkillLine]) -> (usize, usize) { - rendered - .iter() - .fold((0usize, 0usize), |(chars, count), line| { - if line.truncated_chars == 0 { - (chars, count) - } else { - ( - chars.saturating_add(line.truncated_chars), - count.saturating_add(1), - ) - } - }) -} - -impl<'a> SkillLine<'a> { - fn new(skill: &'a SkillMetadata) -> Self { - Self { - name: skill.name.as_str(), - description: skill.description.as_str(), - path: skill.path_to_skills_md.to_string_lossy().replace('\\', "/"), - } - } - - fn full_cost(&self, budget: SkillMetadataBudget) -> usize { - line_cost(budget, &self.render_full()) - } - - fn minimum_cost(&self, budget: SkillMetadataBudget) -> usize { - line_cost(budget, &self.render_minimum()) - } - - fn description_char_count(&self) -> usize { - self.description.chars().count() - } - - fn render_full(&self) -> String { - self.render_with_description(self.description) - } - - fn render_minimum(&self) -> String { - self.render_with_description("") - } - - fn rendered_description_prefix_len(&self, description_chars: usize) -> usize { - self.description - .char_indices() - .nth(description_chars) - .map_or(self.description.len(), |(idx, _)| idx) - } - - fn render_with_description_chars(&self, description_chars: usize) -> String { - if description_chars == 0 { - format!("- {}: (file: {})", self.name, self.path) - } else { - let end = self.rendered_description_prefix_len(description_chars); - let description = &self.description[..end]; - format!("- {}: {} (file: {})", self.name, description, self.path) - } - } - - fn render_with_description(&self, description: &str) -> String { - if description.is_empty() { - format!("- {}: (file: {})", self.name, self.path) - } else { - format!("- {}: {} (file: {})", self.name, description, self.path) - } - } -} - -impl<'a> DescriptionBudgetLine<'a> { - fn new(line: &'a SkillLine<'a>, budget: SkillMetadataBudget) -> Self { - let minimum_line = line.render_minimum(); - let minimum_chars = minimum_line.chars().count().saturating_add(1); - let minimum_bytes = minimum_line.len().saturating_add(1); - let minimum_cost = budget.cost_from_counts(minimum_chars, minimum_bytes); - - let description_char_count = line.description_char_count(); - let mut extra_costs = Vec::with_capacity(description_char_count.saturating_add(1)); - extra_costs.push(0); - - let mut prefix_chars = 0usize; - let mut prefix_bytes = 0usize; - for ch in line.description.chars() { - prefix_chars = prefix_chars.saturating_add(1); - prefix_bytes = prefix_bytes.saturating_add(ch.len_utf8()); - let rendered_chars = minimum_chars.saturating_add(prefix_chars).saturating_add(1); - let rendered_bytes = minimum_bytes.saturating_add(prefix_bytes).saturating_add(1); - let cost = budget - .cost_from_counts(rendered_chars, rendered_bytes) - .saturating_sub(minimum_cost); - extra_costs.push(cost); - } - - Self { - line, - description_char_count, - extra_costs, - } - } -} - -fn line_cost(budget: SkillMetadataBudget, line: &str) -> usize { - budget.cost(&format!("{line}\n")) -} - -fn render_lines_with_description_budget( - budget: SkillMetadataBudget, - skill_lines: &[SkillLine<'_>], - limit: usize, -) -> Vec { - let budget_lines = skill_lines - .iter() - .map(|line| DescriptionBudgetLine::new(line, budget)) - .collect::>(); - let mut char_allocations = vec![0usize; budget_lines.len()]; - let mut current_extra_costs = vec![0usize; budget_lines.len()]; - let mut remaining = limit; - - // Distribute description space one character at a time across skills. - // Short descriptions naturally drop out, so their unused share can go to - // longer descriptions instead of being stranded in a fixed per-skill quota. - loop { - let mut changed = false; - for (index, line) in budget_lines.iter().enumerate() { - if char_allocations[index] >= line.description_char_count { - continue; - } - - let current_cost = current_extra_costs[index]; - let next_chars = char_allocations[index].saturating_add(1); - let next_cost = line.extra_costs[next_chars]; - let delta = next_cost.saturating_sub(current_cost); - if delta <= remaining { - char_allocations[index] = next_chars; - current_extra_costs[index] = next_cost; - remaining = remaining.saturating_sub(delta); - changed = true; - } - } - - if !changed { - break; - } - } - - budget_lines - .iter() - .zip(char_allocations) - .map(|(line, description_chars)| { - let truncated_chars = line - .description_char_count - .saturating_sub(description_chars); - RenderedSkillLine { - line: line.line.render_with_description_chars(description_chars), - truncated_chars, - } - }) - .collect() -} - -fn ordered_skills_for_budget(skills: &[SkillMetadata]) -> Vec<&SkillMetadata> { - let mut ordered = skills.iter().collect::>(); - ordered.sort_by(|a, b| { - prompt_scope_rank(a.scope) - .cmp(&prompt_scope_rank(b.scope)) - .then_with(|| a.name.cmp(&b.name)) - .then_with(|| a.path_to_skills_md.cmp(&b.path_to_skills_md)) - }); - ordered -} - -fn prompt_scope_rank(scope: SkillScope) -> u8 { - match scope { - SkillScope::System => 0, - SkillScope::Admin => 1, - SkillScope::Repo => 2, - SkillScope::User => 3, - } -} - -#[cfg(test)] -mod tests { - use super::*; - use codex_utils_absolute_path::test_support::PathBufExt; - use codex_utils_absolute_path::test_support::test_path_buf; - use pretty_assertions::assert_eq; - - fn make_skill(name: &str, scope: SkillScope) -> SkillMetadata { - SkillMetadata { - name: name.to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf(&format!("/tmp/{name}/SKILL.md")).abs(), - scope, - } - } - - fn make_skill_with_description( - name: &str, - scope: SkillScope, - description: &str, - ) -> SkillMetadata { - let mut skill = make_skill(name, scope); - skill.description = description.to_string(); - skill - } - - fn expected_skill_line(skill: &SkillMetadata, description: &str) -> String { - SkillLine::new(skill).render_with_description(description) - } - - #[test] - fn default_budget_uses_two_percent_of_full_context_window() { - assert_eq!( - default_skill_metadata_budget(Some(200_000)), - SkillMetadataBudget::Tokens(4_000) - ); - assert_eq!( - default_skill_metadata_budget(Some(99)), - SkillMetadataBudget::Tokens(1) - ); - } - - #[test] - fn default_budget_falls_back_to_characters_without_context_window() { - assert_eq!( - default_skill_metadata_budget(/*context_window*/ None), - SkillMetadataBudget::Characters(DEFAULT_SKILL_METADATA_CHAR_BUDGET) - ); - assert_eq!( - default_skill_metadata_budget(Some(-1)), - SkillMetadataBudget::Characters(DEFAULT_SKILL_METADATA_CHAR_BUDGET) - ); - } - - #[test] - fn budgeted_rendering_truncates_descriptions_equally_before_omitting_skills() { - let alpha = make_skill_with_description("alpha-skill", SkillScope::Repo, "abcdef"); - let beta = make_skill_with_description("beta-skill", SkillScope::Repo, "uvwxyz"); - let minimum_cost = SkillLine::new(&alpha) - .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)) - + SkillLine::new(&beta).minimum_cost(SkillMetadataBudget::Characters(usize::MAX)); - let budget = SkillMetadataBudget::Characters(minimum_cost + 6); - - let rendered = build_available_skills( - &[beta.clone(), alpha.clone()], - budget, - SkillRenderSideEffects::None, - ) - .expect("skills should render"); - - assert_eq!(rendered.report.included_count, 2); - assert_eq!(rendered.report.omitted_count, 0); - assert_eq!(rendered.report.truncated_description_chars, 8); - assert_eq!(rendered.warning_message, None); - assert_eq!( - rendered.skill_lines, - vec![ - expected_skill_line(&alpha, "ab"), - expected_skill_line(&beta, "uv"), - ] - ); - } - - #[test] - fn budgeted_rendering_does_not_warn_when_average_description_truncation_is_within_threshold() { - let alpha = make_skill_with_description("alpha-skill", SkillScope::Repo, "abcdefghij"); - let beta = make_skill_with_description("beta-skill", SkillScope::Repo, "uvwxyzabcd"); - let minimum_cost = SkillLine::new(&alpha) - .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)) - + SkillLine::new(&beta).minimum_cost(SkillMetadataBudget::Characters(usize::MAX)); - let budget = SkillMetadataBudget::Characters(minimum_cost + 6); - - let rendered = build_available_skills(&[alpha, beta], budget, SkillRenderSideEffects::None) - .expect("skills should render"); - - assert_eq!(rendered.report.included_count, 2); - assert_eq!(rendered.report.omitted_count, 0); - assert_eq!(rendered.report.truncated_description_chars, 16); - assert_eq!(rendered.report.truncated_description_count, 2); - assert_eq!(rendered.warning_message, None); - } - - #[test] - fn budgeted_rendering_warns_when_average_description_truncation_exceeds_threshold() { - let alpha = - make_skill_with_description("alpha-skill", SkillScope::Repo, "abcdefghijklmnop"); - let beta = make_skill_with_description("beta-skill", SkillScope::Repo, "uvwxyzabcdefghij"); - let minimum_cost = SkillLine::new(&alpha) - .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)) - + SkillLine::new(&beta).minimum_cost(SkillMetadataBudget::Characters(usize::MAX)); - let budget = SkillMetadataBudget::Characters(minimum_cost + 6); - - let rendered = build_available_skills(&[alpha, beta], budget, SkillRenderSideEffects::None) - .expect("skills should render"); - - assert_eq!(rendered.report.included_count, 2); - assert_eq!(rendered.report.omitted_count, 0); - assert_eq!(rendered.report.truncated_description_chars, 28); - assert_eq!(rendered.report.truncated_description_count, 2); - assert_eq!( - rendered.warning_message, - Some( - "Warning: Exceeded skills context budget. Loaded skill descriptions were truncated by an average of 14 characters per skill." - .to_string() - ) - ); - } - - #[test] - fn budgeted_rendering_redistributes_unused_description_budget() { - let short = make_skill_with_description("short-skill", SkillScope::Repo, "x"); - let long = make_skill_with_description("long-skill", SkillScope::Repo, "abcdefghi"); - let minimum_cost = SkillLine::new(&short) - .minimum_cost(SkillMetadataBudget::Characters(usize::MAX)) - + SkillLine::new(&long).minimum_cost(SkillMetadataBudget::Characters(usize::MAX)); - let budget = SkillMetadataBudget::Characters(minimum_cost + 11); - - let rendered = build_available_skills( - &[short.clone(), long.clone()], - budget, - SkillRenderSideEffects::None, - ) - .expect("skills should render"); - - assert_eq!(rendered.report.included_count, 2); - assert_eq!(rendered.report.omitted_count, 0); - assert_eq!(rendered.warning_message, None); - assert_eq!( - rendered.skill_lines, - vec![ - expected_skill_line(&long, "abcdefgh"), - expected_skill_line(&short, "x"), - ] - ); - } - - #[test] - fn budgeted_rendering_preserves_prompt_priority_when_minimum_lines_exceed_budget() { - let system = make_skill("system-skill", SkillScope::System); - let user = make_skill("user-skill", SkillScope::User); - let repo = make_skill("repo-skill", SkillScope::Repo); - let admin = make_skill("admin-skill", SkillScope::Admin); - let system_cost = SkillMetadataBudget::Characters(usize::MAX) - .cost(&format!("{}\n", SkillLine::new(&system).render_minimum())); - let admin_cost = SkillMetadataBudget::Characters(usize::MAX) - .cost(&format!("{}\n", SkillLine::new(&admin).render_minimum())); - let budget = SkillMetadataBudget::Characters(system_cost + admin_cost); - - let rendered = build_available_skills( - &[system, user, repo, admin], - budget, - SkillRenderSideEffects::None, - ) - .expect("skills should render"); - - assert_eq!(rendered.report.included_count, 2); - assert_eq!(rendered.report.omitted_count, 2); - assert_eq!( - rendered.warning_message, - Some( - "Warning: Exceeded skills context budget. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list." - .to_string() - ) - ); - let rendered_text = rendered.skill_lines.join("\n"); - assert!(rendered_text.contains("- system-skill:")); - assert!(rendered_text.contains("- admin-skill:")); - assert!(!rendered_text.contains("desc")); - assert!(!rendered_text.contains("- repo-skill:")); - assert!(!rendered_text.contains("- user-skill:")); - } - - #[test] - fn budgeted_rendering_keeps_scanning_after_oversized_entry() { - let mut oversized = make_skill("oversized-system-skill", SkillScope::System); - oversized.description = "desc ".repeat(100); - let repo = make_skill("repo-skill", SkillScope::Repo); - let repo_cost = SkillMetadataBudget::Characters(usize::MAX) - .cost(&format!("{}\n", SkillLine::new(&repo).render_full())); - let budget = SkillMetadataBudget::Characters(repo_cost); - - let rendered = - build_available_skills(&[oversized, repo], budget, SkillRenderSideEffects::None) - .expect("skills render"); - - assert_eq!(rendered.report.included_count, 1); - assert_eq!(rendered.report.omitted_count, 1); - assert_eq!( - rendered.warning_message, - Some( - "Warning: Exceeded skills context budget. All skill descriptions were removed and 1 additional skill was not included in the model-visible skills list." - .to_string() - ) - ); - let rendered_text = rendered.skill_lines.join("\n"); - assert!(!rendered_text.contains("- oversized-system-skill:")); - assert!(rendered_text.contains("- repo-skill:")); - } -} diff --git a/vendor/codex/codex-rs/core-skills/src/system.rs b/vendor/codex/codex-rs/core-skills/src/system.rs deleted file mode 100644 index a8050ef6..00000000 --- a/vendor/codex/codex-rs/core-skills/src/system.rs +++ /dev/null @@ -1,15 +0,0 @@ -use codex_utils_absolute_path::AbsolutePathBuf; - -const SYSTEM_SKILLS_DIR_NAME: &str = ".system"; -const SKILLS_DIR_NAME: &str = "skills"; - -/// Returns the legacy on-disk cache location for previously embedded system skills. -pub(crate) fn system_cache_root_dir(codex_home: &AbsolutePathBuf) -> AbsolutePathBuf { - codex_home - .join(SKILLS_DIR_NAME) - .join(SYSTEM_SKILLS_DIR_NAME) -} - -pub(crate) fn uninstall_system_skills(codex_home: &AbsolutePathBuf) { - let _ = std::fs::remove_dir_all(system_cache_root_dir(codex_home)); -} diff --git a/vendor/codex/codex-rs/core/Cargo.toml b/vendor/codex/codex-rs/core/Cargo.toml index 68f10b84..4e585e6c 100644 --- a/vendor/codex/codex-rs/core/Cargo.toml +++ b/vendor/codex/codex-rs/core/Cargo.toml @@ -32,7 +32,6 @@ codex-apply-patch = { workspace = true } codex-async-utils = { workspace = true } codex-code-mode = { workspace = true } codex-config = { workspace = true } -codex-core-skills = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } codex-feedback = { workspace = true } diff --git a/vendor/codex/codex-rs/core/src/codex_delegate.rs b/vendor/codex/codex-rs/core/src/codex_delegate.rs index ce330203..0d9a208a 100644 --- a/vendor/codex/codex-rs/core/src/codex_delegate.rs +++ b/vendor/codex/codex-rs/core/src/codex_delegate.rs @@ -79,9 +79,7 @@ pub(crate) async fn run_codex_thread_interactive( auth_manager, models_manager, environment_manager: Arc::clone(&parent_session.services.environment_manager), - skills_manager: Arc::clone(&parent_session.services.skills_manager), mcp_manager: Arc::clone(&parent_session.services.mcp_manager), - skills_watcher: Arc::clone(&parent_session.services.skills_watcher), conversation_history: initial_history.unwrap_or(InitialHistory::New), session_source: SessionSource::SubAgent(subagent_source.clone()), agent_control: parent_session.services.agent_control.clone(), diff --git a/vendor/codex/codex-rs/core/src/config/mod.rs b/vendor/codex/codex-rs/core/src/config/mod.rs index e9d8cb7b..5b2cc5df 100644 --- a/vendor/codex/codex-rs/core/src/config/mod.rs +++ b/vendor/codex/codex-rs/core/src/config/mod.rs @@ -2465,10 +2465,6 @@ impl Config { .network .is_some() } - - pub fn bundled_skills_enabled(&self) -> bool { - crate::manager::bundled_skills_enabled_from_stack(&self.config_layer_stack) - } } pub(crate) fn uses_deprecated_instructions_file(config_layer_stack: &ConfigLayerStack) -> bool { diff --git a/vendor/codex/codex-rs/core/src/context/skill_instructions.rs b/vendor/codex/codex-rs/core/src/context/skill_instructions.rs index 22f6ba22..3c77cc9c 100644 --- a/vendor/codex/codex-rs/core/src/context/skill_instructions.rs +++ b/vendor/codex/codex-rs/core/src/context/skill_instructions.rs @@ -1,5 +1,3 @@ -use codex_core_skills::injection::SkillInjection; - use super::ContextualUserFragment; #[derive(Debug, Clone, PartialEq)] @@ -9,16 +7,6 @@ pub(crate) struct SkillInstructions { pub(crate) contents: String, } -impl From<&SkillInjection> for SkillInstructions { - fn from(skill: &SkillInjection) -> Self { - Self { - name: skill.name.clone(), - path: skill.path.clone(), - contents: skill.contents.clone(), - } - } -} - impl ContextualUserFragment for SkillInstructions { const ROLE: &'static str = "user"; const START_MARKER: &'static str = ""; diff --git a/vendor/codex/codex-rs/core/src/lib.rs b/vendor/codex/codex-rs/core/src/lib.rs index e4734778..722bac64 100644 --- a/vendor/codex/codex-rs/core/src/lib.rs +++ b/vendor/codex/codex-rs/core/src/lib.rs @@ -95,11 +95,6 @@ pub mod sandboxing; mod session_prefix; mod session_startup_prewarm; mod shell_detect; -pub mod skills; -pub(crate) use skills::SkillLoadOutcome; -pub(crate) use skills::SkillsManager; -pub(crate) use skills::manager; -mod skills_watcher; mod stream_events_utils; pub mod test_support; mod unified_exec; diff --git a/vendor/codex/codex-rs/core/src/session/mod.rs b/vendor/codex/codex-rs/core/src/session/mod.rs index deb49174..ad62f559 100644 --- a/vendor/codex/codex-rs/core/src/session/mod.rs +++ b/vendor/codex/codex-rs/core/src/session/mod.rs @@ -187,7 +187,6 @@ pub(crate) use self::session::SessionSettingsUpdate; use self::turn::AssistantMessageStreamParsers; use self::turn::realtime_text_for_event; use self::turn_context::TurnContext; -use self::turn_context::TurnSkillsContext; #[cfg(test)] mod rollout_reconstruction_tests; @@ -242,8 +241,6 @@ pub(crate) struct PreviousTurnSettings { pub(crate) realtime_active: Option, } -use crate::SkillLoadOutcome; -use crate::SkillsManager; use crate::agents_md::AgentsMdManager; use crate::context::UserInstructions; use crate::exec_policy::ExecPolicyUpdateError; @@ -259,7 +256,6 @@ use crate::rollout::policy::EventPersistenceMode; use crate::session_startup_prewarm::SessionStartupPrewarmHandle; use crate::shell; use crate::shell_snapshot::ShellSnapshot; -use crate::skills_watcher::SkillsWatcher; use crate::state::ActiveTurn; use crate::state::MailboxDeliveryPhase; use crate::state::PendingRequestPermissions; @@ -364,9 +360,7 @@ pub(crate) struct CodexSpawnArgs { pub(crate) auth_manager: Arc, pub(crate) models_manager: Arc, pub(crate) environment_manager: Arc, - pub(crate) skills_manager: Arc, pub(crate) mcp_manager: Arc, - pub(crate) skills_watcher: Arc, pub(crate) conversation_history: InitialHistory, pub(crate) session_source: SessionSource, pub(crate) agent_control: AgentControl, @@ -419,9 +413,7 @@ impl Codex { auth_manager, models_manager, environment_manager, - skills_manager, mcp_manager, - skills_watcher, conversation_history, session_source, agent_control, @@ -604,9 +596,7 @@ impl Codex { agent_status_tx.clone(), conversation_history, session_source_clone, - skills_manager, mcp_manager.clone(), - skills_watcher, agent_control, environment_manager, analytics_events_client, diff --git a/vendor/codex/codex-rs/core/src/session/review.rs b/vendor/codex/codex-rs/core/src/session/review.rs index 62f4c9a8..9ecda468 100644 --- a/vendor/codex/codex-rs/core/src/session/review.rs +++ b/vendor/codex/codex-rs/core/src/session/review.rs @@ -138,7 +138,6 @@ pub(super) async fn spawn_review_thread( dynamic_tools: parent_turn_context.dynamic_tools.clone(), truncation_policy: model_info.truncation_policy.into(), turn_metadata_state, - turn_skills: TurnSkillsContext::new(parent_turn_context.turn_skills.outcome.clone()), turn_timing_state: Arc::new(TurnTimingState::default()), }; diff --git a/vendor/codex/codex-rs/core/src/session/session.rs b/vendor/codex/codex-rs/core/src/session/session.rs index b146b967..c8df5c64 100644 --- a/vendor/codex/codex-rs/core/src/session/session.rs +++ b/vendor/codex/codex-rs/core/src/session/session.rs @@ -265,9 +265,7 @@ impl Session { agent_status: watch::Sender, initial_history: InitialHistory, session_source: SessionSource, - skills_manager: Arc, mcp_manager: Arc, - skills_watcher: Arc, agent_control: AgentControl, environment_manager: Arc, analytics_events_client: Option, @@ -731,9 +729,7 @@ impl Session { guardian_rejections: Mutex::new(HashMap::new()), guardian_rejection_circuit_breaker: Mutex::new(Default::default()), runtime_handle: tokio::runtime::Handle::current(), - skills_manager, mcp_manager: Arc::clone(&mcp_manager), - skills_watcher, agent_control, network_proxy, network_approval: Arc::clone(&network_approval), diff --git a/vendor/codex/codex-rs/core/src/session/tests.rs b/vendor/codex/codex-rs/core/src/session/tests.rs index 5243e3d1..a3d2ba78 100644 --- a/vendor/codex/codex-rs/core/src/session/tests.rs +++ b/vendor/codex/codex-rs/core/src/session/tests.rs @@ -14,13 +14,8 @@ use crate::context::TurnAborted; use crate::exec::ExecCapturePolicy; use crate::function_tool::FunctionCallError; use crate::shell::default_user_shell; -use crate::skills::SkillRenderSideEffects; -use crate::skills::build_available_skills; -use crate::skills::render::SkillMetadataBudget; use crate::tools::format_exec_output_str; -use codex_core_skills::SkillMetadata; -use codex_core_skills::SkillsLoadInput; use codex_features::Feature; use codex_features::Features; use codex_login::CodexAuth; @@ -68,12 +63,6 @@ use codex_execpolicy::Decision; use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; use codex_network_proxy::NetworkProxyConfig; -use codex_otel::MetricsClient; -use codex_otel::MetricsConfig; -use codex_otel::THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC; -use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC; -use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; -use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; use codex_otel::TelemetryAuthMode; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; @@ -116,7 +105,6 @@ use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::ResumedHistory; use codex_protocol::protocol::RolloutItem; -use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::Submission; use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::TokenCountEvent; @@ -138,16 +126,10 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::test_codex; -use core_test_support::test_path_buf; use core_test_support::tracing::install_test_tracing; use core_test_support::wait_for_event; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; -use opentelemetry_sdk::metrics::InMemoryMetricExporter; -use opentelemetry_sdk::metrics::data::AggregatedMetrics; -use opentelemetry_sdk::metrics::data::Metric; -use opentelemetry_sdk::metrics::data::MetricData; -use opentelemetry_sdk::metrics::data::ResourceMetrics; use std::path::Path; use std::time::Duration; use tokio::sync::Semaphore; @@ -166,18 +148,6 @@ use std::time::Duration as StdDuration; mod guardian_tests; -fn skills_load_input_from_config( - config: &Config, - effective_skill_roots: Vec, -) -> SkillsLoadInput { - SkillsLoadInput::new( - config.cwd.clone(), - effective_skill_roots, - config.config_layer_stack.clone(), - config.bundled_skills_enabled(), - ) -} - fn sample_epiphany_state_for_persistence() -> EpiphanyThreadState { EpiphanyThreadState { revision: 1, @@ -468,54 +438,6 @@ fn assistant_message(text: &str) -> ResponseItem { } } -fn test_session_telemetry_without_metadata() -> SessionTelemetry { - let exporter = InMemoryMetricExporter::default(); - let metrics = MetricsClient::new( - MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter) - .with_runtime_reader(), - ) - .expect("in-memory metrics client"); - SessionTelemetry::new( - ThreadId::new(), - "gpt-5.4", - "gpt-5.4", - /*account_id*/ None, - /*account_email*/ None, - /*auth_mode*/ None, - "test_originator".to_string(), - /*log_user_prompts*/ false, - "tty".to_string(), - SessionSource::Cli, - ) - .with_metrics_without_metadata_tags(metrics) -} - -fn find_metric<'a>(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Metric { - for scope_metrics in resource_metrics.scope_metrics() { - for metric in scope_metrics.metrics() { - if metric.name() == name { - return metric; - } - } - } - panic!("metric {name} missing"); -} - -fn histogram_sum(resource_metrics: &ResourceMetrics, name: &str) -> u64 { - let metric = find_metric(resource_metrics, name); - match metric.data() { - AggregatedMetrics::F64(data) => match data { - MetricData::Histogram(histogram) => { - let points: Vec<_> = histogram.data_points().collect(); - assert_eq!(points.len(), 1); - points[0].sum().round() as u64 - } - _ => panic!("unexpected histogram aggregation"), - }, - _ => panic!("unexpected metric data type"), - } -} - #[tokio::test] async fn regular_turn_emits_turn_started_without_waiting_for_startup_prewarm() { let (sess, tc, rx) = make_session_and_context_with_rx().await; @@ -2891,92 +2813,6 @@ async fn session_configuration_apply_permission_profile_preserves_existing_deny_ ); } -#[cfg_attr(windows, ignore)] -#[tokio::test] -async fn new_default_turn_uses_config_aware_skills_for_role_overrides() { - let (session, _turn_context) = make_session_and_context().await; - let parent_config = session.get_config().await; - let codex_home = parent_config.codex_home.clone(); - let skill_dir = codex_home.join("skills").join("demo"); - std::fs::create_dir_all(&skill_dir).expect("create skill dir"); - let skill_path = skill_dir.join("SKILL.md"); - std::fs::write( - &skill_path, - "---\nname: demo-skill\ndescription: demo description\n---\n\n# Body\n", - ) - .expect("write skill"); - - let skill_fs = session - .services - .environment_manager - .default_environment() - .map(|environment| environment.get_filesystem()) - .unwrap_or_else(|| std::sync::Arc::clone(&codex_exec_server::LOCAL_FS)); - let parent_outcome = session - .services - .skills_manager - .skills_for_cwd( - &skills_load_input_from_config(&parent_config, Vec::new()), - /*force_reload*/ true, - Some(Arc::clone(&skill_fs)), - ) - .await; - let parent_skill = parent_outcome - .skills - .iter() - .find(|skill| skill.name == "demo-skill") - .expect("demo skill should be discovered"); - assert_eq!(parent_outcome.is_skill_enabled(parent_skill), true); - - let role_path = codex_home.join("skills-role.toml"); - std::fs::write( - &role_path, - format!( - r#"developer_instructions = "Stay focused" - -[[skills.config]] -path = "{}" -enabled = false -"#, - skill_path.display() - ), - ) - .expect("write role config"); - - let mut child_config = (*parent_config).clone(); - child_config.agent_roles.insert( - "custom".to_string(), - crate::config::AgentRoleConfig { - description: None, - config_file: Some(role_path.to_path_buf()), - nickname_candidates: None, - }, - ); - crate::agent::role::apply_role_to_config(&mut child_config, Some("custom")) - .await - .expect("custom role should apply"); - - { - let mut state = session.state.lock().await; - state.session_configuration.original_config_do_not_use = Arc::new(child_config); - } - - let child_turn = session - .new_default_turn_with_sub_id("role-skill-turn".to_string()) - .await; - let child_skill = child_turn - .turn_skills - .outcome - .skills - .iter() - .find(|skill| skill.name == "demo-skill") - .expect("demo skill should be discovered"); - assert_eq!( - child_turn.turn_skills.outcome.is_skill_enabled(child_skill), - false - ); -} - #[tokio::test] async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_update() { let mut session_configuration = make_session_configuration_for_tests().await; @@ -3115,10 +2951,6 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { let (tx_event, _rx_event) = async_channel::unbounded(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new( - config.codex_home.clone(), - /*bundled_skills_enabled*/ true, - )); let result = Session::new( session_configuration, Arc::clone(&config), @@ -3129,9 +2961,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() { agent_status_tx, InitialHistory::New, SessionSource::Exec, - skills_manager, mcp_manager, - Arc::new(SkillsWatcher::noop()), AgentControl::default(), Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, @@ -3225,17 +3055,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let state = SessionState::new(session_configuration.clone()); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new( - config.codex_home.clone(), - /*bundled_skills_enabled*/ true, - )); let network_approval = Arc::new(NetworkApprovalService::default()); let environment = Arc::new( codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None) .expect("create environment"), ); - - let skills_watcher = Arc::new(SkillsWatcher::noop()); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, @@ -3269,9 +3093,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { guardian_rejections: Mutex::new(std::collections::HashMap::new()), guardian_rejection_circuit_breaker: Mutex::new(Default::default()), runtime_handle: tokio::runtime::Handle::current(), - skills_manager, mcp_manager, - skills_watcher, agent_control, network_proxy: None, network_approval: Arc::clone(&network_approval), @@ -3299,15 +3121,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { config.js_repl_node_path.clone(), config.js_repl_node_module_dirs.clone(), )); - - let skills_input = skills_load_input_from_config(&per_turn_config, Vec::new()); - let skill_fs = environment.get_filesystem(); - let skills_outcome = Arc::new( - services - .skills_manager - .skills_for_config(&skills_input, Some(Arc::clone(&skill_fs))) - .await, - ); let turn_context = Session::make_turn_context( conversation_id, Some(Arc::clone(&auth_manager)), @@ -3326,7 +3139,6 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { session_configuration.cwd.clone(), "turn_id".to_string(), Arc::clone(&js_repl), - skills_outcome, ); let (mailbox, mailbox_rx) = crate::agent::Mailbox::new(); @@ -3423,10 +3235,6 @@ async fn make_session_with_config_and_rx( let (tx_event, rx_event) = async_channel::unbounded(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new( - config.codex_home.clone(), - /*bundled_skills_enabled*/ true, - )); let session = Session::new( session_configuration, @@ -3438,9 +3246,7 @@ async fn make_session_with_config_and_rx( agent_status_tx, InitialHistory::New, SessionSource::Exec, - skills_manager, mcp_manager, - Arc::new(SkillsWatcher::noop()), AgentControl::default(), Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*analytics_events_client*/ None, @@ -4527,17 +4333,11 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( let state = SessionState::new(session_configuration.clone()); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new( - config.codex_home.clone(), - /*bundled_skills_enabled*/ true, - )); let network_approval = Arc::new(NetworkApprovalService::default()); let environment = Arc::new( codex_exec_server::Environment::create_for_tests(/*exec_server_url*/ None) .expect("create environment"), ); - - let skills_watcher = Arc::new(SkillsWatcher::noop()); let services = SessionServices { mcp_connection_manager: Arc::new(RwLock::new(McpConnectionManager::new_uninitialized( &config.permissions.approval_policy, @@ -4571,9 +4371,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( guardian_rejections: Mutex::new(std::collections::HashMap::new()), guardian_rejection_circuit_breaker: Mutex::new(Default::default()), runtime_handle: tokio::runtime::Handle::current(), - skills_manager, mcp_manager, - skills_watcher, agent_control, network_proxy: None, network_approval: Arc::clone(&network_approval), @@ -4601,15 +4399,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( config.js_repl_node_path.clone(), config.js_repl_node_module_dirs.clone(), )); - - let skills_input = skills_load_input_from_config(&per_turn_config, Vec::new()); - let skill_fs = environment.get_filesystem(); - let skills_outcome = Arc::new( - services - .skills_manager - .skills_for_config(&skills_input, Some(Arc::clone(&skill_fs))) - .await, - ); let turn_context = Arc::new(Session::make_turn_context( conversation_id, Some(Arc::clone(&auth_manager)), @@ -4628,7 +4417,6 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( session_configuration.cwd.clone(), "turn_id".to_string(), Arc::clone(&js_repl), - skills_outcome, )); let (mailbox, mailbox_rx) = crate::agent::Mailbox::new(); @@ -5034,200 +4822,6 @@ async fn build_initial_context_omits_default_image_save_location_without_image_h ); } -#[tokio::test] -async fn build_initial_context_trims_skill_metadata_from_context_window_budget() { - let (session, mut turn_context) = make_session_and_context().await; - let mut outcome = SkillLoadOutcome::default(); - outcome.skills = vec![ - SkillMetadata { - name: "admin-skill".to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(), - scope: SkillScope::Admin, - }, - SkillMetadata { - name: "repo-skill".to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), - scope: SkillScope::Repo, - }, - ]; - turn_context.model_info.context_window = Some(100); - turn_context.turn_skills = TurnSkillsContext::new(Arc::new(outcome)); - - let initial_context = session.build_initial_context(&turn_context).await; - let developer_texts = developer_input_texts(&initial_context); - - assert!( - developer_texts - .iter() - .all(|text| !text.contains("Exceeded skills context budget")), - "expected skill budget warning to stay out of the initial context, got {developer_texts:?}" - ); - assert!( - developer_texts - .iter() - .all(|text| !text.contains("- admin-skill:") && !text.contains("- repo-skill:")), - "expected no skill metadata entries to fit the tiny budget, got {developer_texts:?}" - ); -} - -#[test] -fn emit_thread_start_skill_metrics_records_enabled_kept_and_truncated_values() { - let session_telemetry = test_session_telemetry_without_metadata(); - let rendered = build_available_skills( - &[SkillMetadata { - name: "repo-skill".to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), - scope: SkillScope::Repo, - }], - SkillMetadataBudget::Characters(1), - SkillRenderSideEffects::ThreadStart { - session_telemetry: &session_telemetry, - }, - ) - .expect("skills should render"); - - assert_eq!( - rendered.warning_message, - Some( - "Warning: Exceeded skills context budget. All skill descriptions were removed and 1 additional skill was not included in the model-visible skills list." - .to_string() - ) - ); - let snapshot = session_telemetry - .snapshot_metrics() - .expect("runtime metrics snapshot"); - assert_eq!( - histogram_sum(&snapshot, THREAD_SKILLS_ENABLED_TOTAL_METRIC), - 1 - ); - assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_KEPT_TOTAL_METRIC), 0); - assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_TRUNCATED_METRIC), 1); - assert_eq!( - histogram_sum(&snapshot, THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC), - 4 - ); -} - -#[test] -fn emit_thread_start_skill_metrics_records_description_truncated_chars_without_omitted_skills() { - let session_telemetry = test_session_telemetry_without_metadata(); - let alpha = SkillMetadata { - name: "alpha-skill".to_string(), - description: "abcdef".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/alpha-skill/SKILL.md").abs(), - scope: SkillScope::Repo, - }; - let beta = SkillMetadata { - name: "beta-skill".to_string(), - description: "uvwxyz".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/beta-skill/SKILL.md").abs(), - scope: SkillScope::Repo, - }; - let minimum_skill_line_cost = |skill: &SkillMetadata| { - let path = skill.path_to_skills_md.to_string_lossy().replace('\\', "/"); - format!("- {}: (file: {})\n", skill.name, path) - .chars() - .count() - }; - let minimum_budget = minimum_skill_line_cost(&alpha) + minimum_skill_line_cost(&beta); - - let rendered = build_available_skills( - &[alpha, beta], - SkillMetadataBudget::Characters(minimum_budget + 6), - SkillRenderSideEffects::ThreadStart { - session_telemetry: &session_telemetry, - }, - ) - .expect("skills should render"); - - assert_eq!(rendered.report.omitted_count, 0); - assert_eq!(rendered.report.truncated_description_chars, 8); - let snapshot = session_telemetry - .snapshot_metrics() - .expect("runtime metrics snapshot"); - assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_TRUNCATED_METRIC), 0); - assert_eq!( - histogram_sum(&snapshot, THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC), - 8 - ); -} - -#[tokio::test] -async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_builds() { - let (session, turn_context, rx) = make_session_and_context_with_rx().await; - let mut turn_context = Arc::into_inner(turn_context).expect("sole turn context owner"); - let mut outcome = SkillLoadOutcome::default(); - outcome.skills = vec![ - SkillMetadata { - name: "admin-skill".to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(), - scope: SkillScope::Admin, - }, - SkillMetadata { - name: "repo-skill".to_string(), - description: "desc".to_string(), - short_description: None, - interface: None, - dependencies: None, - policy: None, - path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), - scope: SkillScope::Repo, - }, - ]; - turn_context.model_info.context_window = Some(100); - turn_context.turn_skills = TurnSkillsContext::new(Arc::new(outcome)); - - let _ = session.build_initial_context(&turn_context).await; - let warning_event = timeout(Duration::from_secs(1), rx.recv()) - .await - .expect("warning event should arrive") - .expect("warning event should be readable"); - assert!(matches!( - warning_event.msg, - EventMsg::Warning(WarningEvent { message }) - if message == "Warning: Exceeded skills context budget of 2%. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list." - )); - - let _ = session.build_initial_context(&turn_context).await; - let warning_event = timeout(Duration::from_secs(1), rx.recv()) - .await - .expect("warning event should arrive on repeated build") - .expect("warning event should be readable"); - assert!(matches!( - warning_event.msg, - EventMsg::Warning(WarningEvent { message }) - if message == "Warning: Exceeded skills context budget of 2%. All skill descriptions were removed and 2 additional skills were not included in the model-visible skills list." - )); -} - #[tokio::test] async fn handle_output_item_done_records_image_save_history_message() { let (session, turn_context) = make_session_and_context().await; @@ -7127,7 +6721,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { }; let expected = format!( - "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}", + "approval policy is {policy:?}; reject command — you should not ask for escalated permissions if the approval policy is {policy:?}", policy = turn_context.approval_policy.value() ); @@ -7204,7 +6798,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() }; let expected = format!( - "approval policy is {policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {policy:?}", + "approval policy is {policy:?}; reject command — you cannot ask for escalated permissions if the approval policy is {policy:?}", policy = turn_context.approval_policy.value() ); diff --git a/vendor/codex/codex-rs/core/src/session/tests/guardian_tests.rs b/vendor/codex/codex-rs/core/src/session/tests/guardian_tests.rs index 7bcd2839..e3b0aa06 100644 --- a/vendor/codex/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/vendor/codex/codex-rs/core/src/session/tests/guardian_tests.rs @@ -737,21 +737,14 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { /*model_catalog*/ None, CollaborationModesConfig::default(), )); - let skills_manager = Arc::new(SkillsManager::new( - config.codex_home.clone(), - /*bundled_skills_enabled*/ true, - )); let mcp_manager = Arc::new(McpManager::new()); - let skills_watcher = Arc::new(SkillsWatcher::noop()); let CodexSpawnOk { codex, .. } = Codex::spawn(CodexSpawnArgs { config, auth_manager, models_manager, environment_manager: Arc::new(EnvironmentManager::default_for_tests()), - skills_manager, mcp_manager, - skills_watcher, conversation_history: InitialHistory::New, session_source: SessionSource::SubAgent(SubAgentSource::Other( GUARDIAN_REVIEWER_NAME.to_string(), diff --git a/vendor/codex/codex-rs/core/src/session/turn_context.rs b/vendor/codex/codex-rs/core/src/session/turn_context.rs index 92f39fb1..954ff941 100644 --- a/vendor/codex/codex-rs/core/src/session/turn_context.rs +++ b/vendor/codex/codex-rs/core/src/session/turn_context.rs @@ -11,17 +11,6 @@ pub(super) fn image_generation_tool_auth_allowed(auth_manager: Option<&AuthManag ) } -#[derive(Clone, Debug)] -pub(crate) struct TurnSkillsContext { - pub(crate) outcome: Arc, -} - -impl TurnSkillsContext { - pub(crate) fn new(outcome: Arc) -> Self { - Self { outcome } - } -} - #[derive(Clone, Debug)] pub(crate) struct TurnEnvironment { #[allow(dead_code)] @@ -76,7 +65,6 @@ pub(crate) struct TurnContext { pub(crate) js_repl: Arc, pub(crate) dynamic_tools: Vec, pub(crate) turn_metadata_state: Arc, - pub(crate) turn_skills: TurnSkillsContext, pub(crate) turn_timing_state: Arc, } impl TurnContext { @@ -204,7 +192,6 @@ impl TurnContext { js_repl: Arc::clone(&self.js_repl), dynamic_tools: self.dynamic_tools.clone(), turn_metadata_state: self.turn_metadata_state.clone(), - turn_skills: self.turn_skills.clone(), turn_timing_state: Arc::clone(&self.turn_timing_state), } } @@ -365,7 +352,6 @@ impl Session { cwd: AbsolutePathBuf, sub_id: String, js_repl: Arc, - skills_outcome: Arc, ) -> TurnContext { let reasoning_effort = session_configuration.collaboration_mode.reasoning_effort(); let reasoning_summary = session_configuration @@ -457,7 +443,6 @@ impl Session { js_repl, dynamic_tools: session_configuration.dynamic_tools.clone(), turn_metadata_state, - turn_skills: TurnSkillsContext::new(skills_outcome), turn_timing_state: Arc::new(TurnTimingState::default()), } } @@ -614,7 +599,6 @@ impl Session { &per_turn_config.to_models_manager_config(), ) .await; - let skills_outcome = Arc::new(SkillLoadOutcome::default()); let mut turn_context: TurnContext = Self::make_turn_context( self.conversation_id, Some(Arc::clone(&self.services.auth_manager)), @@ -641,7 +625,6 @@ impl Session { cwd, sub_id, Arc::clone(&self.js_repl), - skills_outcome, ); turn_context.realtime_active = self.conversation.running_state().await.is_some(); diff --git a/vendor/codex/codex-rs/core/src/skills.rs b/vendor/codex/codex-rs/core/src/skills.rs deleted file mode 100644 index cefecfac..00000000 --- a/vendor/codex/codex-rs/core/src/skills.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub use codex_core_skills::SkillDependencyInfo; -pub use codex_core_skills::SkillError; -pub use codex_core_skills::SkillLoadOutcome; -pub use codex_core_skills::SkillMetadata; -pub use codex_core_skills::SkillPolicy; -pub use codex_core_skills::SkillRenderReport; -pub use codex_core_skills::SkillsLoadInput; -pub use codex_core_skills::SkillsManager; -pub use codex_core_skills::build_available_skills; -pub use codex_core_skills::collect_env_var_dependencies; -pub use codex_core_skills::config_rules; -pub use codex_core_skills::default_skill_metadata_budget; -pub use codex_core_skills::detect_implicit_skill_invocation_for_command; -pub use codex_core_skills::filter_skill_load_outcome_for_product; -pub use codex_core_skills::injection; -pub use codex_core_skills::injection::SkillInjections; -pub use codex_core_skills::injection::build_skill_injections; -pub use codex_core_skills::injection::collect_explicit_skill_mentions; -pub use codex_core_skills::loader; -pub use codex_core_skills::manager; -pub use codex_core_skills::model; -pub use codex_core_skills::remote; -pub use codex_core_skills::render; -pub use codex_core_skills::render::SkillRenderSideEffects; -pub use codex_core_skills::system; diff --git a/vendor/codex/codex-rs/core/src/skills_watcher.rs b/vendor/codex/codex-rs/core/src/skills_watcher.rs deleted file mode 100644 index cd4eed18..00000000 --- a/vendor/codex/codex-rs/core/src/skills_watcher.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Compatibility marker for the disabled Codex skill watcher. - -pub(crate) struct SkillsWatcher; - -impl SkillsWatcher { - pub(crate) fn noop() -> Self { - Self - } -} diff --git a/vendor/codex/codex-rs/core/src/state/service.rs b/vendor/codex/codex-rs/core/src/state/service.rs index 0d283650..5f03eb7e 100644 --- a/vendor/codex/codex-rs/core/src/state/service.rs +++ b/vendor/codex/codex-rs/core/src/state/service.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::sync::Arc; use crate::RolloutRecorder; -use crate::SkillsManager; use crate::agent::AgentControl; use crate::client::ModelClient; use crate::config::StartedNetworkProxy; @@ -10,7 +9,6 @@ use crate::exec_policy::ExecPolicyManager; use crate::guardian::GuardianRejection; use crate::guardian::GuardianRejectionCircuitBreaker; use crate::mcp::McpManager; -use crate::skills_watcher::SkillsWatcher; use crate::tools::code_mode::CodeModeService; use crate::tools::network_approval::NetworkApprovalService; use crate::tools::sandboxing::ApprovalStore; @@ -55,9 +53,7 @@ pub(crate) struct SessionServices { pub(crate) guardian_rejections: Mutex>, pub(crate) guardian_rejection_circuit_breaker: Mutex, pub(crate) runtime_handle: Handle, - pub(crate) skills_manager: Arc, pub(crate) mcp_manager: Arc, - pub(crate) skills_watcher: Arc, pub(crate) agent_control: AgentControl, pub(crate) network_proxy: Option, pub(crate) network_approval: Arc, diff --git a/vendor/codex/codex-rs/core/src/thread_manager.rs b/vendor/codex/codex-rs/core/src/thread_manager.rs index 19683987..76f1b4ac 100644 --- a/vendor/codex/codex-rs/core/src/thread_manager.rs +++ b/vendor/codex/codex-rs/core/src/thread_manager.rs @@ -1,4 +1,3 @@ -use crate::SkillsManager; use crate::agent::AgentControl; use crate::codex_thread::CodexThread; use crate::config::Config; @@ -10,7 +9,6 @@ use crate::session::CodexSpawnArgs; use crate::session::CodexSpawnOk; use crate::session::INITIAL_SUBMIT_ID; use crate::shell_snapshot::ShellSnapshot; -use crate::skills_watcher::SkillsWatcher; use crate::tasks::interrupted_turn_history_marker; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::ThreadHistoryBuilder; @@ -42,7 +40,6 @@ use codex_protocol::protocol::TurnAbortReason; use codex_protocol::protocol::TurnAbortedEvent; use codex_protocol::protocol::W3cTraceContext; use codex_state::DirectionalThreadSpawnEdgeStatus; -use codex_utils_absolute_path::AbsolutePathBuf; use futures::StreamExt; use futures::stream::FuturesUnordered; use std::collections::HashMap; @@ -85,10 +82,6 @@ impl Drop for TempCodexHomeGuard { } } -fn build_skills_watcher(_skills_manager: Arc) -> Arc { - Arc::new(SkillsWatcher::noop()) -} - /// Represents a newly created Codex thread (formerly called a conversation), including the first event /// (which is [`EventMsg::SessionConfigured`]). pub struct NewThread { @@ -165,9 +158,7 @@ pub(crate) struct ThreadManagerState { auth_manager: Arc, models_manager: Arc, environment_manager: Arc, - skills_manager: Arc, mcp_manager: Arc, - skills_watcher: Arc, session_source: SessionSource, analytics_events_client: Option, // Captures submitted ops for testing purpose when test mode is enabled. @@ -203,16 +194,8 @@ impl ThreadManager { environment_manager: Arc, analytics_events_client: Option, ) -> Self { - let codex_home = config.codex_home.clone(); - let restriction_product = session_source.restriction_product(); let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new_with_restriction_product( - codex_home, - config.bundled_skills_enabled(), - restriction_product, - )); - let skills_watcher = build_skills_watcher(Arc::clone(&skills_manager)); Self { state: Arc::new(ThreadManagerState { threads: Arc::new(RwLock::new(HashMap::new())), @@ -223,9 +206,7 @@ impl ThreadManager { collaboration_modes_config, ), environment_manager, - skills_manager, mcp_manager, - skills_watcher, auth_manager, session_source, analytics_events_client, @@ -269,19 +250,8 @@ impl ThreadManager { ) -> Self { set_thread_manager_test_mode_for_tests(/*enabled*/ true); let auth_manager = AuthManager::from_auth_for_testing(auth); - let skills_codex_home = match AbsolutePathBuf::from_absolute_path_checked(&codex_home) { - Ok(codex_home) => codex_home, - Err(err) => panic!("test codex_home should be absolute: {err}"), - }; let (thread_created_tx, _) = broadcast::channel(THREAD_CREATED_CHANNEL_CAPACITY); - let restriction_product = SessionSource::Exec.restriction_product(); let mcp_manager = Arc::new(McpManager::new()); - let skills_manager = Arc::new(SkillsManager::new_with_restriction_product( - skills_codex_home, - /*bundled_skills_enabled*/ true, - restriction_product, - )); - let skills_watcher = build_skills_watcher(Arc::clone(&skills_manager)); Self { state: Arc::new(ThreadManagerState { threads: Arc::new(RwLock::new(HashMap::new())), @@ -292,9 +262,7 @@ impl ThreadManager { provider, )), environment_manager, - skills_manager, mcp_manager, - skills_watcher, auth_manager, session_source: SessionSource::Exec, analytics_events_client: None, @@ -313,10 +281,6 @@ impl ThreadManager { self.state.auth_manager.clone() } - pub fn skills_manager(&self) -> Arc { - self.state.skills_manager.clone() - } - pub fn mcp_manager(&self) -> Arc { self.state.mcp_manager.clone() } @@ -871,9 +835,7 @@ impl ThreadManagerState { auth_manager, models_manager: Arc::clone(&self.models_manager), environment_manager: Arc::clone(&self.environment_manager), - skills_manager: Arc::clone(&self.skills_manager), mcp_manager: Arc::clone(&self.mcp_manager), - skills_watcher: Arc::clone(&self.skills_watcher), conversation_history: initial_history, session_source, agent_control, diff --git a/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs b/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs index 5c2080c7..bc0993d1 100644 --- a/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs +++ b/vendor/codex/codex-rs/tui/src/bottom_pane/mod.rs @@ -1210,7 +1210,6 @@ mod tests { use crate::test_support::PathBufExt; use crate::test_support::test_path_buf; use codex_protocol::protocol::Op; - use codex_protocol::protocol::SkillScope; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind;