From a845ffaff47fd1584d9f547fe72b933123109cb3 Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 30 Jun 2026 04:02:35 -0500 Subject: [PATCH 1/3] Complete the child-customization model (#285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add per-child enable/disable plus a symmetric agent/skill invocation matrix, completing the customization model. - Introduce `ChildCustomizationBase` with an optional `enabled` flag, shared by the five leaf child customizations (Agent/Skill/Prompt/Rule/ Hook); absent means enabled. McpServer and the container types keep their own required `enabled`. (Putting `enabled?` directly on `CustomizationBase` makes the client generators inherit the optional declaration and flip McpServer/containers to optional, disagreeing with the schema — a dedicated child base avoids that.) - Extend `session/customizationToggled` and every client reducer to toggle a top-level container OR an individual child by id. A child's effective state is `container.enabled && (child.enabled ?? true)`. - Add `disableUserInvocation` to `SkillCustomization`, and `disableModelInvocation` + `disableUserInvocation` to `AgentCustomization`. Regenerate the schema and all five client mirrors, update the customizations/actions guides and reference docs, refresh all six CHANGELOGs, and add reducer conformance fixtures (225/226). Verified across TypeScript, Rust, Go, Kotlin, and Swift. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 13 +++ clients/go/CHANGELOG.md | 10 ++ clients/go/ahp/reducers.go | 30 ++++++ clients/go/ahptypes/actions.generated.go | 19 ++-- clients/go/ahptypes/state.generated.go | 92 ++++++++++++++++-- clients/kotlin/CHANGELOG.md | 10 ++ .../microsoft/agenthostprotocol/Reducers.kt | 27 +++++- .../generated/Actions.generated.kt | 4 +- .../generated/State.generated.kt | 90 ++++++++++++++++- clients/rust/CHANGELOG.md | 11 +++ clients/rust/crates/ahp-types/src/actions.rs | 17 ++-- clients/rust/crates/ahp-types/src/state.rs | 80 ++++++++++++++++ clients/rust/crates/ahp/src/reducers.rs | 20 ++++ .../Generated/Actions.generated.swift | 4 +- .../Generated/State.generated.swift | 93 +++++++++++++++++- .../AgentHostProtocol/NativeReducer.swift | 37 +++++++ clients/swift/CHANGELOG.md | 10 ++ clients/typescript/CHANGELOG.md | 10 ++ docs/guide/actions.md | 4 +- docs/guide/customizations.md | 14 +-- schema/actions.schema.json | 76 ++++++++++++++- schema/commands.schema.json | 76 ++++++++++++++- schema/errors.schema.json | 70 ++++++++++++++ schema/notifications.schema.json | 70 ++++++++++++++ schema/state.schema.json | 70 ++++++++++++++ types/channels-session/actions.ts | 17 ++-- types/channels-session/reducer.ts | 29 +++++- types/channels-session/state.ts | 57 ++++++++++- ...tomizationtoggled-toggles-child-by-id.json | 96 +++++++++++++++++++ ...toggled-is-no-op-for-unknown-child-id.json | 61 ++++++++++++ 30 files changed, 1155 insertions(+), 62 deletions(-) create mode 100644 types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json create mode 100644 types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dfa7dac..e775ac93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,19 @@ changes accumulate. Track in-flight protocol changes via PRs touching - Optional `intention` field on `chat/toolCallStart` and every `ToolCallState` variant, providing a human-readable description of what the invocation intends to do. +- Optional `enabled` flag on the child customizations (`AgentCustomization`, + `SkillCustomization`, `PromptCustomization`, `RuleCustomization`, + `HookCustomization`) so an individual child can be turned off independently of + its container; absent means enabled. +- `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` + and `disableUserInvocation` on `AgentCustomization`, giving custom agents and + skills a symmetric user/model invocation matrix. + +### Changed + +- `session/customizationToggled` now targets a top-level container **or** an + individual child by `id` and sets that entry's `enabled`; the effective state + of a child is `container.enabled && (child.enabled ?? true)`. ## [0.5.1] — Unreleased diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index ad657835..49614c23 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -18,6 +18,16 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. - Optional `Intention` field on `ChatToolCallStartAction` and every tool-call lifecycle state. +- Optional `Enabled` field on the child customization types + (`AgentCustomization`, `SkillCustomization`, `PromptCustomization`, + `RuleCustomization`, `HookCustomization`). +- `DisableUserInvocation` on `SkillCustomization`, plus `DisableModelInvocation` + and `DisableUserInvocation` on `AgentCustomization`. + +### Changed + +- The `session/customizationToggled` reducer now toggles a top-level container + **or** an individual child by `id`, setting that entry's `Enabled`. ## [0.5.0] — 2026-06-26 diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index f3683231..5d1ef765 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -300,6 +300,23 @@ func setContainerEnabled(c *ahptypes.Customization, enabled bool) { } } +func setChildEnabled(c *ahptypes.ChildCustomization, enabled bool) { + switch v := c.Value.(type) { + case *ahptypes.AgentCustomization: + v.Enabled = &enabled + case *ahptypes.SkillCustomization: + v.Enabled = &enabled + case *ahptypes.PromptCustomization: + v.Enabled = &enabled + case *ahptypes.RuleCustomization: + v.Enabled = &enabled + case *ahptypes.HookCustomization: + v.Enabled = &enabled + case *ahptypes.McpServerCustomization: + v.Enabled = enabled + } +} + func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool { for i := range list { got, ok := customizationID(list[i]) @@ -308,6 +325,19 @@ func applyToggle(list []ahptypes.Customization, id string, enabled bool) bool { return true } } + for i := range list { + children := containerChildren(&list[i]) + if children == nil { + continue + } + for j := range *children { + got, ok := childCustomizationID((*children)[j]) + if ok && got == id { + setChildEnabled(&(*children)[j], enabled) + return true + } + } + } return false } diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index 97893439..a2987583 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -774,17 +774,20 @@ type SessionCustomizationsChangedAction struct { Customizations []Customization `json:"customizations"` } -// A client toggled a container customization on or off. -// -// Targets a top-level container (plugin or directory) by `id`. Only -// containers have an `enabled` flag; children are always active when -// their container is enabled. Is a no-op when no matching container is -// found. +// A client toggled a customization on or off. +// +// Targets either a top-level container (plugin or directory) or an +// individual child (a skill, agent, or other entry inside a container) by +// `id`, and sets that entry's `enabled` flag. Disabling a container still +// disables all of its children — the effective state of a child is +// `container.enabled && (child.enabled ?? true)` — so toggling a child +// only matters while its container is enabled. Is a no-op when no +// customization (container or child) has the given `id`. type SessionCustomizationToggledAction struct { Type ActionType `json:"type"` - // The id of the container to toggle. + // The id of the container or child to toggle. Id string `json:"id"` - // Whether to enable or disable the container. + // Whether to enable or disable the targeted customization. Enabled bool `json:"enabled"` } diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 7acc7adc..f4c6acee 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -2052,11 +2052,31 @@ type AgentCustomization struct { // customization is a subset of a larger file (for example, one entry // in an inline `mcpServers` block of a `plugins.json` manifest). // Absent when the customization covers the whole resource. - Range *TextRange `json:"range,omitempty"` - Type CustomizationType `json:"type"` + Range *TextRange `json:"range,omitempty"` + // Whether this child is individually enabled. Absent means enabled, so a + // producer only needs to set it to surface a child that exists but is + // turned off on its own. + // + // This flag is independent of the parent container's: the **effective** + // enabled state of a child is + // `container.enabled && (child.enabled ?? true)`, so a disabled container + // disables every child regardless of each child's own flag. + // + // A child is turned on or off by id with + // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + Enabled *bool `json:"enabled,omitempty"` + Type CustomizationType `json:"type"` // Short description of what the agent specializes in and when to // invoke it. Sourced from the agent file's frontmatter `description`. Description *string `json:"description,omitempty"` + // When `true`, the agent will not auto-delegate to this custom agent + // as a sub-agent; it can only be selected by the user. Absent or + // `false` means the agent may delegate to it. + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` + // When `true`, the user cannot select this custom agent (for example, + // in a picker); it remains available for the agent to auto-delegate + // to. Absent or `false` means the user may select it. + DisableUserInvocation *bool `json:"disableUserInvocation,omitempty"` // Additional provider-specific metadata for this custom agent. // // Mirrors the MCP `_meta` convention. @@ -2090,8 +2110,20 @@ type SkillCustomization struct { // customization is a subset of a larger file (for example, one entry // in an inline `mcpServers` block of a `plugins.json` manifest). // Absent when the customization covers the whole resource. - Range *TextRange `json:"range,omitempty"` - Type CustomizationType `json:"type"` + Range *TextRange `json:"range,omitempty"` + // Whether this child is individually enabled. Absent means enabled, so a + // producer only needs to set it to surface a child that exists but is + // turned off on its own. + // + // This flag is independent of the parent container's: the **effective** + // enabled state of a child is + // `container.enabled && (child.enabled ?? true)`, so a disabled container + // disables every child regardless of each child's own flag. + // + // A child is turned on or off by id with + // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + Enabled *bool `json:"enabled,omitempty"` + Type CustomizationType `json:"type"` // Short description used for help text and auto-invocation matching. // Sourced from the skill's frontmatter `description`. Description *string `json:"description,omitempty"` @@ -2099,6 +2131,10 @@ type SkillCustomization struct { // auto-invoke it. Sourced from the command skill's frontmatter // `disable-model-invocation` flag. DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` + // When `true`, the user cannot directly invoke this skill (for example, + // as a slash command); it remains available for the agent to + // auto-invoke. Absent or `false` means the user may invoke it. + DisableUserInvocation *bool `json:"disableUserInvocation,omitempty"` } // A prompt contributed by a plugin or directory. @@ -2123,8 +2159,20 @@ type PromptCustomization struct { // customization is a subset of a larger file (for example, one entry // in an inline `mcpServers` block of a `plugins.json` manifest). // Absent when the customization covers the whole resource. - Range *TextRange `json:"range,omitempty"` - Type CustomizationType `json:"type"` + Range *TextRange `json:"range,omitempty"` + // Whether this child is individually enabled. Absent means enabled, so a + // producer only needs to set it to surface a child that exists but is + // turned off on its own. + // + // This flag is independent of the parent container's: the **effective** + // enabled state of a child is + // `container.enabled && (child.enabled ?? true)`, so a disabled container + // disables every child regardless of each child's own flag. + // + // A child is turned on or off by id with + // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + Enabled *bool `json:"enabled,omitempty"` + Type CustomizationType `json:"type"` // Short description of what the prompt does. Description *string `json:"description,omitempty"` } @@ -2159,8 +2207,20 @@ type RuleCustomization struct { // customization is a subset of a larger file (for example, one entry // in an inline `mcpServers` block of a `plugins.json` manifest). // Absent when the customization covers the whole resource. - Range *TextRange `json:"range,omitempty"` - Type CustomizationType `json:"type"` + Range *TextRange `json:"range,omitempty"` + // Whether this child is individually enabled. Absent means enabled, so a + // producer only needs to set it to surface a child that exists but is + // turned off on its own. + // + // This flag is independent of the parent container's: the **effective** + // enabled state of a child is + // `container.enabled && (child.enabled ?? true)`, so a disabled container + // disables every child regardless of each child's own flag. + // + // A child is turned on or off by id with + // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + Enabled *bool `json:"enabled,omitempty"` + Type CustomizationType `json:"type"` // Description of what the rule enforces. Description *string `json:"description,omitempty"` // When `true`, the rule is always active (subject to `globs` if any). @@ -2194,8 +2254,20 @@ type HookCustomization struct { // customization is a subset of a larger file (for example, one entry // in an inline `mcpServers` block of a `plugins.json` manifest). // Absent when the customization covers the whole resource. - Range *TextRange `json:"range,omitempty"` - Type CustomizationType `json:"type"` + Range *TextRange `json:"range,omitempty"` + // Whether this child is individually enabled. Absent means enabled, so a + // producer only needs to set it to surface a child that exists but is + // turned off on its own. + // + // This flag is independent of the parent container's: the **effective** + // enabled state of a child is + // `container.enabled && (child.enabled ?? true)`, so a disabled container + // disables every child regardless of each child's own flag. + // + // A child is turned on or off by id with + // {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + Enabled *bool `json:"enabled,omitempty"` + Type CustomizationType `json:"type"` } // An MCP server contributed by a plugin or directory. diff --git a/clients/kotlin/CHANGELOG.md b/clients/kotlin/CHANGELOG.md index 4c817edd..ec6c0f36 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -19,6 +19,16 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump - Optional `intention` field on `ChatToolCallStartAction` and every tool-call lifecycle state. +- Optional `enabled` field on the child customization types + (`AgentCustomization`, `SkillCustomization`, `PromptCustomization`, + `RuleCustomization`, `HookCustomization`). +- `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` + and `disableUserInvocation` on `AgentCustomization`. + +### Changed + +- The `session/customizationToggled` reducer now toggles a top-level container + **or** an individual child by `id`, setting that entry's `enabled`. ## [0.5.0] — 2026-06-26 diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 7c3b221e..fde5abcf 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -211,6 +211,16 @@ private fun withCustomizationEnabled(c: Customization, enabled: Boolean): Custom is CustomizationUnknown -> c } +private fun withChildCustomizationEnabled(c: ChildCustomization, enabled: Boolean): ChildCustomization = when (c) { + is ChildCustomizationAgent -> ChildCustomizationAgent(c.value.copy(enabled = enabled)) + is ChildCustomizationSkill -> ChildCustomizationSkill(c.value.copy(enabled = enabled)) + is ChildCustomizationPrompt -> ChildCustomizationPrompt(c.value.copy(enabled = enabled)) + is ChildCustomizationRule -> ChildCustomizationRule(c.value.copy(enabled = enabled)) + is ChildCustomizationHook -> ChildCustomizationHook(c.value.copy(enabled = enabled)) + is ChildCustomizationMcpServer -> ChildCustomizationMcpServer(c.value.copy(enabled = enabled)) + is ChildCustomizationUnknown -> c +} + private fun childCustomizationId(c: ChildCustomization): String? = when (c) { is ChildCustomizationAgent -> c.value.id is ChildCustomizationSkill -> c.value.id @@ -543,10 +553,25 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val list = state.customizations if (list == null) state else { val idx = list.indexOfFirst { customizationId(it) == a.id } - if (idx < 0) state else { + if (idx >= 0) { val updated = list.toMutableList() updated[idx] = withCustomizationEnabled(updated[idx], a.enabled) state.copy(customizations = updated) + } else { + var changed = false + val updated = list.map { container -> + val children = customizationChildren(container) + if (children == null) container else { + val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } + if (childIdx < 0) container else { + changed = true + val newChildren = children.toMutableList() + newChildren[childIdx] = withChildCustomizationEnabled(newChildren[childIdx], a.enabled) + withCustomizationChildren(container, newChildren) + } + } + } + if (!changed) state else state.copy(customizations = updated) } } } diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index 70e01270..78fb3f3b 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -891,11 +891,11 @@ data class SessionCustomizationsChangedAction( data class SessionCustomizationToggledAction( val type: ActionType, /** - * The id of the container to toggle. + * The id of the container or child to toggle. */ val id: String, /** - * Whether to enable or disable the container. + * Whether to enable or disable the targeted customization. */ val enabled: Boolean ) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index 4b764b00..b081a673 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -2932,12 +2932,38 @@ data class AgentCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + val enabled: Boolean? = null, val type: CustomizationType, /** * Short description of what the agent specializes in and when to * invoke it. Sourced from the agent file's frontmatter `description`. */ val description: String? = null, + /** + * When `true`, the agent will not auto-delegate to this custom agent + * as a sub-agent; it can only be selected by the user. Absent or + * `false` means the agent may delegate to it. + */ + val disableModelInvocation: Boolean? = null, + /** + * When `true`, the user cannot select this custom agent (for example, + * in a picker); it remains available for the agent to auto-delegate + * to. Absent or `false` means the user may select it. + */ + val disableUserInvocation: Boolean? = null, /** * Additional provider-specific metadata for this custom agent. * @@ -2980,6 +3006,20 @@ data class SkillCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + val enabled: Boolean? = null, val type: CustomizationType, /** * Short description used for help text and auto-invocation matching. @@ -2991,7 +3031,13 @@ data class SkillCustomization( * auto-invoke it. Sourced from the command skill's frontmatter * `disable-model-invocation` flag. */ - val disableModelInvocation: Boolean? = null + val disableModelInvocation: Boolean? = null, + /** + * When `true`, the user cannot directly invoke this skill (for example, + * as a slash command); it remains available for the agent to + * auto-invoke. Absent or `false` means the user may invoke it. + */ + val disableUserInvocation: Boolean? = null ) @Serializable @@ -3027,6 +3073,20 @@ data class PromptCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + val enabled: Boolean? = null, val type: CustomizationType, /** * Short description of what the prompt does. @@ -3067,6 +3127,20 @@ data class RuleCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + val enabled: Boolean? = null, val type: CustomizationType, /** * Description of what the rule enforces. @@ -3118,6 +3192,20 @@ data class HookCustomization( * Absent when the customization covers the whole resource. */ val range: TextRange? = null, + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + val enabled: Boolean? = null, val type: CustomizationType ) diff --git a/clients/rust/CHANGELOG.md b/clients/rust/CHANGELOG.md index 6e4106d6..9bcccefa 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -19,6 +19,17 @@ matching `## [X.Y.Z]` heading is missing from this file. - Optional `intention` field on `ChatToolCallStartAction` and every tool-call lifecycle state. +- Optional `enabled` field on the child customization types + (`AgentCustomization`, `SkillCustomization`, `PromptCustomization`, + `RuleCustomization`, `HookCustomization`). +- `disable_user_invocation` on `SkillCustomization`, plus + `disable_model_invocation` and `disable_user_invocation` on + `AgentCustomization`. + +### Changed + +- The `session/customizationToggled` reducer now toggles a top-level container + **or** an individual child by `id`, setting that entry's `enabled`. ## [0.5.0] — 2026-06-26 diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 465462a1..de65f385 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -938,18 +938,21 @@ pub struct SessionCustomizationsChangedAction { pub customizations: Vec, } -/// A client toggled a container customization on or off. +/// A client toggled a customization on or off. /// -/// Targets a top-level container (plugin or directory) by `id`. Only -/// containers have an `enabled` flag; children are always active when -/// their container is enabled. Is a no-op when no matching container is -/// found. +/// Targets either a top-level container (plugin or directory) or an +/// individual child (a skill, agent, or other entry inside a container) by +/// `id`, and sets that entry's `enabled` flag. Disabling a container still +/// disables all of its children — the effective state of a child is +/// `container.enabled && (child.enabled ?? true)` — so toggling a child +/// only matters while its container is enabled. Is a no-op when no +/// customization (container or child) has the given `id`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCustomizationToggledAction { - /// The id of the container to toggle. + /// The id of the container or child to toggle. pub id: String, - /// Whether to enable or disable the container. + /// Whether to enable or disable the targeted customization. pub enabled: bool, } diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index dc39b10a..89e78d75 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2560,10 +2560,33 @@ pub struct AgentCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, /// Short description of what the agent specializes in and when to /// invoke it. Sourced from the agent file's frontmatter `description`. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, + /// When `true`, the agent will not auto-delegate to this custom agent + /// as a sub-agent; it can only be selected by the user. Absent or + /// `false` means the agent may delegate to it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, + /// When `true`, the user cannot select this custom agent (for example, + /// in a picker); it remains available for the agent to auto-delegate + /// to. Absent or `false` means the user may select it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disable_user_invocation: Option, /// Additional provider-specific metadata for this custom agent. /// /// Mirrors the MCP `_meta` convention. @@ -2603,6 +2626,19 @@ pub struct SkillCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, /// Short description used for help text and auto-invocation matching. /// Sourced from the skill's frontmatter `description`. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -2612,6 +2648,11 @@ pub struct SkillCustomization { /// `disable-model-invocation` flag. #[serde(default, skip_serializing_if = "Option::is_none")] pub disable_model_invocation: Option, + /// When `true`, the user cannot directly invoke this skill (for example, + /// as a slash command); it remains available for the agent to + /// auto-invoke. Absent or `false` means the user may invoke it. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub disable_user_invocation: Option, } /// A prompt contributed by a plugin or directory. @@ -2641,6 +2682,19 @@ pub struct PromptCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, /// Short description of what the prompt does. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -2681,6 +2735,19 @@ pub struct RuleCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, /// Description of what the rule enforces. #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -2722,6 +2789,19 @@ pub struct HookCustomization { /// Absent when the customization covers the whole resource. #[serde(default, skip_serializing_if = "Option::is_none")] pub range: Option, + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub enabled: Option, } /// An MCP server contributed by a plugin or directory. diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 9489a310..0622639e 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -403,11 +403,31 @@ fn set_container_enabled(c: &mut Customization, enabled: bool) { } } +fn set_child_enabled(c: &mut ChildCustomization, enabled: bool) { + match c { + ChildCustomization::Agent(x) => x.enabled = Some(enabled), + ChildCustomization::Skill(x) => x.enabled = Some(enabled), + ChildCustomization::Prompt(x) => x.enabled = Some(enabled), + ChildCustomization::Rule(x) => x.enabled = Some(enabled), + ChildCustomization::Hook(x) => x.enabled = Some(enabled), + ChildCustomization::McpServer(x) => x.enabled = enabled, + ChildCustomization::Unknown(_) => {} + } +} + fn apply_toggle(list: &mut [Customization], id: &str, enabled: bool) -> bool { if let Some(container) = list.iter_mut().find(|c| customization_id(c) == Some(id)) { set_container_enabled(container, enabled); return true; } + for container in list.iter_mut() { + if let Some(children) = container_children_mut(container) { + if let Some(child) = children.iter_mut().find(|c| child_id_of(c) == Some(id)) { + set_child_enabled(child, enabled); + return true; + } + } + } false } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index 557c910e..66299a40 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1139,9 +1139,9 @@ public struct SessionCustomizationsChangedAction: Codable, Sendable { public struct SessionCustomizationToggledAction: Codable, Sendable { public var type: ActionType - /// The id of the container to toggle. + /// The id of the container or child to toggle. public var id: String - /// Whether to enable or disable the container. + /// Whether to enable or disable the targeted customization. public var enabled: Bool public init( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 2c3a3c9c..29450374 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -3127,10 +3127,30 @@ public struct AgentCustomization: Codable, Sendable { /// in an inline `mcpServers` block of a `plugins.json` manifest). /// Absent when the customization covers the whole resource. public var range: TextRange? + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + public var enabled: Bool? public var type: CustomizationType /// Short description of what the agent specializes in and when to /// invoke it. Sourced from the agent file's frontmatter `description`. public var description: String? + /// When `true`, the agent will not auto-delegate to this custom agent + /// as a sub-agent; it can only be selected by the user. Absent or + /// `false` means the agent may delegate to it. + public var disableModelInvocation: Bool? + /// When `true`, the user cannot select this custom agent (for example, + /// in a picker); it remains available for the agent to auto-delegate + /// to. Absent or `false` means the user may select it. + public var disableUserInvocation: Bool? /// Additional provider-specific metadata for this custom agent. /// /// Mirrors the MCP `_meta` convention. @@ -3142,8 +3162,11 @@ public struct AgentCustomization: Codable, Sendable { case name case icons case range + case enabled case type case description + case disableModelInvocation + case disableUserInvocation case meta = "_meta" } @@ -3153,8 +3176,11 @@ public struct AgentCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil, + disableModelInvocation: Bool? = nil, + disableUserInvocation: Bool? = nil, meta: [String: AnyCodable]? = nil ) { self.id = id @@ -3162,8 +3188,11 @@ public struct AgentCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description + self.disableModelInvocation = disableModelInvocation + self.disableUserInvocation = disableUserInvocation self.meta = meta } } @@ -3190,6 +3219,18 @@ public struct SkillCustomization: Codable, Sendable { /// in an inline `mcpServers` block of a `plugins.json` manifest). /// Absent when the customization covers the whole resource. public var range: TextRange? + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + public var enabled: Bool? public var type: CustomizationType /// Short description used for help text and auto-invocation matching. /// Sourced from the skill's frontmatter `description`. @@ -3198,6 +3239,10 @@ public struct SkillCustomization: Codable, Sendable { /// auto-invoke it. Sourced from the command skill's frontmatter /// `disable-model-invocation` flag. public var disableModelInvocation: Bool? + /// When `true`, the user cannot directly invoke this skill (for example, + /// as a slash command); it remains available for the agent to + /// auto-invoke. Absent or `false` means the user may invoke it. + public var disableUserInvocation: Bool? public init( id: String, @@ -3205,18 +3250,22 @@ public struct SkillCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil, - disableModelInvocation: Bool? = nil + disableModelInvocation: Bool? = nil, + disableUserInvocation: Bool? = nil ) { self.id = id self.uri = uri self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description self.disableModelInvocation = disableModelInvocation + self.disableUserInvocation = disableUserInvocation } } @@ -3242,6 +3291,18 @@ public struct PromptCustomization: Codable, Sendable { /// in an inline `mcpServers` block of a `plugins.json` manifest). /// Absent when the customization covers the whole resource. public var range: TextRange? + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + public var enabled: Bool? public var type: CustomizationType /// Short description of what the prompt does. public var description: String? @@ -3252,6 +3313,7 @@ public struct PromptCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil ) { @@ -3260,6 +3322,7 @@ public struct PromptCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description } @@ -3287,6 +3350,18 @@ public struct RuleCustomization: Codable, Sendable { /// in an inline `mcpServers` block of a `plugins.json` manifest). /// Absent when the customization covers the whole resource. public var range: TextRange? + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + public var enabled: Bool? public var type: CustomizationType /// Description of what the rule enforces. public var description: String? @@ -3304,6 +3379,7 @@ public struct RuleCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil, alwaysApply: Bool? = nil, @@ -3314,6 +3390,7 @@ public struct RuleCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description self.alwaysApply = alwaysApply @@ -3343,6 +3420,18 @@ public struct HookCustomization: Codable, Sendable { /// in an inline `mcpServers` block of a `plugins.json` manifest). /// Absent when the customization covers the whole resource. public var range: TextRange? + /// Whether this child is individually enabled. Absent means enabled, so a + /// producer only needs to set it to surface a child that exists but is + /// turned off on its own. + /// + /// This flag is independent of the parent container's: the **effective** + /// enabled state of a child is + /// `container.enabled && (child.enabled ?? true)`, so a disabled container + /// disables every child regardless of each child's own flag. + /// + /// A child is turned on or off by id with + /// {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + public var enabled: Bool? public var type: CustomizationType public init( @@ -3351,6 +3440,7 @@ public struct HookCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType ) { self.id = id @@ -3358,6 +3448,7 @@ public struct HookCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type } } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift index a8e32bac..e3e10ef6 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift @@ -233,6 +233,32 @@ func setCustomizationEnabled(_ c: inout Customization, _ enabled: Bool) { } } +func setChildCustomizationEnabled(_ c: inout ChildCustomization, _ enabled: Bool) { + switch c { + case .agent(var x): + x.enabled = enabled + c = .agent(x) + case .skill(var x): + x.enabled = enabled + c = .skill(x) + case .prompt(var x): + x.enabled = enabled + c = .prompt(x) + case .rule(var x): + x.enabled = enabled + c = .rule(x) + case .hook(var x): + x.enabled = enabled + c = .hook(x) + case .mcpServer(var x): + x.enabled = enabled + c = .mcpServer(x) + // Unknown/future child customization: opaque payload, nothing to mutate. + case .unknown: + break + } +} + func toggleCustomization(in list: inout [Customization], id: String, enabled: Bool) -> Bool { for i in list.indices { if customizationId(list[i]) == id { @@ -242,5 +268,16 @@ func toggleCustomization(in list: inout [Customization], id: String, enabled: Bo return true } } + for containerIdx in list.indices { + var container = list[containerIdx] + guard var children = customizationChildren(container) else { continue } + guard let childIdx = children.firstIndex(where: { childId($0) == id }) else { continue } + var child = children[childIdx] + setChildCustomizationEnabled(&child, enabled) + children[childIdx] = child + setCustomizationChildren(&container, children) + list[containerIdx] = container + return true + } return false } diff --git a/clients/swift/CHANGELOG.md b/clients/swift/CHANGELOG.md index b8ede023..44705013 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -21,6 +21,16 @@ the tag matches the version pinned in [`VERSION`](VERSION). - Optional `intention` field on `ChatToolCallStartAction` and every tool-call lifecycle state. +- Optional `enabled` field on the child customization types + (`AgentCustomization`, `SkillCustomization`, `PromptCustomization`, + `RuleCustomization`, `HookCustomization`). +- `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` + and `disableUserInvocation` on `AgentCustomization`. + +### Changed + +- The `session/customizationToggled` reducer now toggles a top-level container + **or** an individual child by `id`, setting that entry's `enabled`. ## [0.5.0] — 2026-06-26 diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 2d2c744e..42174c48 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -24,6 +24,16 @@ hotfix escape hatch. - Optional `intention` field on `ChatToolCallStartAction` and every tool-call lifecycle state. +- Optional `enabled` field on the child customization types + (`AgentCustomization`, `SkillCustomization`, `PromptCustomization`, + `RuleCustomization`, `HookCustomization`). +- `disableUserInvocation` on `SkillCustomization`, plus `disableModelInvocation` + and `disableUserInvocation` on `AgentCustomization`. + +### Changed + +- The `session/customizationToggled` reducer now toggles a top-level container + **or** an individual child by `id`, setting that entry's `enabled`. ## [0.5.0] — 2026-06-26 diff --git a/docs/guide/actions.md b/docs/guide/actions.md index eeb59d11..c3c33a55 100644 --- a/docs/guide/actions.md +++ b/docs/guide/actions.md @@ -123,7 +123,7 @@ See [Elicitation](/guide/elicitation) for the request lifecycle. | Type | Client-dispatchable? | When | |---|---|---| | `session/customizationsChanged` | No | Server replaced the session's top-level customization list (full replacement) | -| `session/customizationToggled` | **Yes** | Client toggled a top-level container customization on or off by id | +| `session/customizationToggled` | **Yes** | Client toggled a container or child customization on or off by id | | `session/customizationUpdated` | No | Server upserted a top-level container (plugin or directory) by id (full-entry replacement, including children) | | `session/customizationRemoved` | No | Server removed a customization by id (containers cascade to children) | @@ -191,7 +191,7 @@ The client applies the action **optimistically** to its local state before sendi | `session/pendingMessageSet` | Stores a steering or queued message (upsert); if queued and idle, auto-starts a turn | | `session/pendingMessageRemoved` | Cancels a pending message before it is consumed | | `session/queuedMessagesReordered` | Reorders queued messages; unknown IDs ignored, unmentioned messages kept at end | -| `session/customizationToggled` | Toggles a top-level container customization on or off by id | +| `session/customizationToggled` | Toggles a container or child customization on or off by id | | `session/isReadChanged` | Marks the session as read or unread | | `session/isArchivedChanged` | Archives or unarchives the session | | `session/activityChanged` | Updates the session's current activity description | diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index f7809865..ba9347f6 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -104,19 +104,21 @@ stateDiagram-v2 ## Children -Every child carries the same base fields (`id`, `uri`, `name`, optional `icons`). Children are leaf nodes — no further nesting — and their parent is implied by which container holds them in its `children` array. Children have no `enabled` or `clientId`: only containers can be toggled, and client provenance lives on the container since clients can only contribute containers, not individual children. +Every child carries the same base fields (`id`, `uri`, `name`, optional `icons`) plus an `enabled` flag — optional for the five leaf children (absent means enabled) and always present on an `McpServerCustomization`. Children are leaf nodes — no further nesting — and their parent is implied by which container holds them in its `children` array. A child's `enabled` is independent of its container's. Children have no `clientId`: client provenance lives on the container since clients can only contribute containers, not individual children. Each child type carries optional metadata sourced from its [Open Plugins](https://open-plugins.com/plugin-builders/specification.md) component definition (typically the file's YAML frontmatter): ```typescript -AgentCustomization { type: 'agent'; description? } -SkillCustomization { type: 'skill'; description?, disableModelInvocation? } +AgentCustomization { type: 'agent'; description?, disableModelInvocation?, disableUserInvocation? } +SkillCustomization { type: 'skill'; description?, disableModelInvocation?, disableUserInvocation? } PromptCustomization { type: 'prompt'; description? } RuleCustomization { type: 'rule'; description?, alwaysApply?, globs? } // covers "instruction" formats too HookCustomization { type: 'hook'; event?, matcher? } McpServerCustomization { type: 'mcpServer'; enabled, state, channel?, mcpApp? } // see /guide/mcp ``` +Agents and skills carry a symmetric invocation matrix. `disableModelInvocation` removes the entry from the agent's automatic choices — a custom agent it won't auto-delegate to, or a skill it won't auto-invoke — while leaving it available for the user to pick. `disableUserInvocation` does the reverse: the entry stays available for the agent to invoke but is hidden from user-facing pickers and slash-commands. Both are absent/`false` by default (invocable by either party), and they are independent, so an entry can be agent-only, user-only, both, or neither. + The protocol intentionally omits host-internal execution details (a hook's command/script, an MCP server's `command`/`args`/`env`, etc.). Those stay on the agent host; clients see only what's needed for display, search, and selection. MCP tools and their descriptions surface through the standard tool channels once the server is running. The MCP-specific runtime fields (`state`, `channel`, `mcpApp`) are covered in [MCP Servers](/guide/mcp). Consumers filter by `type` to find the children they care about — for example, the agent picker reads every `AgentCustomization` under any container: @@ -129,17 +131,17 @@ state.customizations ## Toggling -Any client can enable or disable a top-level container by dispatching `session/customizationToggled` with the container's `id`: +Any client can enable or disable a container or an individual child by dispatching `session/customizationToggled` with that entry's `id`: ```typescript { type: 'session/customizationToggled' - id: string // container id + id: string // container or child id enabled: boolean } ``` -Only containers (plugins and directories) have an `enabled` flag — children are always active when their container is enabled. The action is a no-op if no container has that id. +Both containers and children carry an `enabled` flag. The reducer matches `id` against the top-level containers first, then the children inside every container, and sets that entry's `enabled`. A child's effective state is `container.enabled && (child.enabled ?? true)`, so disabling a container disables all of its children regardless of each child's own flag, and a child toggle only takes effect while its container is enabled. The action is a no-op if no container or child has that id. ```mermaid sequenceDiagram diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 656afa0f..74216f2e 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -427,18 +427,18 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", + "description": "A client toggled a customization on or off.\n\nTargets either a top-level container (plugin or directory) or an\nindividual child (a skill, agent, or other entry inside a container) by\n`id`, and sets that entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization (container or child) has the given `id`.", "properties": { "type": { "$ref": "#/$defs/ActionType.SessionCustomizationToggled" }, "id": { "type": "string", - "description": "The id of the container to toggle." + "description": "The id of the container or child to toggle." }, "enabled": { "type": "boolean", - "description": "Whether to enable or disable the container." + "description": "Whether to enable or disable the targeted customization." } }, "required": [ @@ -3349,6 +3349,44 @@ "writable" ] }, + "ChildCustomizationBase": { + "type": "object", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", + "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + } + }, + "required": [ + "id", + "uri", + "name" + ] + }, "AgentCustomization": { "type": "object", "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", @@ -3376,6 +3414,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Agent" }, @@ -3383,6 +3425,14 @@ "type": "string", "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -3423,6 +3473,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Skill" }, @@ -3433,6 +3487,10 @@ "disableModelInvocation": { "type": "boolean", "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } }, "required": [ @@ -3469,6 +3527,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Prompt" }, @@ -3511,6 +3573,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Rule" }, @@ -3564,6 +3630,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Hook" } diff --git a/schema/commands.schema.json b/schema/commands.schema.json index 7904915d..c5c488df 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -2682,6 +2682,44 @@ "writable" ] }, + "ChildCustomizationBase": { + "type": "object", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", + "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + } + }, + "required": [ + "id", + "uri", + "name" + ] + }, "AgentCustomization": { "type": "object", "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", @@ -2709,6 +2747,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Agent" }, @@ -2716,6 +2758,14 @@ "type": "string", "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -2756,6 +2806,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Skill" }, @@ -2766,6 +2820,10 @@ "disableModelInvocation": { "type": "boolean", "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } }, "required": [ @@ -2802,6 +2860,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Prompt" }, @@ -2844,6 +2906,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Rule" }, @@ -2897,6 +2963,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Hook" } @@ -5814,18 +5884,18 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "description": "A client toggled a container customization on or off.\n\nTargets a top-level container (plugin or directory) by `id`. Only\ncontainers have an `enabled` flag; children are always active when\ntheir container is enabled. Is a no-op when no matching container is\nfound.", + "description": "A client toggled a customization on or off.\n\nTargets either a top-level container (plugin or directory) or an\nindividual child (a skill, agent, or other entry inside a container) by\n`id`, and sets that entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization (container or child) has the given `id`.", "properties": { "type": { "$ref": "#/$defs/ActionType.SessionCustomizationToggled" }, "id": { "type": "string", - "description": "The id of the container to toggle." + "description": "The id of the container or child to toggle." }, "enabled": { "type": "boolean", - "description": "Whether to enable or disable the container." + "description": "Whether to enable or disable the targeted customization." } }, "required": [ diff --git a/schema/errors.schema.json b/schema/errors.schema.json index 7344bc0a..1169e09b 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1546,6 +1546,44 @@ "writable" ] }, + "ChildCustomizationBase": { + "type": "object", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", + "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + } + }, + "required": [ + "id", + "uri", + "name" + ] + }, "AgentCustomization": { "type": "object", "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", @@ -1573,6 +1611,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Agent" }, @@ -1580,6 +1622,14 @@ "type": "string", "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1620,6 +1670,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Skill" }, @@ -1630,6 +1684,10 @@ "disableModelInvocation": { "type": "boolean", "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } }, "required": [ @@ -1666,6 +1724,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Prompt" }, @@ -1708,6 +1770,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Rule" }, @@ -1761,6 +1827,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Hook" } diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 6fa039a7..098114ca 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1706,6 +1706,44 @@ "writable" ] }, + "ChildCustomizationBase": { + "type": "object", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", + "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + } + }, + "required": [ + "id", + "uri", + "name" + ] + }, "AgentCustomization": { "type": "object", "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", @@ -1733,6 +1771,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Agent" }, @@ -1740,6 +1782,14 @@ "type": "string", "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1780,6 +1830,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Skill" }, @@ -1790,6 +1844,10 @@ "disableModelInvocation": { "type": "boolean", "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } }, "required": [ @@ -1826,6 +1884,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Prompt" }, @@ -1868,6 +1930,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Rule" }, @@ -1921,6 +1987,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Hook" } diff --git a/schema/state.schema.json b/schema/state.schema.json index 05dd918f..d2d54209 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1457,6 +1457,44 @@ "writable" ] }, + "ChildCustomizationBase": { + "type": "object", + "description": "Fields shared by the leaf child customizations that live inside a\ncontainer — {@link AgentCustomization}, {@link SkillCustomization},\n{@link PromptCustomization}, {@link RuleCustomization}, and\n{@link HookCustomization}.\n\n{@link McpServerCustomization} is also a child but does not extend this\nbase: it always carries an explicit {@link McpServerCustomization.enabled}\nbecause it can appear as a top-level customization too.", + "properties": { + "id": { + "type": "string", + "description": "Session-unique opaque identifier. Used by every action that targets a\nspecific customization. Minted by whoever publishes the customization\n(typically the agent host)." + }, + "uri": { + "$ref": "#/$defs/URI", + "description": "Source URI for this customization. A plugin URL, a file URI, or a\ndirectory URI.\n\nFor declarations that live inside a larger file — e.g. an MCP\nserver declared inline in a `plugins.json` manifest — `uri` points\nto the containing file and {@link CustomizationBase.range | `range`}\nnarrows it to the declaration's span." + }, + "name": { + "type": "string", + "description": "Human-readable name." + }, + "icons": { + "type": "array", + "items": { + "$ref": "#/$defs/Icon" + }, + "description": "Icons for UI display." + }, + "range": { + "$ref": "#/$defs/TextRange", + "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." + }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + } + }, + "required": [ + "id", + "uri", + "name" + ] + }, "AgentCustomization": { "type": "object", "description": "A custom agent contributed by a plugin or directory.\n\nMirrors the [Open Plugins agent](https://open-plugins.com/agent-builders/components/agents)\nformat: a markdown file with YAML frontmatter, where the body is the\nagent's system prompt.", @@ -1484,6 +1522,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Agent" }, @@ -1491,6 +1533,14 @@ "type": "string", "description": "Short description of what the agent specializes in and when to\ninvoke it. Sourced from the agent file's frontmatter `description`." }, + "disableModelInvocation": { + "type": "boolean", + "description": "When `true`, the agent will not auto-delegate to this custom agent\nas a sub-agent; it can only be selected by the user. Absent or\n`false` means the agent may delegate to it." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot select this custom agent (for example,\nin a picker); it remains available for the agent to auto-delegate\nto. Absent or `false` means the user may select it." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1531,6 +1581,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Skill" }, @@ -1541,6 +1595,10 @@ "disableModelInvocation": { "type": "boolean", "description": "When `true`, only the user can invoke this skill — the agent will not\nauto-invoke it. Sourced from the command skill's frontmatter\n`disable-model-invocation` flag." + }, + "disableUserInvocation": { + "type": "boolean", + "description": "When `true`, the user cannot directly invoke this skill (for example,\nas a slash command); it remains available for the agent to\nauto-invoke. Absent or `false` means the user may invoke it." } }, "required": [ @@ -1577,6 +1635,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Prompt" }, @@ -1619,6 +1681,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Rule" }, @@ -1672,6 +1738,10 @@ "$ref": "#/$defs/TextRange", "description": "Optional span within {@link CustomizationBase.uri | `uri`} when this\ncustomization is a subset of a larger file (for example, one entry\nin an inline `mcpServers` block of a `plugins.json` manifest).\nAbsent when the customization covers the whole resource." }, + "enabled": { + "type": "boolean", + "description": "Whether this child is individually enabled. Absent means enabled, so a\nproducer only needs to set it to surface a child that exists but is\nturned off on its own.\n\nThis flag is independent of the parent container's: the **effective**\nenabled state of a child is\n`container.enabled && (child.enabled ?? true)`, so a disabled container\ndisables every child regardless of each child's own flag.\n\nA child is turned on or off by id with\n{@link SessionCustomizationToggledAction | `session/customizationToggled`}." + }, "type": { "$ref": "#/$defs/CustomizationType.Hook" } diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index 13c9db21..f84acd0d 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -272,12 +272,15 @@ export interface SessionCustomizationsChangedAction { } /** - * A client toggled a container customization on or off. + * A client toggled a customization on or off. * - * Targets a top-level container (plugin or directory) by `id`. Only - * containers have an `enabled` flag; children are always active when - * their container is enabled. Is a no-op when no matching container is - * found. + * Targets either a top-level container (plugin or directory) or an + * individual child (a skill, agent, or other entry inside a container) by + * `id`, and sets that entry's `enabled` flag. Disabling a container still + * disables all of its children — the effective state of a child is + * `container.enabled && (child.enabled ?? true)` — so toggling a child + * only matters while its container is enabled. Is a no-op when no + * customization (container or child) has the given `id`. * * @category Session Actions * @version 1 @@ -285,9 +288,9 @@ export interface SessionCustomizationsChangedAction { */ export interface SessionCustomizationToggledAction { type: ActionType.SessionCustomizationToggled; - /** The id of the container to toggle. */ + /** The id of the container or child to toggle. */ id: string; - /** Whether to enable or disable the container. */ + /** Whether to enable or disable the targeted customization. */ enabled: boolean; } diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index c33766ef..1b495db2 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -168,12 +168,33 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (!list) { return state; } - const idx = list.findIndex(c => c.id === action.id); - if (idx < 0) { + const topIdx = list.findIndex(c => c.id === action.id); + if (topIdx >= 0) { + const updated = list.slice(); + updated[topIdx] = { ...list[topIdx], enabled: action.enabled }; + return { ...state, customizations: updated }; + } + let changed = false; + const updated = list.map(container => { + if (container.type === CustomizationType.McpServer) { + return container; + } + const children = container.children; + if (!children) { + return container; + } + const childIdx = children.findIndex(c => c.id === action.id); + if (childIdx < 0) { + return container; + } + changed = true; + const newChildren = children.slice(); + newChildren[childIdx] = { ...children[childIdx], enabled: action.enabled }; + return { ...container, children: newChildren }; + }); + if (!changed) { return state; } - const updated = [...list]; - updated[idx] = { ...list[idx], enabled: action.enabled }; return { ...state, customizations: updated }; } diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 1cacfcec..e97853d8 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -639,6 +639,35 @@ export interface DirectoryCustomization extends ContainerCustomizationBase { writable: boolean; } +/** + * Fields shared by the leaf child customizations that live inside a + * container — {@link AgentCustomization}, {@link SkillCustomization}, + * {@link PromptCustomization}, {@link RuleCustomization}, and + * {@link HookCustomization}. + * + * {@link McpServerCustomization} is also a child but does not extend this + * base: it always carries an explicit {@link McpServerCustomization.enabled} + * because it can appear as a top-level customization too. + * + * @category Customization Types + */ +interface ChildCustomizationBase extends CustomizationBase { + /** + * Whether this child is individually enabled. Absent means enabled, so a + * producer only needs to set it to surface a child that exists but is + * turned off on its own. + * + * This flag is independent of the parent container's: the **effective** + * enabled state of a child is + * `container.enabled && (child.enabled ?? true)`, so a disabled container + * disables every child regardless of each child's own flag. + * + * A child is turned on or off by id with + * {@link SessionCustomizationToggledAction | `session/customizationToggled`}. + */ + enabled?: boolean; +} + /** * A custom agent contributed by a plugin or directory. * @@ -648,13 +677,25 @@ export interface DirectoryCustomization extends ContainerCustomizationBase { * * @category Customization Types */ -export interface AgentCustomization extends CustomizationBase { +export interface AgentCustomization extends ChildCustomizationBase { type: CustomizationType.Agent; /** * Short description of what the agent specializes in and when to * invoke it. Sourced from the agent file's frontmatter `description`. */ description?: string; + /** + * When `true`, the agent will not auto-delegate to this custom agent + * as a sub-agent; it can only be selected by the user. Absent or + * `false` means the agent may delegate to it. + */ + disableModelInvocation?: boolean; + /** + * When `true`, the user cannot select this custom agent (for example, + * in a picker); it remains available for the agent to auto-delegate + * to. Absent or `false` means the user may select it. + */ + disableUserInvocation?: boolean; /** * Additional provider-specific metadata for this custom agent. * @@ -673,7 +714,7 @@ export interface AgentCustomization extends CustomizationBase { * * @category Customization Types */ -export interface SkillCustomization extends CustomizationBase { +export interface SkillCustomization extends ChildCustomizationBase { type: CustomizationType.Skill; /** * Short description used for help text and auto-invocation matching. @@ -686,6 +727,12 @@ export interface SkillCustomization extends CustomizationBase { * `disable-model-invocation` flag. */ disableModelInvocation?: boolean; + /** + * When `true`, the user cannot directly invoke this skill (for example, + * as a slash command); it remains available for the agent to + * auto-invoke. Absent or `false` means the user may invoke it. + */ + disableUserInvocation?: boolean; } /** @@ -693,7 +740,7 @@ export interface SkillCustomization extends CustomizationBase { * * @category Customization Types */ -export interface PromptCustomization extends CustomizationBase { +export interface PromptCustomization extends ChildCustomizationBase { type: CustomizationType.Prompt; /** Short description of what the prompt does. */ description?: string; @@ -712,7 +759,7 @@ export interface PromptCustomization extends CustomizationBase { * * @category Customization Types */ -export interface RuleCustomization extends CustomizationBase { +export interface RuleCustomization extends ChildCustomizationBase { type: CustomizationType.Rule; /** * Description of what the rule enforces. @@ -736,7 +783,7 @@ export interface RuleCustomization extends CustomizationBase { * * @category Customization Types */ -export interface HookCustomization extends CustomizationBase { +export interface HookCustomization extends ChildCustomizationBase { type: CustomizationType.Hook; } diff --git a/types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json b/types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json new file mode 100644 index 00000000..839c8155 --- /dev/null +++ b/types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json @@ -0,0 +1,96 @@ +{ + "description": "session/customizationToggled toggles a child by id", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "customizations": [ + { + "type": "mcpServer", + "id": "mcp-top", + "uri": "https://mcp.example/server", + "name": "Top-level Server", + "enabled": true, + "state": { + "kind": "ready" + } + }, + { + "type": "plugin", + "id": "plugin-a", + "uri": "https://plugins.example/a", + "name": "Plugin A", + "enabled": true, + "children": [ + { + "type": "skill", + "id": "skill-1", + "uri": "https://plugins.example/a#skills/lint", + "name": "lint", + "disableUserInvocation": true + }, + { + "type": "agent", + "id": "agent-1", + "uri": "https://plugins.example/a#agents/reviewer", + "name": "reviewer" + } + ] + } + ], + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/customizationToggled", + "id": "skill-1", + "enabled": false + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "customizations": [ + { + "type": "mcpServer", + "id": "mcp-top", + "uri": "https://mcp.example/server", + "name": "Top-level Server", + "enabled": true, + "state": { + "kind": "ready" + } + }, + { + "type": "plugin", + "id": "plugin-a", + "uri": "https://plugins.example/a", + "name": "Plugin A", + "enabled": true, + "children": [ + { + "type": "skill", + "id": "skill-1", + "uri": "https://plugins.example/a#skills/lint", + "name": "lint", + "enabled": false, + "disableUserInvocation": true + }, + { + "type": "agent", + "id": "agent-1", + "uri": "https://plugins.example/a#agents/reviewer", + "name": "reviewer" + } + ] + } + ], + "activeClients": [], + "chats": [] + } +} diff --git a/types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json b/types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json new file mode 100644 index 00000000..54924146 --- /dev/null +++ b/types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json @@ -0,0 +1,61 @@ +{ + "description": "session/customizationToggled is no-op for unknown child id", + "reducer": "session", + "initial": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "customizations": [ + { + "type": "plugin", + "id": "plugin-a", + "uri": "https://plugins.example/a", + "name": "Plugin A", + "enabled": true, + "children": [ + { + "type": "skill", + "id": "skill-1", + "uri": "https://plugins.example/a#skills/lint", + "name": "lint" + } + ] + } + ], + "activeClients": [], + "chats": [] + }, + "actions": [ + { + "type": "session/customizationToggled", + "id": "does-not-exist", + "enabled": false + } + ], + "expected": { + "provider": "copilot", + "title": "Test Session", + "status": 1, + "lifecycle": "ready", + "customizations": [ + { + "type": "plugin", + "id": "plugin-a", + "uri": "https://plugins.example/a", + "name": "Plugin A", + "enabled": true, + "children": [ + { + "type": "skill", + "id": "skill-1", + "uri": "https://plugins.example/a#skills/lint", + "name": "lint" + } + ] + } + ], + "activeClients": [], + "chats": [] + } +} From edf834627a6b19b1610716760f17d4b1edbb64ff Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 30 Jun 2026 07:59:48 -0500 Subject: [PATCH 2/3] refactor(reducers): avoid throwaway array alloc in child toggle no-op The SessionCustomizationToggled child-search fallback in the canonical TypeScript reducer and the hand-written Kotlin reducer built a full copy of the customizations list via map() even when no child matched, then discarded it. Replace the map()+changed-flag with an early-return loop that scans in place and only copies the list (and the matched container's children) when a child id actually matches, returning the original state untouched on the no-op path. No behavioral change: same outputs for every fixture (060/061/062/225/ 226), fewer allocations on the no-match path. The Rust, Go, and Swift reducers already used early-return loops and are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../microsoft/agenthostprotocol/Reducers.kt | 25 ++++++++----------- types/channels-session/reducer.ts | 20 +++++++-------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index fde5abcf..7f0e6937 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -557,21 +557,18 @@ public fun sessionReducer(state: SessionState, action: StateAction): SessionStat val updated = list.toMutableList() updated[idx] = withCustomizationEnabled(updated[idx], a.enabled) state.copy(customizations = updated) - } else { - var changed = false - val updated = list.map { container -> - val children = customizationChildren(container) - if (children == null) container else { - val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } - if (childIdx < 0) container else { - changed = true - val newChildren = children.toMutableList() - newChildren[childIdx] = withChildCustomizationEnabled(newChildren[childIdx], a.enabled) - withCustomizationChildren(container, newChildren) - } - } + } else run { + for (i in list.indices) { + val children = customizationChildren(list[i]) ?: continue + val childIdx = children.indexOfFirst { childCustomizationId(it) == a.id } + if (childIdx < 0) continue + val newChildren = children.toMutableList() + newChildren[childIdx] = withChildCustomizationEnabled(newChildren[childIdx], a.enabled) + val updated = list.toMutableList() + updated[i] = withCustomizationChildren(list[i], newChildren) + return@run state.copy(customizations = updated) } - if (!changed) state else state.copy(customizations = updated) + state } } } diff --git a/types/channels-session/reducer.ts b/types/channels-session/reducer.ts index 1b495db2..6763d2ac 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -174,28 +174,26 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: updated[topIdx] = { ...list[topIdx], enabled: action.enabled }; return { ...state, customizations: updated }; } - let changed = false; - const updated = list.map(container => { + for (let i = 0; i < list.length; i++) { + const container = list[i]; if (container.type === CustomizationType.McpServer) { - return container; + continue; } const children = container.children; if (!children) { - return container; + continue; } const childIdx = children.findIndex(c => c.id === action.id); if (childIdx < 0) { - return container; + continue; } - changed = true; const newChildren = children.slice(); newChildren[childIdx] = { ...children[childIdx], enabled: action.enabled }; - return { ...container, children: newChildren }; - }); - if (!changed) { - return state; + const updated = list.slice(); + updated[i] = { ...container, children: newChildren }; + return { ...state, customizations: updated }; } - return { ...state, customizations: updated }; + return state; } case ActionType.SessionCustomizationUpdated: { From 211ad0ca56a7d965a71103aeb00845c6367ffdcd Mon Sep 17 00:00:00 2001 From: Colby Williams Date: Tue, 30 Jun 2026 11:56:53 -0500 Subject: [PATCH 3/3] docs(customizations): clarify toggle matches any top-level customization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SessionCustomizationToggled doc said the action targets a "top-level container (plugin or directory)", but the reducer matches `id` against every top-level customization first — including a bare top-level MCP server — before searching container children. Reword the action JSDoc and the customizations guide (intro, code comment, prose, and action table) so they no longer imply top-level MCP servers can't be toggled by id. Regenerated the Go/Rust type mirrors and JSON schema from the JSDoc. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- clients/go/ahptypes/actions.generated.go | 9 +++++---- clients/rust/crates/ahp-types/src/actions.rs | 9 +++++---- docs/guide/customizations.md | 8 ++++---- schema/actions.schema.json | 2 +- schema/commands.schema.json | 2 +- types/channels-session/actions.ts | 9 +++++---- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index a2987583..9d447abd 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -776,13 +776,14 @@ type SessionCustomizationsChangedAction struct { // A client toggled a customization on or off. // -// Targets either a top-level container (plugin or directory) or an -// individual child (a skill, agent, or other entry inside a container) by -// `id`, and sets that entry's `enabled` flag. Disabling a container still +// Matches `id` against every top-level customization first — a plugin or +// directory container, or a bare top-level MCP server — then against the +// children inside each container (a skill, agent, or other entry), and +// sets the matched entry's `enabled` flag. Disabling a container still // disables all of its children — the effective state of a child is // `container.enabled && (child.enabled ?? true)` — so toggling a child // only matters while its container is enabled. Is a no-op when no -// customization (container or child) has the given `id`. +// customization has the given `id`. type SessionCustomizationToggledAction struct { Type ActionType `json:"type"` // The id of the container or child to toggle. diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index de65f385..85089b2d 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -940,13 +940,14 @@ pub struct SessionCustomizationsChangedAction { /// A client toggled a customization on or off. /// -/// Targets either a top-level container (plugin or directory) or an -/// individual child (a skill, agent, or other entry inside a container) by -/// `id`, and sets that entry's `enabled` flag. Disabling a container still +/// Matches `id` against every top-level customization first — a plugin or +/// directory container, or a bare top-level MCP server — then against the +/// children inside each container (a skill, agent, or other entry), and +/// sets the matched entry's `enabled` flag. Disabling a container still /// disables all of its children — the effective state of a child is /// `container.enabled && (child.enabled ?? true)` — so toggling a child /// only matters while its container is enabled. Is a no-op when no -/// customization (container or child) has the given `id`. +/// customization has the given `id`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionCustomizationToggledAction { diff --git a/docs/guide/customizations.md b/docs/guide/customizations.md index ba9347f6..0af3130c 100644 --- a/docs/guide/customizations.md +++ b/docs/guide/customizations.md @@ -131,17 +131,17 @@ state.customizations ## Toggling -Any client can enable or disable a container or an individual child by dispatching `session/customizationToggled` with that entry's `id`: +Any client can enable or disable any customization by dispatching `session/customizationToggled` with that entry's `id`: ```typescript { type: 'session/customizationToggled' - id: string // container or child id + id: string // any customization id enabled: boolean } ``` -Both containers and children carry an `enabled` flag. The reducer matches `id` against the top-level containers first, then the children inside every container, and sets that entry's `enabled`. A child's effective state is `container.enabled && (child.enabled ?? true)`, so disabling a container disables all of its children regardless of each child's own flag, and a child toggle only takes effect while its container is enabled. The action is a no-op if no container or child has that id. +Both containers and children carry an `enabled` flag. The reducer matches `id` against every top-level customization first — plugins, directories, and bare top-level MCP servers — then against the children inside every container, and sets that entry's `enabled`. A child's effective state is `container.enabled && (child.enabled ?? true)`, so disabling a container disables all of its children regardless of each child's own flag, and a child toggle only takes effect while its container is enabled. The action is a no-op if no customization has that id. ```mermaid sequenceDiagram @@ -387,7 +387,7 @@ sequenceDiagram | Type | Client-dispatchable? | When | |---|---|---| | `session/customizationsChanged` | No | Server replaced the top-level customization list (full replacement) | -| `session/customizationToggled` | **Yes** | Client toggled a container or child on or off by id | +| `session/customizationToggled` | **Yes** | Client toggled a customization on or off by id | | `session/customizationUpdated` | No | Server upserts a top-level container by id (full-entry replacement, including children) | | `session/customizationRemoved` | No | Server removes a customization by id (containers cascade) | | `session/activeClientSet` | **Yes** | Client joins or refreshes as an active client (with tools + customizations), keyed by `clientId` | diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 74216f2e..f63358eb 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -427,7 +427,7 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "description": "A client toggled a customization on or off.\n\nTargets either a top-level container (plugin or directory) or an\nindividual child (a skill, agent, or other entry inside a container) by\n`id`, and sets that entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization (container or child) has the given `id`.", + "description": "A client toggled a customization on or off.\n\nMatches `id` against every top-level customization first — a plugin or\ndirectory container, or a bare top-level MCP server — then against the\nchildren inside each container (a skill, agent, or other entry), and\nsets the matched entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization has the given `id`.", "properties": { "type": { "$ref": "#/$defs/ActionType.SessionCustomizationToggled" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index c5c488df..d0b89be7 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -5884,7 +5884,7 @@ }, "SessionCustomizationToggledAction": { "type": "object", - "description": "A client toggled a customization on or off.\n\nTargets either a top-level container (plugin or directory) or an\nindividual child (a skill, agent, or other entry inside a container) by\n`id`, and sets that entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization (container or child) has the given `id`.", + "description": "A client toggled a customization on or off.\n\nMatches `id` against every top-level customization first — a plugin or\ndirectory container, or a bare top-level MCP server — then against the\nchildren inside each container (a skill, agent, or other entry), and\nsets the matched entry's `enabled` flag. Disabling a container still\ndisables all of its children — the effective state of a child is\n`container.enabled && (child.enabled ?? true)` — so toggling a child\nonly matters while its container is enabled. Is a no-op when no\ncustomization has the given `id`.", "properties": { "type": { "$ref": "#/$defs/ActionType.SessionCustomizationToggled" diff --git a/types/channels-session/actions.ts b/types/channels-session/actions.ts index f84acd0d..8de02ec0 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -274,13 +274,14 @@ export interface SessionCustomizationsChangedAction { /** * A client toggled a customization on or off. * - * Targets either a top-level container (plugin or directory) or an - * individual child (a skill, agent, or other entry inside a container) by - * `id`, and sets that entry's `enabled` flag. Disabling a container still + * Matches `id` against every top-level customization first — a plugin or + * directory container, or a bare top-level MCP server — then against the + * children inside each container (a skill, agent, or other entry), and + * sets the matched entry's `enabled` flag. Disabling a container still * disables all of its children — the effective state of a child is * `container.enabled && (child.enabled ?? true)` — so toggling a child * only matters while its container is enabled. Is a no-op when no - * customization (container or child) has the given `id`. + * customization has the given `id`. * * @category Session Actions * @version 1