diff --git a/CHANGELOG.md b/CHANGELOG.md index db2e5dcd..7cd99323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,23 @@ changes accumulate. Track in-flight protocol changes via PRs touching Spec version: `0.5.2` +### Added + +- 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 any top-level customization + (`plugin`, `directory`, or top-level `mcpServer`) 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] — 2026-07-02 Spec version: `0.5.1` diff --git a/clients/go/CHANGELOG.md b/clients/go/CHANGELOG.md index c9de4004..75f985ac 100644 --- a/clients/go/CHANGELOG.md +++ b/clients/go/CHANGELOG.md @@ -14,6 +14,24 @@ tag whose matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +## [0.5.2] — Unreleased + +Implements AHP 0.5.2. + +### Added + +- 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 any top-level + customization (`plugin`, `directory`, or top-level `mcpServer`) or an + individual child by `id`, setting that entry's `Enabled`. + ## [0.5.1] — 2026-07-02 Implements AHP 0.5.1. diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index 6badfc05..41f047d1 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -324,6 +324,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]) @@ -332,6 +349,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 14c6382d..e39d7d0b 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -824,17 +824,21 @@ 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. +// +// 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 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 1f7e5659..878c7787 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -2206,8 +2206,20 @@ 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"` @@ -2223,6 +2235,14 @@ type AgentCustomization struct { // field rather than sending an empty array, so an empty list carries no // meaning distinct from absence. Tools []string `json:"tools,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. @@ -2256,8 +2276,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"` @@ -2265,6 +2297,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. @@ -2289,8 +2325,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"` } @@ -2325,8 +2373,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). @@ -2360,8 +2420,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 a4f47a30..c58f26c6 100644 --- a/clients/kotlin/CHANGELOG.md +++ b/clients/kotlin/CHANGELOG.md @@ -15,6 +15,24 @@ versions (`*-SNAPSHOT`) are explicitly rejected by the publish pipeline; bump ## [Unreleased] +## [0.5.2] — Unreleased + +Implements AHP 0.5.2. + +### Added + +- 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 any top-level + customization (`plugin`, `directory`, or top-level `mcpServer`) or an + individual child by `id`, setting that entry's `enabled`. + ## [0.5.1] — 2026-07-02 Implements AHP 0.5.1. 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 16bcad54..f6192769 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -235,6 +235,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 @@ -597,10 +607,22 @@ 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 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) + } + state } } } 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 e503242d..7375ff93 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 @@ -915,11 +915,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 baeee461..dd5fd328 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 @@ -3111,6 +3111,20 @@ 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 @@ -3133,6 +3147,18 @@ data class AgentCustomization( * meaning distinct from absence. */ val tools: List? = 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. * @@ -3175,6 +3201,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. @@ -3186,7 +3226,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 @@ -3222,6 +3268,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. @@ -3262,6 +3322,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. @@ -3313,6 +3387,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 81631721..2dc2620e 100644 --- a/clients/rust/CHANGELOG.md +++ b/clients/rust/CHANGELOG.md @@ -15,6 +15,25 @@ matching `## [X.Y.Z]` heading is missing from this file. ## [Unreleased] +## [0.5.2] — Unreleased + +Implements AHP 0.5.2. + +### Added + +- 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 any top-level + customization (`plugin`, `directory`, or top-level `mcpServer`) or an + individual child by `id`, setting that entry's `enabled`. + ## [0.5.1] — 2026-07-02 Implements AHP 0.5.1. diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index 5aaf0357..9664c396 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -978,18 +978,22 @@ 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. +/// 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 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 c41db60f..98a07dcf 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -2732,6 +2732,19 @@ 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")] @@ -2750,6 +2763,16 @@ pub struct AgentCustomization { /// meaning distinct from absence. #[serde(default, skip_serializing_if = "Option::is_none")] pub tools: 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. @@ -2789,6 +2812,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")] @@ -2798,6 +2834,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. @@ -2827,6 +2868,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, @@ -2867,6 +2921,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, @@ -2908,6 +2975,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 3f628556..fd05f71c 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -426,11 +426,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 551ddd40..24504f49 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -1170,9 +1170,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 af555275..8c516563 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -3318,6 +3318,18 @@ 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`. @@ -3334,6 +3346,14 @@ public struct AgentCustomization: Codable, Sendable { /// field rather than sending an empty array, so an empty list carries no /// meaning distinct from absence. public var tools: [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. @@ -3345,10 +3365,13 @@ public struct AgentCustomization: Codable, Sendable { case name case icons case range + case enabled case type case description case model case tools + case disableModelInvocation + case disableUserInvocation case meta = "_meta" } @@ -3358,10 +3381,13 @@ public struct AgentCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil, model: String? = nil, tools: [String]? = nil, + disableModelInvocation: Bool? = nil, + disableUserInvocation: Bool? = nil, meta: [String: AnyCodable]? = nil ) { self.id = id @@ -3369,10 +3395,13 @@ public struct AgentCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description self.model = model self.tools = tools + self.disableModelInvocation = disableModelInvocation + self.disableUserInvocation = disableUserInvocation self.meta = meta } } @@ -3399,6 +3428,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`. @@ -3407,6 +3448,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, @@ -3414,18 +3459,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 } } @@ -3451,6 +3500,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? @@ -3461,6 +3522,7 @@ public struct PromptCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType, description: String? = nil ) { @@ -3469,6 +3531,7 @@ public struct PromptCustomization: Codable, Sendable { self.name = name self.icons = icons self.range = range + self.enabled = enabled self.type = type self.description = description } @@ -3496,6 +3559,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? @@ -3513,6 +3588,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, @@ -3523,6 +3599,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 @@ -3552,6 +3629,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( @@ -3560,6 +3649,7 @@ public struct HookCustomization: Codable, Sendable { name: String, icons: [Icon]? = nil, range: TextRange? = nil, + enabled: Bool? = nil, type: CustomizationType ) { self.id = id @@ -3567,6 +3657,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 dd303744..78aa42b2 100644 --- a/clients/swift/CHANGELOG.md +++ b/clients/swift/CHANGELOG.md @@ -17,6 +17,24 @@ the tag matches the version pinned in [`VERSION`](VERSION). ## [Unreleased] +## [0.5.2] — Unreleased + +Implements AHP 0.5.2. + +### Added + +- 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 any top-level + customization (`plugin`, `directory`, or top-level `mcpServer`) or an + individual child by `id`, setting that entry's `enabled`. + ## [0.5.1] — 2026-07-02 Implements AHP 0.5.1. diff --git a/clients/typescript/CHANGELOG.md b/clients/typescript/CHANGELOG.md index 731c641d..f1774f66 100644 --- a/clients/typescript/CHANGELOG.md +++ b/clients/typescript/CHANGELOG.md @@ -20,6 +20,24 @@ hotfix escape hatch. ## [Unreleased] +## [0.5.2] — Unreleased + +Implements AHP 0.5.2. + +### Added + +- 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 any top-level + customization (`plugin`, `directory`, or top-level `mcpServer`) or an + individual child by `id`, setting that entry's `enabled`. + ## [0.5.1] — 2026-07-02 Implements AHP 0.5.1. 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 d17af9a4..e3325cf9 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?, model?, tools? } -SkillCustomization { type: 'skill'; description?, disableModelInvocation? } +AgentCustomization { type: 'agent'; description?, model?, tools?, 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 any customization by dispatching `session/customizationToggled` with that entry's `id`: ```typescript { type: 'session/customizationToggled' - id: string // container id + id: string // any customization 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 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 @@ -385,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 f44dcefb..54030ef2 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -461,18 +461,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\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" }, "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": [ @@ -3559,6 +3559,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.", @@ -3586,6 +3624,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" }, @@ -3604,6 +3646,14 @@ }, "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." }, + "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": {}, @@ -3644,6 +3694,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" }, @@ -3654,6 +3708,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": [ @@ -3690,6 +3748,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" }, @@ -3732,6 +3794,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" }, @@ -3785,6 +3851,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 523db733..f260b622 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -2871,6 +2871,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.", @@ -2898,6 +2936,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" }, @@ -2916,6 +2958,14 @@ }, "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." }, + "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": {}, @@ -2956,6 +3006,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" }, @@ -2966,6 +3020,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": [ @@ -3002,6 +3060,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" }, @@ -3044,6 +3106,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" }, @@ -3097,6 +3163,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" } @@ -6064,18 +6134,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\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" }, "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 372ea9af..53234985 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -1695,6 +1695,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.", @@ -1722,6 +1760,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 @@ }, "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." }, + "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/notifications.schema.json b/schema/notifications.schema.json index 36d68856..f553faab 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -1855,6 +1855,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.", @@ -1882,6 +1920,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" }, @@ -1900,6 +1942,14 @@ }, "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." }, + "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": {}, @@ -1940,6 +1990,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" }, @@ -1950,6 +2004,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": [ @@ -1986,6 +2044,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" }, @@ -2028,6 +2090,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" }, @@ -2081,6 +2147,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 d0c35738..7f0e638a 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -1606,6 +1606,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.", @@ -1633,6 +1671,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" }, @@ -1651,6 +1693,14 @@ }, "description": "Allowlist of tool names the agent is scoped to, sourced from the\nagent file's frontmatter `tools`. A non-empty list restricts the\nagent to exactly those tools. Absent — or an empty list — imposes no\nrestriction beyond the session default: the agent may use any\navailable tool. Producers express \"no restriction\" by omitting the\nfield rather than sending an empty array, so an empty list carries no\nmeaning distinct from absence." }, + "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": {}, @@ -1691,6 +1741,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" }, @@ -1701,6 +1755,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": [ @@ -1737,6 +1795,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" }, @@ -1779,6 +1841,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" }, @@ -1832,6 +1898,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 6cf40e58..f113b9e4 100644 --- a/types/channels-session/actions.ts +++ b/types/channels-session/actions.ts @@ -317,12 +317,16 @@ 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. + * 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 has the given `id`. * * @category Session Actions * @version 1 @@ -330,9 +334,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 ca0e2985..bdc1252d 100644 --- a/types/channels-session/reducer.ts +++ b/types/channels-session/reducer.ts @@ -220,13 +220,32 @@ export function sessionReducer(state: SessionState, action: SessionAction, log?: if (!list) { return state; } - const idx = list.findIndex(c => c.id === action.id); - if (idx < 0) { - return state; + 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 }; } - const updated = [...list]; - updated[idx] = { ...list[idx], enabled: action.enabled }; - return { ...state, customizations: updated }; + for (let i = 0; i < list.length; i++) { + const container = list[i]; + if (container.type === CustomizationType.McpServer) { + continue; + } + const children = container.children; + if (!children) { + continue; + } + const childIdx = children.findIndex(c => c.id === action.id); + if (childIdx < 0) { + continue; + } + const newChildren = children.slice(); + newChildren[childIdx] = { ...children[childIdx], enabled: action.enabled }; + const updated = list.slice(); + updated[i] = { ...container, children: newChildren }; + return { ...state, customizations: updated }; + } + return state; } case ActionType.SessionCustomizationUpdated: { diff --git a/types/channels-session/state.ts b/types/channels-session/state.ts index 2e875e65..da742f2c 100644 --- a/types/channels-session/state.ts +++ b/types/channels-session/state.ts @@ -791,6 +791,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. * @@ -800,7 +829,7 @@ 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 @@ -823,6 +852,18 @@ export interface AgentCustomization extends CustomizationBase { * meaning distinct from absence. */ tools?: 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. * @@ -841,7 +882,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. @@ -854,6 +895,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; } /** @@ -861,7 +908,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; @@ -880,7 +927,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. @@ -904,7 +951,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": [] + } +}