diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index fb51ab25a..dc5dd6ad6 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2988,6 +2988,20 @@ ], "type": "object" }, + "SkillsExtraRootsSetParams": { + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "type": "object" + }, "SkillsListParams": { "properties": { "cwds": { @@ -4783,6 +4797,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index b78690a4f..92549d74c 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -709,6 +709,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -15121,6 +15145,27 @@ "title": "SkillsConfigWriteResponse", "type": "object" }, + "SkillsExtraRootsSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" + }, + "SkillsExtraRootsSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" + }, "SkillsListEntry": { "properties": { "cwd": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index b74ed83f8..d266a3e97 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1457,6 +1457,30 @@ "title": "Skills/listRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "skills/extraRoots/set" + ], + "title": "Skills/extraRoots/setRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SkillsExtraRootsSetParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Skills/extraRoots/setRequest", + "type": "object" + }, { "properties": { "id": { @@ -12945,6 +12969,27 @@ "title": "SkillsConfigWriteResponse", "type": "object" }, + "SkillsExtraRootsSetParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" + }, + "SkillsExtraRootsSetResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" + }, "SkillsListEntry": { "properties": { "cwd": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json new file mode 100644 index 000000000..9992cdf8b --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetParams.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AbsolutePathBuf": { + "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", + "type": "string" + } + }, + "properties": { + "extraRoots": { + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + } + }, + "required": [ + "extraRoots" + ], + "title": "SkillsExtraRootsSetParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json new file mode 100644 index 000000000..cf0792c74 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SkillsExtraRootsSetResponse.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SkillsExtraRootsSetResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 86ba2cd17..e19ac2dfd 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -57,6 +57,7 @@ import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; +import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; @@ -85,4 +86,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts new file mode 100644 index 000000000..bcddb9f6f --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type SkillsExtraRootsSetParams = { extraRoots: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts new file mode 100644 index 000000000..63be08186 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SkillsExtraRootsSetResponse.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type SkillsExtraRootsSetResponse = Record; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 0b7316fc4..66b67fb1b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -344,6 +344,8 @@ export type { SkillToolDependency } from "./SkillToolDependency"; export type { SkillsChangedNotification } from "./SkillsChangedNotification"; export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams"; export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse"; +export type { SkillsExtraRootsSetParams } from "./SkillsExtraRootsSetParams"; +export type { SkillsExtraRootsSetResponse } from "./SkillsExtraRootsSetResponse"; export type { SkillsListEntry } from "./SkillsListEntry"; export type { SkillsListParams } from "./SkillsListParams"; export type { SkillsListResponse } from "./SkillsListResponse"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index a8bec6b40..71c89a185 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -610,6 +610,11 @@ client_request_definitions! { serialization: global_shared_read("config"), response: v2::SkillsListResponse, }, + SkillsExtraRootsSet => "skills/extraRoots/set" { + params: v2::SkillsExtraRootsSetParams, + serialization: global("config"), + response: v2::SkillsExtraRootsSetResponse, + }, HooksList => "hooks/list" { params: v2::HooksListParams, serialization: global("config"), @@ -1721,6 +1726,17 @@ mod tests { Some(ClientRequestSerializationScope::GlobalSharedRead("config")) ); + let skills_extra_roots_set = ClientRequest::SkillsExtraRootsSet { + request_id: request_id(), + params: v2::SkillsExtraRootsSetParams { + extra_roots: vec![absolute_path("/tmp/skills")], + }, + }; + assert_eq!( + skills_extra_roots_set.serialization_scope(), + Some(ClientRequestSerializationScope::Global("config")) + ); + let plugin_list = ClientRequest::PluginList { request_id: request_id(), params: v2::PluginListParams { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index 70d283994..1b7e8ba8f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -35,6 +35,18 @@ pub struct SkillsListResponse { pub data: Vec, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsExtraRootsSetParams { + pub extra_roots: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SkillsExtraRootsSetResponse {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 7d57f6e09..a0be5cb15 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2626,6 +2626,27 @@ fn skills_list_params_serialization_uses_force_reload() { ); } +#[test] +fn skills_extra_roots_set_params_serialization_uses_extra_roots() { + assert_eq!( + serde_json::to_value(SkillsExtraRootsSetParams { + extra_roots: vec![absolute_path("tmp/skills")], + }) + .unwrap(), + json!({ + "extraRoots": [absolute_path_string("tmp/skills")], + }), + ); +} + +#[test] +fn skills_extra_roots_set_params_rejects_relative_roots() { + let result = serde_json::from_value::(json!({ + "extraRoots": ["relative/path"], + })); + assert!(result.is_err()); +} + #[test] fn plugin_source_serializes_local_git_and_remote_variants() { let local_path = if cfg!(windows) { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index ad3767bb7..2bf6035b2 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -196,6 +196,7 @@ Example with notification opt-out: - `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment. - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). +- `skills/extraRoots/set` — replace the app-server process runtime extra standalone skill roots. The roots are not persisted; missing directories are accepted and simply load no skills. - `hooks/list` — list discovered hooks for one or more `cwd` values. - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. - `marketplace/remove` — remove a configured marketplace by name from the user marketplace config, and delete its installed marketplace root when one exists. @@ -1506,6 +1507,7 @@ $skill-creator Add a new skill for triaging flaky CI and include step-by-step us Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, with `forceReload`). `skills/list` might reuse a cached skills result per `cwd`; setting `forceReload` to `true` refreshes the result from disk. The server also emits `skills/changed` notifications when watched local skill files change. Treat this as an invalidation signal and re-run `skills/list` with your current params when needed. +Use `skills/extraRoots/set` to replace additional standalone skill roots for the current app-server process. These roots use the same layout as other standalone skill roots: each root contains skill directories, and each skill directory contains `SKILL.md`. Missing roots are accepted and load no skills until they exist. This setting is lost when app-server exits. ```json { "method": "skills/list", "id": 25, "params": { @@ -1542,12 +1544,23 @@ The server also emits `skills/changed` notifications when watched local skill fi } ``` +```json +{ + "method": "skills/extraRoots/set", + "id": 26, + "params": { + "extraRoots": ["/Users/me/generated-skills"] + } +} +{ "id": 26, "result": {} } +``` + To enable or disable a skill by absolute path: ```json { "method": "skills/config/write", - "id": 26, + "id": 27, "params": { "path": "/Users/alice/.codex/skills/skill-creator/SKILL.md", "name": null, @@ -1561,7 +1574,7 @@ To enable or disable a skill by name: ```json { "method": "skills/config/write", - "id": 27, + "id": 28, "params": { "path": null, "name": "github:yeet", diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 33b9c56d4..2dbd17a3d 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -354,6 +354,8 @@ impl MessageProcessor { app_list_shutdown_token, ); let catalog_processor = CatalogRequestProcessor::new( + outgoing.clone(), + Arc::clone(&skills_watcher), auth_manager.clone(), Arc::clone(&thread_manager), Arc::clone(&config), @@ -1108,6 +1110,9 @@ impl MessageProcessor { ClientRequest::SkillsList { params, .. } => { self.catalog_processor.skills_list(params).await } + ClientRequest::SkillsExtraRootsSet { params, .. } => { + self.catalog_processor.skills_extra_roots_set(params).await + } ClientRequest::HooksList { params, .. } => { self.catalog_processor.hooks_list(params).await } diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 8408a3f67..a110b7319 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -156,6 +156,8 @@ use codex_app_server_protocol::ServerRequestResolvedNotification; use codex_app_server_protocol::SkillSummary; use codex_app_server_protocol::SkillsConfigWriteParams; use codex_app_server_protocol::SkillsConfigWriteResponse; +use codex_app_server_protocol::SkillsExtraRootsSetParams; +use codex_app_server_protocol::SkillsExtraRootsSetResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::SortDirection; diff --git a/codex-rs/app-server/src/request_processors/catalog_processor.rs b/codex-rs/app-server/src/request_processors/catalog_processor.rs index ba6c53566..6f17e4614 100644 --- a/codex-rs/app-server/src/request_processors/catalog_processor.rs +++ b/codex-rs/app-server/src/request_processors/catalog_processor.rs @@ -4,6 +4,8 @@ use futures::StreamExt; #[derive(Clone)] pub(crate) struct CatalogRequestProcessor { + pub(super) outgoing: Arc, + pub(super) skills_watcher: Arc, pub(super) auth_manager: Arc, pub(super) thread_manager: Arc, pub(super) config: Arc, @@ -96,6 +98,8 @@ fn errors_to_info( impl CatalogRequestProcessor { pub(crate) fn new( + outgoing: Arc, + skills_watcher: Arc, auth_manager: Arc, thread_manager: Arc, config: Arc, @@ -103,6 +107,8 @@ impl CatalogRequestProcessor { workspace_settings_cache: Arc, ) -> Self { Self { + outgoing, + skills_watcher, auth_manager, thread_manager, config, @@ -138,6 +144,15 @@ impl CatalogRequestProcessor { .map(|response| Some(response.into())) } + pub(crate) async fn skills_extra_roots_set( + &self, + params: SkillsExtraRootsSetParams, + ) -> Result, JSONRPCErrorError> { + self.skills_extra_roots_set_response(params) + .await + .map(|response| Some(response.into())) + } + pub(crate) async fn model_list( &self, params: ModelListParams, @@ -567,6 +582,24 @@ impl CatalogRequestProcessor { Ok(SkillsListResponse { data }) } + async fn skills_extra_roots_set_response( + &self, + params: SkillsExtraRootsSetParams, + ) -> Result { + let SkillsExtraRootsSetParams { extra_roots } = params; + self.skills_watcher + .register_runtime_extra_roots(&extra_roots); + self.thread_manager + .skills_manager() + .set_extra_roots(extra_roots); + self.outgoing + .send_server_notification(ServerNotification::SkillsChanged( + codex_app_server_protocol::SkillsChangedNotification {}, + )) + .await; + Ok(SkillsExtraRootsSetResponse {}) + } + /// Handle `hooks/list` by resolving hooks for each requested cwd. async fn hooks_list_response( &self, diff --git a/codex-rs/app-server/src/skills_watcher.rs b/codex-rs/app-server/src/skills_watcher.rs index 9e34cdac8..57edb89c7 100644 --- a/codex-rs/app-server/src/skills_watcher.rs +++ b/codex-rs/app-server/src/skills_watcher.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use crate::outgoing_message::OutgoingMessageSender; @@ -15,6 +16,7 @@ use codex_file_watcher::ThrottledWatchReceiver; use codex_file_watcher::WatchPath; use codex_file_watcher::WatchRegistration; use codex_protocol::protocol::TurnEnvironmentSelection; +use codex_utils_absolute_path::AbsolutePathBuf; use tokio_util::sync::CancellationToken; use tokio_util::sync::DropGuard; use tracing::warn; @@ -26,6 +28,7 @@ const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_millis(50); pub(crate) struct SkillsWatcher { subscriber: FileWatcherSubscriber, + runtime_extra_roots_registration: Mutex, shutdown_token: CancellationToken, _shutdown_drop_guard: DropGuard, } @@ -48,6 +51,7 @@ impl SkillsWatcher { Self::spawn_event_loop(rx, skills_manager, outgoing, shutdown_token.child_token()); Arc::new(Self { subscriber, + runtime_extra_roots_registration: Mutex::new(WatchRegistration::default()), shutdown_token, _shutdown_drop_guard: shutdown_drop_guard, }) @@ -57,6 +61,22 @@ impl SkillsWatcher { self.shutdown_token.cancel(); } + pub(crate) fn register_runtime_extra_roots(&self, extra_roots: &[AbsolutePathBuf]) { + let roots = extra_roots + .iter() + .map(|root| WatchPath { + path: root.clone().into_path_buf(), + recursive: true, + }) + .collect(); + let registration = self.subscriber.register_paths(roots); + let mut guard = self + .runtime_extra_roots_registration + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *guard = registration; + } + pub(crate) async fn register_thread_config( &self, config: &Config, diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 2d2c26b5a..d3bbfbf18 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -71,6 +71,7 @@ use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewStartParams; use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; use codex_app_server_protocol::ServerRequest; +use codex_app_server_protocol::SkillsExtraRootsSetParams; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::ThreadArchiveParams; use codex_app_server_protocol::ThreadCompactStartParams; @@ -675,6 +676,15 @@ impl McpProcess { self.send_request("skills/list", params).await } + /// Send a `skills/extraRoots/set` JSON-RPC request. + pub async fn send_skills_extra_roots_set_request( + &mut self, + params: SkillsExtraRootsSetParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("skills/extraRoots/set", params).await + } + /// Send a `hooks/list` JSON-RPC request. pub async fn send_hooks_list_request( &mut self, diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 74100bc19..2ad0b2006 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -13,11 +13,14 @@ use codex_app_server_protocol::PluginListParams; use codex_app_server_protocol::PluginListResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SkillsChangedNotification; +use codex_app_server_protocol::SkillsExtraRootsSetParams; +use codex_app_server_protocol::SkillsExtraRootsSetResponse; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::SkillsListResponse; use codex_app_server_protocol::ThreadStartParams; use codex_config::types::AuthCredentialsStoreMode; use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR; +use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; use tokio::time::timeout; @@ -40,6 +43,23 @@ fn write_skill(root: &TempDir, name: &str) -> Result<()> { Ok(()) } +async fn expect_skills_changed_notification( + mcp: &mut McpProcess, + timeout_duration: Duration, +) -> Result<()> { + let notification = timeout( + timeout_duration, + mcp.read_stream_until_notification_message("skills/changed"), + ) + .await??; + let params = notification + .params + .context("skills/changed params must be present")?; + let notification: SkillsChangedNotification = serde_json::from_value(params)?; + assert_eq!(notification, SkillsChangedNotification {}); + Ok(()) +} + fn write_plugins_enabled_config_with_base_url( codex_home: &std::path::Path, base_url: &str, @@ -573,6 +593,150 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> { Ok(()) } +#[tokio::test] +async fn skills_extra_roots_set_updates_process_runtime_roots() -> Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let extra_root = TempDir::new()?; + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + std::fs::create_dir_all(&skill_dir)?; + std::fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + )?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let set_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: vec![AbsolutePathBuf::from_absolute_path(&extra_skills_root)?], + }) + .await?; + let set_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(set_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(set_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + let missing_root = extra_root.path().join("missing-skills"); + let reset_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: vec![AbsolutePathBuf::from_absolute_path(&missing_root)?], + }) + .await?; + let reset_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(reset_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(reset_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let clear_request_id = mcp + .send_skills_extra_roots_set_request(SkillsExtraRootsSetParams { + extra_roots: Vec::new(), + }) + .await?; + let clear_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(clear_request_id)), + ) + .await??; + let _: SkillsExtraRootsSetResponse = to_response(clear_response)?; + expect_skills_changed_notification(&mut mcp, DEFAULT_TIMEOUT).await?; + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + drop(mcp); + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + let skills_request_id = mcp + .send_skills_list_request(SkillsListParams { + cwds: vec![cwd.path().to_path_buf()], + force_reload: false, + }) + .await?; + let skills_response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(skills_request_id)), + ) + .await??; + let SkillsListResponse { data } = to_response(skills_response)?; + assert_eq!(data.len(), 1); + assert_eq!(data[0].errors, Vec::new()); + assert!( + data[0] + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + Ok(()) +} + #[tokio::test] async fn skills_changed_notification_is_emitted_after_skill_change() -> Result<()> { let server = create_mock_responses_server_repeating_assistant("Done").await; diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 00d1cbba1..8663737ee 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -235,6 +235,7 @@ pub(crate) async fn skill_roots( config_layer_stack: &ConfigLayerStack, cwd: &AbsolutePathBuf, plugin_skill_roots: Vec, + extra_skill_roots: Vec, ) -> Vec { let home_dir = home_dir().and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok()); @@ -244,6 +245,7 @@ pub(crate) async fn skill_roots( cwd, home_dir.as_ref(), plugin_skill_roots, + extra_skill_roots, ) .await } @@ -254,6 +256,7 @@ async fn skill_roots_with_home_dir( cwd: &AbsolutePathBuf, home_dir: Option<&AbsolutePathBuf>, plugin_skill_roots: Vec, + extra_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(|root| SkillRoot { @@ -263,6 +266,13 @@ async fn skill_roots_with_home_dir( plugin_id: Some(root.plugin_id), plugin_root: Some(root.plugin_root), })); + roots.extend(extra_skill_roots.into_iter().map(|path| SkillRoot { + path, + scope: SkillScope::User, + file_system: Arc::clone(&LOCAL_FS), + plugin_id: None, + plugin_root: None, + })); roots.extend(repo_agents_skill_roots(fs, config_layer_stack, cwd).await); dedupe_skill_roots_by_path(&mut roots); roots @@ -1051,7 +1061,15 @@ pub(crate) async fn skill_roots_from_layer_stack( cwd: &AbsolutePathBuf, home_dir: Option<&AbsolutePathBuf>, ) -> Vec { - skill_roots_with_home_dir(Some(fs), config_layer_stack, cwd, home_dir, Vec::new()).await + skill_roots_with_home_dir( + Some(fs), + config_layer_stack, + cwd, + home_dir, + Vec::new(), + Vec::new(), + ) + .await } #[cfg(test)] diff --git a/codex-rs/core-skills/src/loader_tests.rs b/codex-rs/core-skills/src/loader_tests.rs index 84867ca66..69e357d93 100644 --- a/codex-rs/core-skills/src/loader_tests.rs +++ b/codex-rs/core-skills/src/loader_tests.rs @@ -1900,6 +1900,7 @@ async fn skill_roots_include_admin_with_lowest_priority() { &cfg.config_layer_stack, &cfg.cwd, Vec::new(), + Vec::new(), ) .await .into_iter() diff --git a/codex-rs/core-skills/src/manager.rs b/codex-rs/core-skills/src/manager.rs index db19322ac..185eaa932 100644 --- a/codex-rs/core-skills/src/manager.rs +++ b/codex-rs/core-skills/src/manager.rs @@ -51,6 +51,7 @@ impl SkillsLoadInput { pub struct SkillsManager { codex_home: AbsolutePathBuf, restriction_product: Option, + extra_roots: RwLock>, cache_by_cwd: RwLock>, cache_by_config: RwLock>, } @@ -68,6 +69,7 @@ impl SkillsManager { let manager = Self { codex_home, restriction_product, + extra_roots: RwLock::new(Vec::new()), cache_by_cwd: RwLock::new(HashMap::new()), cache_by_config: RwLock::new(HashMap::new()), }; @@ -81,6 +83,17 @@ impl SkillsManager { manager } + pub fn set_extra_roots(&self, extra_roots: Vec) { + { + let mut roots = self + .extra_roots + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + *roots = extra_roots; + } + self.clear_cache(); + } + /// Load skills for an already-constructed [`Config`], avoiding any additional config-layer /// loading. /// @@ -118,6 +131,7 @@ impl SkillsManager { &input.config_layer_stack, &input.cwd, input.effective_skill_roots.clone(), + self.extra_roots(), ) .await; if !input.bundled_skills_enabled { @@ -145,6 +159,7 @@ impl SkillsManager { &input.config_layer_stack, &input.cwd, input.effective_skill_roots.clone(), + self.extra_roots(), ) .await; if !bundled_skills_enabled_from_stack(&input.config_layer_stack) { @@ -214,6 +229,13 @@ impl SkillsManager { Err(err) => err.into_inner().get(cache_key).cloned(), } } + + fn extra_roots(&self) -> Vec { + match self.extra_roots.read() { + Ok(roots) => roots.clone(), + Err(err) => err.into_inner().clone(), + } + } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/codex-rs/core-skills/src/manager_tests.rs b/codex-rs/core-skills/src/manager_tests.rs index 4afa83e21..713b94d7b 100644 --- a/codex-rs/core-skills/src/manager_tests.rs +++ b/codex-rs/core-skills/src/manager_tests.rs @@ -221,6 +221,128 @@ async fn skills_for_config_reuses_cache_for_same_effective_config() { assert_eq!(outcome2.skills, outcome1.skills); } +#[tokio::test] +async fn set_extra_roots_replaces_runtime_roots_and_clears_cache() { + 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_input = SkillsLoadInput::new( + cwd.path().abs(), + Vec::new(), + config_layer_stack.clone(), + bundled_skills_enabled_from_stack(&config_layer_stack), + ); + let empty_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert!( + empty_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + fs::create_dir_all(&skill_dir).expect("create skill dir"); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + ) + .expect("write skill"); + skills_manager.set_extra_roots(vec![extra_skills_root.abs()]); + + let runtime_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert!( + runtime_outcome + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + skills_manager.set_extra_roots(vec![extra_root.path().join("missing-skills").abs()]); + let replaced_outcome = skills_manager + .skills_for_cwd( + &skills_input, + /*force_reload*/ false, + Some(Arc::clone(&LOCAL_FS)), + ) + .await; + assert_eq!(replaced_outcome.errors, Vec::new()); + assert!( + replaced_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); +} + +#[tokio::test] +async fn set_extra_roots_applies_to_config_loads_and_empty_clears() { + 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 empty_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + empty_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); + + let extra_skills_root = extra_root.path().join("skills"); + let skill_dir = extra_skills_root.join("runtime-skill"); + fs::create_dir_all(&skill_dir).expect("create skill dir"); + fs::write( + skill_dir.join("SKILL.md"), + "---\nname: runtime-skill\ndescription: runtime skill\n---\n\n# Body\n", + ) + .expect("write skill"); + skills_manager.set_extra_roots(vec![extra_skills_root.abs()]); + + let runtime_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + runtime_outcome + .skills + .iter() + .any(|skill| skill.name == "runtime-skill") + ); + + skills_manager.set_extra_roots(Vec::new()); + let cleared_outcome = + skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await; + assert!( + cleared_outcome + .skills + .iter() + .all(|skill| skill.name != "runtime-skill") + ); +} + #[tokio::test] async fn skills_for_config_disables_plugin_skills_by_name() { let codex_home = tempfile::tempdir().expect("tempdir");